Browse Source

Make test context optionally type aware for TypeScript (#1298)

master
mmkal 8 years ago
committed by Sindre Sorhus
parent
commit
50ad21372c
  1. 31
      docs/recipes/typescript.md
  2. 2
      readme.md
  3. 37
      types/base.d.ts
  4. 79
      types/make.js

31
docs/recipes/typescript.md

@ -6,7 +6,7 @@ AVA comes bundled with a TypeScript definition file. This allows developers to l
## Setup
First install [TypeScript](https://github.com/Microsoft/TypeScript).
First install [TypeScript](https://github.com/Microsoft/TypeScript) (if you already have it installed, make sure you use version 2.1 or greater).
```
$ npm install --save-dev typescript
@ -50,6 +50,35 @@ test(async (t) => {
});
```
## Working with [`context`](https://github.com/avajs/ava#test-context)
By default, the type of `t.context` will be [`any`](https://www.typescriptlang.org/docs/handbook/basic-types.html#any). AVA exposes an interface `RegisterContextual<T>` which you can use to apply your own type to `t.context`. This can help you catch errors at compile-time:
```ts
import * as ava from 'ava';
function contextualize<T>(getContext: () => T): ava.RegisterContextual<T> {
ava.test.beforeEach(t => {
Object.assign(t.context, getContext());
});
return ava.test;
}
const test = contextualize(() => ({ foo: 'bar' }));
test.beforeEach(t => {
t.context.foo = 123; // error: Type '123' is not assignable to type 'string'
});
test.after.always.failing.cb.serial('very long chains are properly typed', t => {
t.context.fooo = 'a value'; // error: Property 'fooo' does not exist on type '{ foo: string }'
});
test('an actual test', t => {
t.deepEqual(t.context.foo.map(c => c), ['b', 'a', 'r']); // error: Property 'map' does not exist on type 'string'
});
```
## Execute the tests

2
readme.md

@ -578,6 +578,8 @@ Keep in mind that the `beforeEach` and `afterEach` hooks run just before and aft
Remember that AVA runs each test file in its own process. You may not have to clean up global state in a `after`-hook since that's only called right before the process exits.
#### Test context
The `beforeEach` & `afterEach` hooks can share context with the test:
```js

37
types/base.d.ts

@ -1,5 +1,3 @@
export default test;
export type ErrorValidator
= (new (...args: any[]) => any)
| RegExp
@ -9,11 +7,16 @@ export type ErrorValidator
export interface Observable {
subscribe(observer: (value: {}) => void): void;
}
export type Test = (t: TestContext) => PromiseLike<void> | Iterator<any> | Observable | void;
export type ContextualTest = (t: ContextualTestContext) => PromiseLike<void> | Iterator<any> | Observable | void;
export type GenericTest<T> = (t: GenericTestContext<T>) => PromiseLike<void> | Iterator<any> | Observable | void;
export type CallbackTest = (t: CallbackTestContext) => void;
export type ContextualCallbackTest = (t: ContextualCallbackTestContext) => void;
export type GenericCallbackTest<T> = (t: GenericCallbackTestContext<T>) => void;
export interface Context<T> { context: T }
export type AnyContext = Context<any>;
export type ContextualTest = GenericTest<AnyContext>;
export type ContextualCallbackTest = GenericCallbackTest<AnyContext>;
export interface AssertContext {
/**
@ -99,12 +102,9 @@ export interface CallbackTestContext extends TestContext {
*/
end(): void;
}
export interface ContextualTestContext extends TestContext {
context: any;
}
export interface ContextualCallbackTestContext extends CallbackTestContext {
context: any;
}
export type GenericTestContext<T> = TestContext & T;
export type GenericCallbackTestContext<T> = CallbackTestContext & T;
export interface Macro<T> {
(t: T, ...args: any[]): void;
@ -112,7 +112,14 @@ export interface Macro<T> {
}
export type Macros<T> = Macro<T> | Macro<T>[];
export function test(name: string, run: ContextualTest): void;
export function test(run: ContextualTest): void;
export function test(name: string, run: Macros<ContextualTestContext>, ...args: any[]): void;
export function test(run: Macros<ContextualTestContext>, ...args: any[]): void;
interface RegisterBase<T> {
(name: string, run: GenericTest<T>): void;
(run: GenericTest<T>): void;
(name: string, run: Macros<GenericTestContext<T>>, ...args: any[]): void;
(run: Macros<GenericTestContext<T>>, ...args: any[]): void;
}
export default test;
export const test: RegisterContextual<any>;
export interface RegisterContextual<T> extends Register<Context<T>> {
}

79
types/make.js

@ -24,7 +24,10 @@ const base = fs.readFileSync(path.join(__dirname, 'base.d.ts'), 'utf8');
// All suported function names
const allParts = Object.keys(runner._chainableMethods).filter(name => name !== 'test');
// The output consists of the base declarations, the actual 'test' function declarations,
// and the namespaced chainable methods.
const output = base + generatePrefixed([]);
fs.writeFileSync(path.join(__dirname, 'generated.d.ts'), output);
// Generates type definitions, for the specified prefix
@ -43,10 +46,21 @@ function generatePrefixed(prefix) {
// If `parts` is not sorted, we alias it to the sorted chain
if (!isArraySorted(parts)) {
const chain = parts.sort().join('.');
if (exists(parts)) {
output += `\texport const ${part}: typeof test.${chain};\n`;
parts.sort();
let chain;
if (hasChildren(parts)) {
chain = parts.join('_') + '<T>';
} else {
// this is a single function, not a namespace, so there's no type associated
// and we need to dereference it as a property type
const last = parts.pop();
const joined = parts.join('_');
chain = `${joined}<T>['${last}']`;
}
output += `\t${part}: Register_${chain};\n`;
}
continue;
@ -56,14 +70,19 @@ function generatePrefixed(prefix) {
// `always` is a valid prefix, for instance of `always.after`,
// but not a valid function name.
if (verify(parts, false)) {
if (parts.indexOf('todo') !== -1) { // eslint-disable-line no-negated-condition
output += '\t' + writeFunction(part, 'name: string', 'void');
if (arrayHas(parts)('todo')) {
// 'todo' functions don't have a function argument, just a string
output += `\t${part}: (name: string) => void;\n`;
} else {
const type = testType(parts);
output += '\t' + writeFunction(part, `name: string, implementation: ${type}`);
output += '\t' + writeFunction(part, `implementation: ${type}`);
output += '\t' + writeFunction(part, `name: string, implementation: Macros<${type}Context>, ...args: any[]`);
output += '\t' + writeFunction(part, `implementation: Macros<${type}Context>, ...args: any[]`);
output += `\t${part}: RegisterBase<T>`;
if (hasChildren(parts)) {
// this chain can be continued, make the property an intersection type with the chain continuation
const joined = parts.join('_');
output += ` & Register_${joined}<T>`;
}
output += ';\n';
}
}
@ -74,13 +93,14 @@ function generatePrefixed(prefix) {
return children;
}
const namespace = ['test'].concat(prefix).join('.');
const typeBody = `{\n${output}}\n${children}`;
return `export namespace ${namespace} {\n${output}}\n${children}`;
}
function writeFunction(name, args) {
return `export function ${name}(${args}): void;\n`;
if (prefix.length === 0) {
// no prefix, so this is the type for the default export
return `export interface Register<T> extends RegisterBase<T> ${typeBody}`;
}
const namespace = ['Register'].concat(prefix).join('_');
return `interface ${namespace}<T> ${typeBody}`;
}
// Checks whether a chain is a valid function name (when `asPrefix === false`)
@ -126,6 +146,17 @@ function verify(parts, asPrefix) {
return true;
}
// Returns true if a chain can have any child properties
function hasChildren(parts) {
// concatenate the chain with each other part, and see if any concatenations are valid functions
const validChildren = allParts
.filter(newPart => parts.indexOf(newPart) === -1)
.map(newPart => parts.concat([newPart]))
.filter(longer => verify(longer, false));
return validChildren.length > 0;
}
// Checks whether a chain is a valid function name or a valid prefix with some member
function exists(parts) {
if (verify(parts, false)) {
@ -147,19 +178,3 @@ function exists(parts) {
return false;
}
// Returns the type name of for the test implementation
function testType(parts) {
const has = arrayHas(parts);
let type = 'Test';
if (has('cb')) {
type = `Callback${type}`;
}
if (!has('before') && !has('after')) {
type = `Contextual${type}`;
}
return type;
}

Loading…
Cancel
Save