diff --git a/docs/recipes/flow.md b/docs/recipes/flow.md new file mode 100644 index 000000000..7c2189e2f --- /dev/null +++ b/docs/recipes/flow.md @@ -0,0 +1,83 @@ +# Flow + +AVA comes bundled with a Flow definition file. This allows developers to leverage Flow for writing tests. + +This guide assumes you've already set up Flow for your project. Note that AVA's definition as been tested with version 0.64. + +We recommend you use AVA's built-in Babel pipeline to strip Flow type annotations and declarations. AVA automatically applies your project's Babel configuration, so everything may just work without changes. Alternatively install [`@babel/plugin-transform-flow-strip-types`](https://www.npmjs.com/package/@babel/plugin-transform-flow-strip-types) and customize AVA's configuration in the `package.json` file as follows: + +```json +{ + "ava": { + "babel": { + "testOptions": { + "plugins": ["@babel/plugin-transform-flow-strip-types"] + } + } + } +} +``` + +See our [Babel documentation](babel.md) for more details. + +## Writing tests + +Create a `test.js` file. + +```js +// @flow +import test from 'ava'; + +const fn = async () => Promise.resolve('foo'); + +test(async (t) => { + t.is(await fn(), 'foo'); +}); +``` + +## Typing [`t.context`](https://github.com/avajs/ava#test-context) + +By default, the type of `t.context` will be the empty object (`{}`). AVA exposes an interface `TestInterface` which you can use to apply your own type to `t.context`. This can help you catch errors at compile-time: + +```js +// @flow +import anyTest from 'ava'; +import type {TestInterface} from 'ava'; + +const test: TestInterface<{foo: string}> = (anyTest: any); + +test.beforeEach(t => { + t.context = {foo: 'bar'}; +}); + +test.beforeEach(t => { + t.context.foo = 123; // error: Type '123' is not assignable to type 'string' +}); + +test.serial.cb.failing('very long chains are properly typed', t => { + t.context.fooo = 'a value'; // error: Property 'fooo' does not exist on type '' +}); + +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' +}); +``` + +Note that, despite the type cast above, when executing `t.context` is an empty object unless it's assigned. + +## Using `t.throws()` and `t.notThrows()` + +The `t.throws()` and `t.noThrows()` assertions can be called with a function that returns an observable or a promise. You may have to explicitly type functions: + +```ts +import test from 'ava'; + +test('just throws', async t => { + const expected = new Error(); + const err = t.throws((): void => { throw expected; }); + t.is(err, expected); + + const err2 = await t.throws((): Promise<*> => Promise.reject(expected)); + t.is(err2, expected); +}); +``` diff --git a/docs/recipes/typescript.md b/docs/recipes/typescript.md index 04becf4df..f41b90092 100644 --- a/docs/recipes/typescript.md +++ b/docs/recipes/typescript.md @@ -4,26 +4,7 @@ Translations: [Español](https://github.com/avajs/ava-docs/blob/master/es_ES/doc AVA comes bundled with a TypeScript definition file. This allows developers to leverage TypeScript for writing tests. - -## Setup - -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 -``` - -Create a [`tsconfig.json`](http://www.typescriptlang.org/docs/handbook/tsconfig-json.html) file. This file specifies the compiler options required to compile the project or the test file. - -```json -{ - "compilerOptions": { - "module": "commonjs", - "target": "es2015", - "sourceMap": true - } -} -``` +This guide assumes you've already set up TypeScript for your project. Note that AVA's definition has been tested with version 2.7.1. Add a `test` script in the `package.json` file. It will compile the project first and then run AVA. @@ -35,8 +16,9 @@ Add a `test` script in the `package.json` file. It will compile the project firs } ``` +Make sure that AVA runs your built TypeScript files. -## Add tests +## Writing tests Create a `test.ts` file. @@ -50,47 +32,55 @@ test(async (t) => { }); ``` -## Working with [macros](https://github.com/avajs/ava#test-macros) +## Using [macros](https://github.com/avajs/ava#test-macros) -In order to be able to assign the `title` property to a macro: +In order to be able to assign the `title` property to a macro you need to type the function: ```ts -import test, { AssertContext, Macro } from 'ava'; +import test, {Macro} from 'ava'; -const macro: Macro = (t, input, expected) => { +const macro: Macro = (t, input: string, expected: number) => { t.is(eval(input), expected); -} - -macro.title = (providedTitle, input, expected) => `${providedTitle} ${input} = ${expected}`.trim(); +}; +macro.title = (providedTitle: string, input: string, expected: number) => `${providedTitle} ${input} = ${expected}`.trim(); test(macro, '2 + 2', 4); test(macro, '2 * 3', 6); test('providedTitle', macro, '3 * 3', 9); ``` -## 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` which you can use to apply your own type to `t.context`. This can help you catch errors at compile-time: +You'll need a different type if you're expecting your macro to be used with a callback test: ```ts -import * as ava from 'ava'; +import test, {CbMacro} from 'ava'; -function contextualize(getContext: () => T): ava.RegisterContextual { - ava.test.beforeEach(t => { - Object.assign(t.context, getContext()); - }); +const macro: CbMacro = t => { + t.pass(); + setTimeout(t.end, 100); +}; - return ava.test; -} +test.cb(macro); +``` -const test = contextualize(() => ({ foo: 'bar' })); +## Typing [`t.context`](https://github.com/avajs/ava#test-context) + +By default, the type of `t.context` will be the empty object (`{}`). AVA exposes an interface `TestInterface` which you can use to apply your own type to `t.context`. This can help you catch errors at compile-time: + +```ts +import anyTest, {TestInterface} from 'ava'; + +const test: TestInterface<{foo: string}> = anyTest; + +test.beforeEach(t => { + t.context = {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.serial.cb.failing('very long chains are properly typed', t => { + t.context.fooo = 'a value'; // error: Property 'fooo' does not exist on type '' }); test('an actual test', t => { @@ -98,9 +88,26 @@ test('an actual test', t => { }); ``` +You can also type the context when creating macros: -## Execute the tests +```ts +import anyTest, {Macro, TestInterface} from 'ava'; +interface Context { + foo: string +} + +const test: TestInterface = anyTest; + +const macro: Macro = (t, expected: string) => { + t.is(t.context.foo, expected); +}; + +test.beforeEach(t => { + t.context = {foo: 'bar'}; +}); + +test('foo is bar', macro, 'bar'); ``` -$ npm test -``` + +Note that, despite the type cast above, when executing `t.context` is an empty object unless it's assigned. diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 000000000..1d43ec91b --- /dev/null +++ b/index.d.ts @@ -0,0 +1,199 @@ +export interface ObservableLike { + subscribe(observer: (value: any) => void): void; +} + +export type ThrowsErrorValidator = (new (...args: Array) => any) | RegExp | string | ((error: any) => boolean); + +export interface SnapshotOptions { + id?: string; +} + +export interface Assertions { + deepEqual(actual: ValueType, expected: ValueType, message?: string): void; + fail(message?: string): void; + false(actual: any, message?: string): void; + falsy(actual: any, message?: string): void; + ifError(error: any, message?: string): void; + is(actual: ValueType, expected: ValueType, message?: string): void; + not(actual: ValueType, expected: ValueType, message?: string): void; + notDeepEqual(actual: ValueType, expected: ValueType, message?: string): void; + notRegex(string: string, regex: RegExp, message?: string): void; + notThrows(value: () => never, message?: string): void; + notThrows(value: () => ObservableLike, message?: string): Promise; + notThrows(value: () => PromiseLike, message?: string): Promise; + notThrows(value: () => any, message?: string): void; + notThrows(value: ObservableLike, message?: string): Promise; + notThrows(value: PromiseLike, message?: string): Promise; + pass(message?: string): void; + regex(string: string, regex: RegExp, message?: string): void; + snapshot(expected: any, message?: string): void; + snapshot(expected: any, options: SnapshotOptions, message?: string): void; + throws(value: () => never, error?: ThrowsErrorValidator, message?: string): any; + throws(value: () => ObservableLike, error?: ThrowsErrorValidator, message?: string): Promise; + throws(value: () => PromiseLike, error?: ThrowsErrorValidator, message?: string): Promise; + throws(value: () => any, error?: ThrowsErrorValidator, message?: string): any; + throws(value: ObservableLike, error?: ThrowsErrorValidator, message?: string): Promise; + throws(value: PromiseLike, error?: ThrowsErrorValidator, message?: string): Promise; + true(actual: any, message?: string): void; + truthy(actual: any, message?: string): void; +} + +export interface ExecutionContext extends Assertions { + context: Context; + skip: Assertions; + title: string; + log(...values: Array): void; + plan(count: number): void; +} + +export interface CbExecutionContext extends ExecutionContext { + end(): void; +} + +export type ImplementationResult = PromiseLike | ObservableLike | Iterator | void; +export type Implementation = (t: ExecutionContext) => ImplementationResult; +export type CbImplementation = (t: CbExecutionContext) => ImplementationResult; + +export interface Macro { + (t: ExecutionContext, ...args: Array): ImplementationResult; + title?: (providedTitle: string, ...args: Array) => string; +} + +export interface CbMacro { + (t: CbExecutionContext, ...args: Array): ImplementationResult; + title?: (providedTitle: string, ...args: Array) => string; +} + +export interface TestInterface { + (title: string, implementation: Implementation): void; + (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: Macro | Macro[], ...args: Array): void; + + after: AfterInterface; + afterEach: AfterInterface; + before: BeforeInterface; + beforeEach: BeforeInterface; + cb: CbInterface; + failing: FailingInterface; + only: OnlyInterface; + serial: SerialInterface; + skip: SkipInterface; + todo: TodoDeclaration; +} + +export interface AfterInterface { + (title: string, implementation: Implementation): void; + (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: Macro | Macro[], ...args: Array): void; + + always: AlwaysInterface; + cb: HookCbInterface; + skip: SkipInterface; +} + +export interface AlwaysInterface { + (title: string, implementation: Implementation): void; + (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: Macro | Macro[], ...args: Array): void; + + cb: HookCbInterface; + skip: SkipInterface; +} + +export interface BeforeInterface { + (title: string, implementation: Implementation): void; + (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: Macro | Macro[], ...args: Array): void; + + cb: HookCbInterface; + skip: SkipInterface; +} + +export interface CbInterface { + (title: string, implementation: CbImplementation): void; + (title: string, macro: CbMacro | CbMacro[], ...args: Array): void; + (macro: CbMacro | CbMacro[], ...args: Array): void; + + failing: CbFailingInterface; + only: CbOnlyInterface; + skip: CbSkipInterface; +} + +export interface CbFailingInterface { + (title: string, implementation: CbImplementation): void; + (title: string, macro: CbMacro | CbMacro[], ...args: Array): void; + (macro: CbMacro | CbMacro[], ...args: Array): void; + + only: CbOnlyInterface; + skip: CbSkipInterface; +} + +export interface CbOnlyInterface { + (title: string, implementation: CbImplementation): void; + (title: string, macro: CbMacro | CbMacro[], ...args: Array): void; + (macro: CbMacro | CbMacro[], ...args: Array): void; +} + +export interface CbSkipInterface { + (title: string, implementation: CbImplementation): void; + (title: string, macro: CbMacro | CbMacro[], ...args: Array): void; + (macro: CbMacro | CbMacro[], ...args: Array): void; +} + +export interface FailingInterface { + (title: string, implementation: Implementation): void; + (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: Macro | Macro[], ...args: Array): void; + + only: OnlyInterface; + skip: SkipInterface; +} + +export interface HookCbInterface { + (title: string, implementation: CbImplementation): void; + (title: string, macro: CbMacro | CbMacro[], ...args: Array): void; + (macro: CbMacro | CbMacro[], ...args: Array): void; + + skip: CbSkipInterface; +} + +export interface OnlyInterface { + (title: string, implementation: Implementation): void; + (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: Macro | Macro[], ...args: Array): void; +} + +export interface SerialInterface { + (title: string, implementation: Implementation): void; + (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: Macro | Macro[], ...args: Array): void; + + cb: CbInterface; + failing: FailingInterface; + only: OnlyInterface; + skip: SkipInterface; + todo: TodoDeclaration; +} + +export interface SkipInterface { + (title: string, implementation: Implementation): void; + (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: Macro | Macro[], ...args: Array): void; +} + +export type TodoDeclaration = (title: string) => void; + +declare const test: TestInterface; +export default test; + +export {test}; +export const after: AfterInterface; +export const afterEach: AfterInterface; +export const before: BeforeInterface; +export const beforeEach: BeforeInterface; +export const cb: CbInterface; +export const failing: FailingInterface; +export const only: OnlyInterface; +export const serial: SerialInterface; +export const skip: SkipInterface; +export const todo: TodoDeclaration; diff --git a/index.js.flow b/index.js.flow index d867a75cc..118307b29 100644 --- a/index.js.flow +++ b/index.js.flow @@ -1,166 +1,201 @@ -/* @flow */ - -/** - * Misc Setup Types - */ - -type PromiseLike = { - then( - onFulfill?: (value: R) => Promise | U, - onReject?: (error: any) => Promise | U - ): Promise; +// @flow +export interface PromiseLike { + then(onFulfill?: (value: R) => Promise | U, onReject?: (error: any) => Promise | U): Promise; } -type ObservableLike = { - subscribe(observer: (value: {}) => void): void; -}; - -type SpecialReturnTypes = - | PromiseLike - | Iterator - | ObservableLike; - -type Constructor = Class<{ - constructor(...args: Array): any -}>; +export interface ObservableLike { + subscribe(observer: (value: any) => void): void; +} -type ErrorValidator = - | Constructor - | RegExp - | string - | ((error: any) => boolean); +export type ThrowsErrorValidator = Class<{constructor(...args: Array): any}> | RegExp | string | ((error: any) => boolean); -/** - * Assertion Types - */ +export interface SnapshotOptions { + id?: string; +} -type AssertContext = { - // Passing assertion. - pass(message?: string): void; - // Failing assertion. +export interface Assertions { + deepEqual(actual: any, expected: any, message?: string): void; fail(message?: string): void; - // Assert that value is truthy. - truthy(value: mixed, message?: string): void; - // Assert that value is falsy. - falsy(value: mixed, message?: string): void; - // Assert that value is true. - true(value: mixed, message?: string): void; - // Assert that value is false. - false(value: mixed, message?: string): void; - // Assert that value is equal to expected. - is(value: U, expected: U, message?: string): void; - // Assert that value is not equal to expected. - not(value: U, expected: U, message?: string): void; - // Assert that value is deep equal to expected. - deepEqual(value: U, expected: U, message?: string): void; - // Assert that value is not deep equal to expected. - notDeepEqual(value: U, expected: U, message?: string): void; - // Assert that the promise rejects, or the function throws or returns a rejected promise. - // @param error Can be a constructor, regex, error message or validation function. - throws: { - (value: PromiseLike, error?: ErrorValidator, message?: string): Promise; - (value: () => mixed, error?: ErrorValidator, message?: string): any; - }; - // Assert that the promise resolves, or the function doesn't throw or return a resolved promise. - notThrows: { - (value: PromiseLike, message?: string): Promise; - (value: () => mixed, message?: string): void; - }; - // Assert that contents matches regex. - regex(contents: string, regex: RegExp, message?: string): void; - // Assert that contents matches a snapshot. - snapshot: ((contents: any, message?: string) => void) & ((contents: any, options: {id: string}, message?: string) => void); - // Assert that contents does not match regex. - notRegex(contents: string, regex: RegExp, message?: string): void; - // Assert that error is falsy. + false(actual: any, message?: string): void; + falsy(actual: any, message?: string): void; ifError(error: any, message?: string): void; -}; - -/** - * Context Types - */ + is(actual: any, expected: any, message?: string): void; + not(actual: any, expected: any, message?: string): void; + notDeepEqual(actual: any, expected: any, message?: string): void; + notRegex(string: string, regex: RegExp, message?: string): void; + notThrows(value: () => ObservableLike, message?: string): Promise; + notThrows(value: () => PromiseLike, message?: string): Promise; + notThrows(value: () => any, message?: string): void; + notThrows(value: ObservableLike, message?: string): Promise; + notThrows(value: PromiseLike, message?: string): Promise; + pass(message?: string): void; + regex(string: string, regex: RegExp, message?: string): void; + snapshot(expected: any, message?: string): void; + snapshot(expected: any, options: SnapshotOptions, message?: string): void; + throws(value: () => ObservableLike, error?: ThrowsErrorValidator, message?: string): Promise; + throws(value: () => PromiseLike, error?: ThrowsErrorValidator, message?: string): Promise; + throws(value: () => any, error?: ThrowsErrorValidator, message?: string): any; + throws(value: ObservableLike, error?: ThrowsErrorValidator, message?: string): Promise; + throws(value: PromiseLike, error?: ThrowsErrorValidator, message?: string): Promise; + true(actual: any, message?: string): void; + truthy(actual: any, message?: string): void; +} -type TestContext = AssertContext & { +export interface ExecutionContext extends Assertions { + context: Context; + skip: Assertions; title: string; - plan(count: number): void; - skip: AssertContext; log(...values: Array): void; -}; -type ContextualTestContext = TestContext & { context: any; }; -type ContextualCallbackTestContext = TestContext & { context: any; end(): void; }; + plan(count: number): void; +} -/** - * Test Implementations - */ +export interface CbExecutionContext extends ExecutionContext { + end(): void; +} -type TestFunction = { - (t: T, ...args: Array): R; +export type ImplementationResult = PromiseLike | ObservableLike | Iterator | void; +export type Implementation = {(t: ExecutionContext): ImplementationResult}; +export type CbImplementation = {(t: CbExecutionContext): ImplementationResult}; + +export interface Macro { + (t: ExecutionContext, ...args: Array): ImplementationResult; + title?: (providedTitle: string, ...args: Array) => string; +} + +export interface CbMacro { + (t: CbExecutionContext, ...args: Array): ImplementationResult; title?: (providedTitle: string, ...args: Array) => string; -}; - -type TestImplementation = - | TestFunction - | Array>; - -type ContextualTest = TestImplementation; -type ContextualCallbackTest = TestImplementation; - - -/** - * Method Types - */ - -type ContextualTestMethod = { - ( implementation: ContextualTest): void; - (title: string, implementation: ContextualTest): void; - - serial : ContextualTestMethod; - before : ContextualTestMethod; - after : ContextualTestMethod; - skip : ContextualTestMethod; - todo : ContextualTestMethod; - failing : ContextualTestMethod; - only : ContextualTestMethod; - beforeEach : ContextualTestMethod; - afterEach : ContextualTestMethod; - cb : ContextualCallbackTestMethod; - always : ContextualTestMethod; -}; - -type ContextualCallbackTestMethod = { - ( implementation: ContextualCallbackTest): void; - (title: string, implementation: ContextualCallbackTest): void; - - serial : ContextualCallbackTestMethod; - before : ContextualCallbackTestMethod; - after : ContextualCallbackTestMethod; - skip : ContextualCallbackTestMethod; - todo : ContextualCallbackTestMethod; - failing : ContextualCallbackTestMethod; - only : ContextualCallbackTestMethod; - beforeEach : ContextualCallbackTestMethod; - afterEach : ContextualCallbackTestMethod; - cb : ContextualCallbackTestMethod; - always : ContextualCallbackTestMethod; -}; - -/** - * Public API - */ - -declare module.exports: { - ( run: ContextualTest, ...args: any): void; - (title: string, run: ContextualTest, ...args: any): void; - - beforeEach : ContextualTestMethod; - afterEach : ContextualTestMethod; - serial : ContextualTestMethod; - before : ContextualTestMethod; - after : ContextualTestMethod; - skip : ContextualTestMethod; - todo : ContextualTestMethod; - failing : ContextualTestMethod; - only : ContextualTestMethod; - cb : ContextualCallbackTestMethod; - always : ContextualTestMethod; -}; +} + +export interface TestInterface { + (title: string, implementation: Implementation | Macro): void; + (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: Macro | Macro[], ...args: Array): void; + + after: AfterInterface; + afterEach: AfterInterface; + before: BeforeInterface; + beforeEach: BeforeInterface; + cb: CbInterface; + failing: FailingInterface; + only: OnlyInterface; + serial: SerialInterface; + skip: SkipInterface; + todo: TodoDeclaration; +} + +export interface AfterInterface { + (title: string, implementation: Implementation | Macro): void; + (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: Macro | Macro[], ...args: Array): void; + + always: AlwaysInterface; + cb: HookCbInterface; + skip: SkipInterface; +} + +export interface AlwaysInterface { + (title: string, implementation: Implementation | Macro): void; + (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: Macro | Macro[], ...args: Array): void; + + cb: HookCbInterface; + skip: SkipInterface; +} + +export interface BeforeInterface { + (title: string, implementation: Implementation | Macro): void; + (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: Macro | Macro[], ...args: Array): void; + + cb: HookCbInterface; + skip: SkipInterface; +} + +export interface CbInterface { + (title: string, implementation: CbImplementation | CbMacro): void; + (title: string, macro: CbMacro | CbMacro[], ...args: Array): void; + (macro: CbMacro | CbMacro[], ...args: Array): void; + + failing: CbFailingInterface; + only: CbOnlyInterface; + skip: CbSkipInterface; +} + +export interface CbFailingInterface { + (title: string, implementation: CbImplementation | CbMacro): void; + (title: string, macro: CbMacro | CbMacro[], ...args: Array): void; + (macro: CbMacro | CbMacro[], ...args: Array): void; + + only: CbOnlyInterface; + skip: CbSkipInterface; +} + +export interface CbOnlyInterface { + (title: string, implementation: CbImplementation | CbMacro): void; + (title: string, macro: CbMacro | CbMacro[], ...args: Array): void; + (macro: CbMacro | CbMacro[], ...args: Array): void; +} + +export interface CbSkipInterface { + (title: string, implementation: CbImplementation | CbMacro): void; + (title: string, macro: CbMacro | CbMacro[], ...args: Array): void; + (macro: CbMacro | CbMacro[], ...args: Array): void; +} + +export interface FailingInterface { + (title: string, implementation: Implementation | Macro): void; + (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: Macro | Macro[], ...args: Array): void; + + only: OnlyInterface; + skip: SkipInterface; +} + +export interface HookCbInterface { + (title: string, implementation: CbImplementation | CbMacro): void; + (title: string, macro: CbMacro | CbMacro[], ...args: Array): void; + (macro: CbMacro | CbMacro[], ...args: Array): void; + + skip: CbSkipInterface; +} + +export interface OnlyInterface { + (title: string, implementation: Implementation | Macro): void; + (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: Macro | Macro[], ...args: Array): void; +} + +export interface SerialInterface { + (title: string, implementation: Implementation | Macro): void; + (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: Macro | Macro[], ...args: Array): void; + + cb: CbInterface; + failing: FailingInterface; + only: OnlyInterface; + skip: SkipInterface; + todo: TodoDeclaration; +} + +export interface SkipInterface { + (title: string, implementation: Implementation | Macro): void; + (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: Macro | Macro[], ...args: Array): void; +} + +export type TodoDeclaration = {(title: string): void}; + +declare export default TestInterface<>; + +declare export var test: TestInterface<>; +declare export var after: AfterInterface; +declare export var afterEach: AfterInterface<>; +declare export var before: BeforeInterface; +declare export var beforeEach: BeforeInterface<>; +declare export var cb: CbInterface<>; +declare export var failing: FailingInterface<>; +declare export var only: OnlyInterface<>; +declare export var serial: SerialInterface<>; +declare export var skip: SkipInterface<>; +declare export var todo: TodoDeclaration; diff --git a/lib/main.js b/lib/main.js index ad363faf0..3ab8d5a97 100644 --- a/lib/main.js +++ b/lib/main.js @@ -95,9 +95,21 @@ globals.setImmediate(() => { }); }); -module.exports = runner.chain; - -// TypeScript imports the `default` property for -// an ES2015 default import (`import test from 'ava'`) -// See: https://github.com/Microsoft/TypeScript/issues/2242#issuecomment-83694181 -module.exports.default = runner.chain; +const makeCjsExport = () => { + function test() { + return runner.chain.apply(null, arguments); + } + return Object.assign(test, runner.chain); +}; + +// Support CommonJS modules by exporting a test function that can be fully +// chained. Also support ES module loaders by exporting __esModule and a +// default. Support `import * as ava from 'ava'` use cases by exporting a +// `test` member. Do all this whilst preventing `test.test.test() or +// `test.default.test()` chains, though in CommonJS `test.test()` is +// unavoidable. +module.exports = Object.assign(makeCjsExport(), { + __esModule: true, + default: runner.chain, + test: runner.chain +}); diff --git a/lib/runner.js b/lib/runner.js index eb02dde45..d425a0399 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -2,38 +2,112 @@ const EventEmitter = require('events'); const path = require('path'); const Bluebird = require('bluebird'); -const optionChain = require('option-chain'); const matcher = require('matcher'); const snapshotManager = require('./snapshot-manager'); const TestCollection = require('./test-collection'); const validateTest = require('./validate-test'); -const chainableMethods = { - defaults: { - type: 'test', - serial: false, - exclusive: false, - skipped: false, - todo: false, - failing: false, - callback: false, - always: false - }, - chainableMethods: { - test: {}, - serial: {serial: true}, - before: {type: 'before'}, - after: {type: 'after'}, - skip: {skipped: true}, - todo: {todo: true}, - failing: {failing: true}, - only: {exclusive: true}, - beforeEach: {type: 'beforeEach'}, - afterEach: {type: 'afterEach'}, - cb: {callback: true}, - always: {always: true} +const chainRegistry = new WeakMap(); + +function startChain(name, call, defaults) { + const fn = function () { + call(Object.assign({}, defaults), Array.from(arguments)); + }; + Object.defineProperty(fn, 'name', {value: name}); + chainRegistry.set(fn, {call, defaults, fullName: name}); + return fn; +} + +function extendChain(prev, name, flag) { + if (!flag) { + flag = name; } -}; + + const fn = function () { + callWithFlag(prev, flag, Array.from(arguments)); + }; + const fullName = `${chainRegistry.get(prev).fullName}.${name}`; + Object.defineProperty(fn, 'name', {value: fullName}); + prev[name] = fn; + + chainRegistry.set(fn, {flag, fullName, prev}); + return fn; +} + +function callWithFlag(prev, flag, args) { + const combinedFlags = {[flag]: true}; + do { + const step = chainRegistry.get(prev); + if (step.call) { + step.call(Object.assign({}, step.defaults, combinedFlags), args); + prev = null; + } else { + combinedFlags[step.flag] = true; + prev = step.prev; + } + } while (prev); +} + +function createHookChain(hook, isAfterHook) { + // Hook chaining rules: + // * `always` comes immediately after "after hooks" + // * `skip` must come at the end + // * no `only` + // * no repeating + extendChain(hook, 'cb', 'callback'); + extendChain(hook, 'skip', 'skipped'); + extendChain(hook.cb, 'skip', 'skipped'); + if (isAfterHook) { + extendChain(hook, 'always'); + extendChain(hook.always, 'cb', 'callback'); + extendChain(hook.always, 'skip', 'skipped'); + extendChain(hook.always.cb, 'skip', 'skipped'); + } + return hook; +} + +function createChain(fn, defaults) { + // Test chaining rules: + // * `serial` must come at the start + // * `only` and `skip` must come at the end + // * `failing` must come at the end, but can be followed by `only` and `skip` + // * `only` and `skip` cannot be chained together + // * no repeating + const root = startChain('test', fn, Object.assign({}, defaults, {type: 'test'})); + extendChain(root, 'cb', 'callback'); + extendChain(root, 'failing'); + extendChain(root, 'only', 'exclusive'); + extendChain(root, 'serial'); + extendChain(root, 'skip', 'skipped'); + extendChain(root.cb, 'failing'); + extendChain(root.cb, 'only', 'exclusive'); + extendChain(root.cb, 'skip', 'skipped'); + extendChain(root.cb.failing, 'only', 'exclusive'); + extendChain(root.cb.failing, 'skip', 'skipped'); + extendChain(root.failing, 'only', 'exclusive'); + extendChain(root.failing, 'skip', 'skipped'); + extendChain(root.serial, 'cb', 'callback'); + extendChain(root.serial, 'failing'); + extendChain(root.serial, 'only', 'exclusive'); + extendChain(root.serial, 'skip', 'skipped'); + extendChain(root.serial.cb, 'failing'); + extendChain(root.serial.cb, 'only', 'exclusive'); + extendChain(root.serial.cb, 'skip', 'skipped'); + extendChain(root.serial.cb.failing, 'only', 'exclusive'); + extendChain(root.serial.cb.failing, 'skip', 'skipped'); + + root.after = createHookChain(startChain('test.after', fn, Object.assign({}, defaults, {type: 'after'})), true); + root.afterEach = createHookChain(startChain('test.afterEach', fn, Object.assign({}, defaults, {type: 'afterEach'})), true); + root.before = createHookChain(startChain('test.before', fn, Object.assign({}, defaults, {type: 'before'})), false); + root.beforeEach = createHookChain(startChain('test.beforeEach', fn, Object.assign({}, defaults, {type: 'beforeEach'})), false); + + // Todo tests cannot be chained. Allow todo tests to be flagged as needing to + // be serial. + root.todo = startChain('test.todo', fn, Object.assign({}, defaults, {type: 'test', todo: true})); + root.serial.todo = startChain('test.serial.todo', fn, Object.assign({}, defaults, {serial: true, type: 'test', todo: true})); + + return root; +} function wrapFunction(fn, args) { return function (t) { @@ -63,7 +137,7 @@ class Runner extends EventEmitter { compareTestSnapshot: this.compareTestSnapshot.bind(this) }); - this.chain = optionChain(chainableMethods, (opts, args) => { + this.chain = createChain((opts, args) => { let title; let fn; let macroArgIndex; @@ -100,6 +174,14 @@ class Runner extends EventEmitter { } else { this.addTest(title, opts, fn, args); } + }, { + serial: false, + exclusive: false, + skipped: false, + todo: false, + failing: false, + callback: false, + always: false }); } diff --git a/lib/test-collection.js b/lib/test-collection.js index a0bb9e76f..46183d252 100644 --- a/lib/test-collection.js +++ b/lib/test-collection.js @@ -38,10 +38,6 @@ class TestCollection extends EventEmitter { const metadata = test.metadata; const type = metadata.type; - if (!type) { - throw new Error('Test type must be specified'); - } - if (test.title === '' || typeof test.title !== 'string') { if (type === 'test') { throw new TypeError('Tests must have a title'); @@ -58,16 +54,8 @@ class TestCollection extends EventEmitter { } } - if (metadata.always && type !== 'after' && type !== 'afterEach') { - throw new Error('"always" can only be used with after and afterEach hooks'); - } - // Add a hook if (type !== 'test') { - if (metadata.exclusive) { - throw new Error(`"only" cannot be used with a ${type} hook`); - } - this.hooks[type + (metadata.always ? 'Always' : '')].push(test); return; } diff --git a/package-lock.json b/package-lock.json index eb05509b2..18852f01c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4174,12 +4174,6 @@ } } }, - "is-array-sorted": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-array-sorted/-/is-array-sorted-1.0.0.tgz", - "integrity": "sha1-fyQGt+kYStas6D0RpvzLHCyDeHM=", - "dev": true - }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -6143,11 +6137,6 @@ } } }, - "option-chain": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/option-chain/-/option-chain-1.0.0.tgz", - "integrity": "sha1-k41zvU4Xg/lI00AjZEraI2aeMPI=" - }, "optionator": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", @@ -8241,9 +8230,9 @@ "dev": true }, "typescript": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz", - "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.1.tgz", + "integrity": "sha512-bqB1yS6o9TNA9ZC/MJxM0FZzPnZdtHj0xWK/IZ5khzVqdpGul/R/EIiHRgFXlwTD7PSIaYVnGKq1QgMCu2mnqw==", "dev": true }, "ua-parser-js": { diff --git a/package.json b/package.json index 953c27083..7f5d1d3d8 100644 --- a/package.json +++ b/package.json @@ -13,15 +13,13 @@ "lint": "xo && (cd test/fixture && xo '**' '!{source-map-initial,syntax-error}.js' '!snapshots/test-sourcemaps/build/**') && lock-verify", "test": "npm run lint && flow check test/flow-types && tsc -p test/ts-types && nyc tap --no-cov --timeout=300 --jobs=4 test/*.js test/reporters/*.js", "test-win": "tap --no-cov --reporter=classic --timeout=300 --jobs=4 test/*.js test/reporters/*.js", - "visual": "node test/visual/run-visual-tests.js", - "prepare": "npm run make-ts", - "make-ts": "node types/make.js" + "visual": "node test/visual/run-visual-tests.js" }, "files": [ "lib", "*.js", "*.js.flow", - "types/generated.d.ts" + "index.d.ts" ], "keywords": [ "🦄", @@ -124,7 +122,6 @@ "ms": "^2.1.1", "multimatch": "^2.1.0", "observable-to-promise": "^0.5.0", - "option-chain": "^1.0.0", "package-hash": "^2.0.0", "pkg-conf": "^2.1.0", "plur": "^2.0.0", @@ -155,7 +152,6 @@ "git-branch": "^1.0.0", "has-ansi": "^3.0.0", "inquirer": "^5.0.1", - "is-array-sorted": "^1.0.0", "lock-verify": "^1.1.0", "lolex": "^2.3.1", "nyc": "^11.4.1", @@ -168,11 +164,11 @@ "tap": "^11.0.1", "temp-write": "^3.4.0", "touch": "^3.1.0", - "typescript": "^2.6.2", + "typescript": "^2.7.1", "xo": "^0.18.2", "zen-observable": "^0.7.1" }, - "typings": "types/generated.d.ts", + "typings": "index.d.ts", "xo": { "ignores": [ "media/**" diff --git a/readme.md b/readme.md index b69f84a24..0c6fbadc8 100644 --- a/readme.md +++ b/readme.md @@ -385,6 +385,8 @@ test.serial('passes serially', t => { Note that this only applies to tests within a particular test file. AVA will still run multiple tests files at the same time unless you pass the [`--serial` CLI flag](#cli). +You can use the `.serial` modifier with all tests, hooks and even `.todo()`, but it's only available on the `test` function. + ### Running specific tests During development it can be helpful to only run a few specific tests. This can be accomplished using the `.only` modifier: @@ -399,6 +401,8 @@ test.only('will be run', t => { }); ``` +You can use the `.only` modifier with all tests. It cannot be used with hooks or `.todo()`. + *Note:* The `.only` modifier applies to the test file it's defined in, so if you run multiple test files, tests in other files will still run. If you want to only run the `test.only` test, provide just that test file to AVA. ### Running tests with matching titles @@ -485,7 +489,7 @@ test.skip('will not be run', t => { }); ``` -You must specify the implementation function. +You must specify the implementation function. You can use the `.skip` modifier with all tests and hooks, but not with `.todo()`. You can not apply further modifiers to `.skip`. ### Test placeholders ("todo") @@ -495,6 +499,12 @@ You can use the `.todo` modifier when you're planning to write a test. Like skip test.todo('will think about writing this later'); ``` +You can signal that you need to write a serial test: + +```js +test.serial.todo('will think about writing this later'); +``` + ### Failing tests You can use the `.failing` modifier to document issues with your code that need to be fixed. Failing tests are run just like normal ones, but they are expected to fail, and will not break your build when they do. If a test marked as failing actually passes, it will be reported as an error and fail the build with a helpful message instructing you to remove the `.failing` modifier. @@ -558,7 +568,7 @@ test('title', t => { }); ``` -Hooks can be synchronous or asynchronous, just like tests. To make a hook asynchronous return a promise or observable, use an async function, or enable callback mode via `test.cb.before()`, `test.cb.beforeEach()` etc. +Hooks can be synchronous or asynchronous, just like tests. To make a hook asynchronous return a promise or observable, use an async function, or enable callback mode via `test.before.cb()`, `test.beforeEach.cb()` etc. ```js test.before(async t => { @@ -569,7 +579,7 @@ test.after(t => { return new Promise(/* ... */); }); -test.cb.beforeEach(t => { +test.beforeEach.cb(t => { setTimeout(t.end); }); @@ -610,19 +620,6 @@ test('context is unicorn', t => { Context sharing is *not* available to `before` and `after` hooks. -### Chaining test modifiers - -You can use the `.serial`, `.only` and `.skip` modifiers in any order, with `test`, `before`, `after`, `beforeEach` and `afterEach`. For example: - -```js -test.before.skip(...); -test.skip.after(...); -test.serial.only(...); -test.only.serial(...); -``` - -This means you can temporarily add `.skip` or `.only` at the end of a test or hook definition without having to make any other changes. - ### Test macros Additional arguments passed to the test declaration will be passed to the test implementation. This is useful for creating reusable test macros. @@ -1135,6 +1132,7 @@ It's the [Andromeda galaxy](https://simple.wikipedia.org/wiki/Andromeda_galaxy). - [When to use `t.plan()`](docs/recipes/when-to-use-plan.md) - [Browser testing](docs/recipes/browser-testing.md) - [TypeScript](docs/recipes/typescript.md) +- [Flow](docs/recipes/flow.md) - [Configuring Babel][Babel recipe] - [Testing React components](docs/recipes/react.md) - [Testing Vue.js components](docs/recipes/vue.md) diff --git a/test/flow-types/log.js.flow b/test/flow-types/log.js similarity index 62% rename from test/flow-types/log.js.flow rename to test/flow-types/log.js index 0c0c94323..83d6a0645 100644 --- a/test/flow-types/log.js.flow +++ b/test/flow-types/log.js @@ -1,8 +1,7 @@ -/* @flow */ - -const test = require('../../index.js.flow'); +// @flow +import test from '../../index.js.flow'; test('log', t => { t.pass(); t.log({object: true}, 42, ['array'], false, new Date(), new Map()); -}) +}); diff --git a/test/flow-types/regression-1114.js.flow b/test/flow-types/regression-1114.js similarity index 72% rename from test/flow-types/regression-1114.js.flow rename to test/flow-types/regression-1114.js index dfe7756ab..7459cae90 100644 --- a/test/flow-types/regression-1114.js.flow +++ b/test/flow-types/regression-1114.js @@ -1,12 +1,10 @@ -/* @flow */ - -const test = require('../../index.js.flow'); +// @flow +import test from '../../index.js.flow'; test('Named test', t => { t.pass('Success'); // $ExpectError: Unknown method "unknownAssertion" t.unknownAssertion('Whoops'); - const context = t.context; // $ExpectError: Unknown method "end" t.end(); }); @@ -15,7 +13,6 @@ test('test', t => { t.pass('Success'); // $ExpectError: Unknown method "unknownAssertion" t.unknownAssertion('Whoops'); - const context = t.context; // $ExpectError: Unknown method "end" t.end(); }); @@ -25,17 +22,13 @@ test.cb('test', t => { t.end(); }); -test.beforeEach(t => { - const context = t.context; -}) - function macro(t, input, expected) { - t.is(eval(input), expected); + t.is(eval(input), expected); // eslint-disable-line no-eval } -macro.title = (title, input, expected) => title || input; +macro.title = (title, input) => title || input; function macro2(t, input, expected) { - t.is(eval(input), expected); + t.is(eval(input), expected); // eslint-disable-line no-eval } test('2 + 2 === 4', macro, '2 + 2', 4); @@ -45,7 +38,7 @@ test('2 + 2 === 4', [macro, macro2], '2 + 2', 4); test([macro, macro2], '2 * 3', 6); function macroBadTitle(t, input, expected) { - t.is(eval(input), expected); + t.is(eval(input), expected); // eslint-disable-line no-eval } macroBadTitle.title = 'Not a function'; // $ExpectError: Macro "title" is not a function diff --git a/test/flow-types/regression-1148.js.flow b/test/flow-types/regression-1148.js.flow index 45cf45f8d..915bda765 100644 --- a/test/flow-types/regression-1148.js.flow +++ b/test/flow-types/regression-1148.js.flow @@ -1,19 +1,18 @@ -/* @flow */ - -const test = require('../../index.js.flow'); +// @flow +import test from '../../index.js.flow'; test('test', t => { - t.throws(() => { throw new Error(); }); + t.throws((): void => { throw new Error(); }); t.throws(Promise.reject(new Error())); - t.notThrows(() => { return; }); + t.notThrows((): void => { return; }); t.notThrows(Promise.resolve('Success')); - const error = t.throws(() => { throw new Error(); }); + const error = t.throws((): void => { throw new Error(); }); const message: string = error.message; const promise = t.throws(Promise.reject(new Error())); promise.then(error => { const message: string = error.message; - }) + }); }); diff --git a/test/flow-types/regression-1500.js b/test/flow-types/regression-1500.js new file mode 100644 index 000000000..1d0a92019 --- /dev/null +++ b/test/flow-types/regression-1500.js @@ -0,0 +1,16 @@ +// @flow +import test from '../../index.js.flow'; + +test('test', t => { + t.snapshot({}); + t.snapshot({}, 'a message'); + t.snapshot({}, {id: 'snapshot-id'}); + t.snapshot({}, {id: 'snapshot-id'}, 'a message'); + + // $ExpectError Message should be a string + t.snapshot({}, 1); + // $ExpectError unknownOption is an unknown options attribute + t.snapshot({}, {unknownOption: true}); + // $ExpectError Message should be a string + t.snapshot({}, {id: 'snapshot-id'}, 1); +}); diff --git a/test/flow-types/regression-1500.js.flow b/test/flow-types/regression-1500.js.flow deleted file mode 100644 index ca4f83d98..000000000 --- a/test/flow-types/regression-1500.js.flow +++ /dev/null @@ -1,17 +0,0 @@ -/* @flow */ - -const test = require('../../index.js.flow'); - -test('test', t => { - t.snapshot({}); - t.snapshot({}, "a message"); - t.snapshot({}, {id: "snapshot-id"}); - t.snapshot({}, {id: "snapshot-id"}, "a message"); - - // $ExpectError Message should be a string - t.snapshot({}, 1); - // $ExpectError unknownOption is an unknown options attribute - t.snapshot({}, { unknownOption: true }); - // $ExpectError Message should be a string - t.snapshot({}, { id: "snapshot-id" }, 1); -}); diff --git a/test/hooks.js b/test/hooks.js index a0795c985..28c176732 100644 --- a/test/hooks.js +++ b/test/hooks.js @@ -38,7 +38,7 @@ test('before', t => { arr.push('a'); }); - runner.chain.test('test', a => { + runner.chain('test', a => { a.pass(); arr.push('b'); }); @@ -58,7 +58,7 @@ test('after', t => { arr.push('b'); }); - runner.chain.test('test', a => { + runner.chain('test', a => { a.pass(); arr.push('a'); }); @@ -82,7 +82,7 @@ test('after not run if test failed', t => { arr.push('a'); }); - runner.chain.test('test', () => { + runner.chain('test', () => { throw new Error('something went wrong'); }); return runner.run({}).then(() => { @@ -104,7 +104,7 @@ test('after.always run even if test failed', t => { arr.push('a'); }); - runner.chain.test('test', () => { + runner.chain('test', () => { throw new Error('something went wrong'); }); return runner.run({}).then(() => { @@ -150,7 +150,7 @@ test('stop if before hooks failed', t => { throw new Error('something went wrong'); }); - runner.chain.test('test', a => { + runner.chain('test', a => { a.pass(); arr.push('b'); a.end(); @@ -178,12 +178,12 @@ test('before each with concurrent tests', t => { arr[k++].push('b'); }); - runner.chain.test('c', a => { + runner.chain('c', a => { a.pass(); arr[0].push('c'); }); - runner.chain.test('d', a => { + runner.chain('d', a => { a.pass(); arr[1].push('d'); }); @@ -235,7 +235,7 @@ test('fail if beforeEach hook fails', t => { a.fail(); }); - runner.chain.test('test', a => { + runner.chain('test', a => { arr.push('b'); a.pass(); }); @@ -264,12 +264,12 @@ test('after each with concurrent tests', t => { arr[k++].push('b'); }); - runner.chain.test('c', a => { + runner.chain('c', a => { a.pass(); arr[0].push('c'); }); - runner.chain.test('d', a => { + runner.chain('d', a => { a.pass(); arr[1].push('d'); }); @@ -320,7 +320,7 @@ test('afterEach not run if concurrent tests failed', t => { arr.push('a'); }); - runner.chain.test('test', () => { + runner.chain('test', () => { throw new Error('something went wrong'); }); @@ -360,7 +360,7 @@ test('afterEach.always run even if concurrent tests failed', t => { arr.push('a'); }); - runner.chain.test('test', () => { + runner.chain('test', () => { throw new Error('something went wrong'); }); @@ -400,7 +400,7 @@ test('afterEach.always run even if beforeEach failed', t => { throw new Error('something went wrong'); }); - runner.chain.test('test', a => { + runner.chain('test', a => { a.pass(); arr.push('a'); }); @@ -437,7 +437,7 @@ test('ensure hooks run only around tests', t => { arr.push('after'); }); - runner.chain.test('test', a => { + runner.chain('test', a => { a.pass(); arr.push('test'); }); @@ -465,7 +465,7 @@ test('shared context', t => { a.context.arr = ['a']; }); - runner.chain.test('test', a => { + runner.chain('test', a => { a.pass(); a.context.arr.push('b'); a.deepEqual(a.context.arr, ['a', 'b']); @@ -492,7 +492,7 @@ test('shared context of any type', t => { a.context = 'foo'; }); - runner.chain.test('test', a => { + runner.chain('test', a => { a.pass(); a.is(a.context, 'foo'); }); diff --git a/test/runner.js b/test/runner.js index be267c579..f8d85acb9 100644 --- a/test/runner.js +++ b/test/runner.js @@ -10,9 +10,9 @@ test('nested tests and hooks aren\'t allowed', t => { const runner = new Runner(); - runner.chain.test('test', a => { + runner.chain('test', a => { t.throws(() => { - runner.chain.test(noop); + runner.chain(noop); }, {message: 'All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.'}); a.pass(); }); @@ -27,7 +27,7 @@ test('tests must be declared synchronously', t => { const runner = new Runner(); - runner.chain.test('test', a => { + runner.chain('test', a => { a.pass(); return Promise.resolve(); }); @@ -35,7 +35,7 @@ test('tests must be declared synchronously', t => { runner.run({}); t.throws(() => { - runner.chain.test(noop); + runner.chain(noop); }, {message: 'All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.'}); t.end(); @@ -44,7 +44,7 @@ test('tests must be declared synchronously', t => { test('runner emits a "test" event', t => { const runner = new Runner(); - runner.chain.test('foo', a => { + runner.chain('foo', a => { a.pass(); }); @@ -62,7 +62,7 @@ test('run serial tests before concurrent ones', t => { const runner = new Runner(); const arr = []; - runner.chain.test('test', a => { + runner.chain('test', a => { arr.push('c'); a.end(); }); @@ -106,8 +106,8 @@ test('anything can be skipped', t => { runner.chain.beforeEach(pusher('beforeEach')); runner.chain.beforeEach.skip(pusher('beforeEach.skip')); - runner.chain.test('concurrent', pusher('concurrent')); - runner.chain.test.skip('concurrent.skip', pusher('concurrent.skip')); + runner.chain('concurrent', pusher('concurrent')); + runner.chain.skip('concurrent.skip', pusher('concurrent.skip')); runner.chain.serial('serial', pusher('serial')); runner.chain.serial.skip('serial.skip', pusher('serial.skip')); @@ -137,8 +137,8 @@ test('include skipped tests in results', t => { runner.chain.beforeEach('beforeEach', noop); runner.chain.beforeEach.skip('beforeEach.skip', noop); - runner.chain.test.serial('test', a => a.pass()); - runner.chain.test.serial.skip('test.skip', noop); + runner.chain.serial('test', a => a.pass()); + runner.chain.serial.skip('test.skip', noop); runner.chain.after('after', noop); runner.chain.after.skip('after.skip', noop); @@ -186,7 +186,7 @@ test('test types and titles', t => { runner.chain.beforeEach(fn); runner.chain.after(fn); runner.chain.afterEach(named); - runner.chain.test('test', fn); + runner.chain('test', fn); const tests = [ { @@ -226,7 +226,7 @@ test('skip test', t => { const runner = new Runner(); const arr = []; - runner.chain.test('test', a => { + runner.chain('test', a => { arr.push('a'); a.pass(); }); @@ -255,7 +255,7 @@ test('test throws when given no function', t => { const runner = new Runner(); t.throws(() => { - runner.chain.test(); + runner.chain(); }, new TypeError('Expected an implementation. Use `test.todo()` for tests without an implementation.')); }); @@ -265,7 +265,7 @@ test('todo test', t => { const runner = new Runner(); const arr = []; - runner.chain.test('test', a => { + runner.chain('test', a => { arr.push('a'); a.pass(); }); @@ -296,7 +296,7 @@ test('only test', t => { const runner = new Runner(); const arr = []; - runner.chain.test('test', a => { + runner.chain('test', a => { arr.push('a'); a.pass(); }); @@ -315,41 +315,11 @@ test('only test', t => { }); }); -test('throws if you try to set a hook as exclusive', t => { - const runner = new Runner(); - - t.throws(() => { - runner.chain.beforeEach.only('', noop); - }, new TypeError('`only` is only for tests and cannot be used with hooks')); - - t.end(); -}); - -test('throws if you try to set a before hook as always', t => { - const runner = new Runner(); - - t.throws(() => { - runner.chain.before.always('', noop); - }, new TypeError('`always` can only be used with `after` and `afterEach`')); - - t.end(); -}); - -test('throws if you try to set a test as always', t => { - const runner = new Runner(); - - t.throws(() => { - runner.chain.test.always('', noop); - }, new TypeError('`always` can only be used with `after` and `afterEach`')); - - t.end(); -}); - test('throws if you give a function to todo', t => { const runner = new Runner(); t.throws(() => { - runner.chain.test.todo('todo with function', noop); + runner.chain.todo('todo with function', noop); }, new TypeError('`todo` tests are not allowed to have an implementation. Use ' + '`test.skip()` for tests with an implementation.')); @@ -360,122 +330,18 @@ test('throws if todo has no title', t => { const runner = new Runner(); t.throws(() => { - runner.chain.test.todo(); + runner.chain.todo(); }, new TypeError('`todo` tests require a title')); t.end(); }); -test('throws if todo has failing, skip, or only', t => { - const runner = new Runner(); - - const errorMessage = '`todo` tests are just for documentation and cannot be' + - ' used with `skip`, `only`, or `failing`'; - - t.throws(() => { - runner.chain.test.failing.todo('test'); - }, new TypeError(errorMessage)); - - t.throws(() => { - runner.chain.test.skip.todo('test'); - }, new TypeError(errorMessage)); - - t.throws(() => { - runner.chain.test.only.todo('test'); - }, new TypeError(errorMessage)); - - t.end(); -}); - -test('throws if todo isn\'t a test', t => { - const runner = new Runner(); - - const errorMessage = '`todo` is only for documentation of future tests and' + - ' cannot be used with hooks'; - - t.throws(() => { - runner.chain.before.todo('test'); - }, new TypeError(errorMessage)); - - t.throws(() => { - runner.chain.beforeEach.todo('test'); - }, new TypeError(errorMessage)); - - t.throws(() => { - runner.chain.after.todo('test'); - }, new TypeError(errorMessage)); - - t.throws(() => { - runner.chain.afterEach.todo('test'); - }, new TypeError(errorMessage)); - - t.end(); -}); - -test('throws if test has skip and only', t => { - const runner = new Runner(); - - t.throws(() => { - runner.chain.test.only.skip('test', noop); - }, new TypeError('`only` tests cannot be skipped')); - - t.end(); -}); - -test('throws if failing is used on non-tests', t => { - const runner = new Runner(); - - const errorMessage = '`failing` is only for tests and cannot be used with hooks'; - - t.throws(() => { - runner.chain.beforeEach.failing('', noop); - }, new TypeError(errorMessage)); - - t.throws(() => { - runner.chain.before.failing('', noop); - }, new TypeError(errorMessage)); - - t.throws(() => { - runner.chain.afterEach.failing('', noop); - }, new TypeError(errorMessage)); - - t.throws(() => { - runner.chain.after.failing('', noop); - }, new TypeError(errorMessage)); - - t.end(); -}); - -test('throws if only is used on non-tests', t => { - const runner = new Runner(); - - const errorMessage = '`only` is only for tests and cannot be used with hooks'; - - t.throws(() => { - runner.chain.beforeEach.only(noop); - }, new TypeError(errorMessage)); - - t.throws(() => { - runner.chain.before.only(noop); - }, new TypeError(errorMessage)); - - t.throws(() => { - runner.chain.afterEach.only(noop); - }, new TypeError(errorMessage)); - - t.throws(() => { - runner.chain.after.only(noop); - }, new TypeError(errorMessage)); - - t.end(); -}); - test('validate accepts skipping failing tests', t => { t.plan(2); const runner = new Runner(); - runner.chain.test.skip.failing('skip failing', noop); + runner.chain.failing.skip('skip failing', noop); runner.run({}).then(() => { const stats = runner.buildStats(); @@ -492,7 +358,7 @@ test('runOnlyExclusive option test', t => { const options = {runOnlyExclusive: true}; const arr = []; - runner.chain.test('test', () => { + runner.chain('test', () => { arr.push('a'); }); @@ -524,7 +390,7 @@ test('options.serial forces all tests to be serial', t => { a.pass(); }); - runner.chain.test('test', a => { + runner.chain('test', a => { a.pass(); t.strictDeepEqual(arr, [1, 2]); t.end(); @@ -538,12 +404,12 @@ test('options.bail will bail out', t => { const runner = new Runner({bail: true}); - runner.chain.test('test', a => { + runner.chain('test', a => { t.pass(); a.fail(); }); - runner.chain.test('test 2', () => { + runner.chain('test 2', () => { t.fail(); }); @@ -626,22 +492,22 @@ test('options.match will not run tests with non-matching titles', t => { match: ['*oo', '!foo'] }); - runner.chain.test('mhm. grass tasty. moo', a => { + runner.chain('mhm. grass tasty. moo', a => { t.pass(); a.pass(); }); - runner.chain.test('juggaloo', a => { + runner.chain('juggaloo', a => { t.pass(); a.pass(); }); - runner.chain.test('foo', a => { + runner.chain('foo', a => { t.fail(); a.pass(); }); - runner.chain.test('test', a => { + runner.chain('test', a => { t.fail(); a.pass(); }); @@ -668,7 +534,7 @@ test('options.match hold no effect on hooks with titles', t => { actual = 'foo'; }); - runner.chain.test('after', a => { + runner.chain('after', a => { t.is(actual, 'foo'); a.pass(); }); @@ -689,12 +555,12 @@ test('options.match overrides .only', t => { match: ['*oo'] }); - runner.chain.test('moo', a => { + runner.chain('moo', a => { t.pass(); a.pass(); }); - runner.chain.test.only('boo', a => { + runner.chain.only('boo', a => { t.pass(); a.pass(); }); @@ -713,7 +579,7 @@ test('macros: Additional args will be spread as additional args on implementatio const runner = new Runner(); - runner.chain.test('test1', function (a) { + runner.chain('test1', function (a) { t.deepEqual(slice.call(arguments, 1), ['foo', 'bar']); a.pass(); }, 'foo', 'bar'); @@ -754,9 +620,9 @@ test('macros: Customize test names attaching a `title` function', t => { t.is(props.title, expectedTitles.shift()); }); - runner.chain.test(macroFn, 'A'); - runner.chain.test('supplied', macroFn, 'B'); - runner.chain.test(macroFn, 'C'); + runner.chain(macroFn, 'A'); + runner.chain('supplied', macroFn, 'B'); + runner.chain(macroFn, 'C'); runner.run({}).then(() => { const stats = runner.buildStats(); @@ -783,8 +649,8 @@ test('match applies to macros', t => { t.is(props.title, 'foobar'); }); - runner.chain.test(macroFn, 'foo'); - runner.chain.test(macroFn, 'bar'); + runner.chain(macroFn, 'foo'); + runner.chain(macroFn, 'bar'); runner.run({}).then(() => { const stats = runner.buildStats(); @@ -821,10 +687,10 @@ test('arrays of macros', t => { const runner = new Runner(); - runner.chain.test('A', [macroFnA, macroFnB], 'A'); - runner.chain.test('B', [macroFnA, macroFnB], 'B'); - runner.chain.test('C', macroFnA, 'C'); - runner.chain.test('D', macroFnB, 'D'); + runner.chain('A', [macroFnA, macroFnB], 'A'); + runner.chain('B', [macroFnA, macroFnB], 'B'); + runner.chain('C', macroFnA, 'C'); + runner.chain('D', macroFnB, 'D'); runner.run({}).then(() => { const stats = runner.buildStats(); @@ -865,8 +731,8 @@ test('match applies to arrays of macros', t => { t.is(props.title, 'foobar'); }); - runner.chain.test([fooMacro, barMacro, bazMacro], 'foo'); - runner.chain.test([fooMacro, barMacro, bazMacro], 'bar'); + runner.chain([fooMacro, barMacro, bazMacro], 'foo'); + runner.chain([fooMacro, barMacro, bazMacro], 'bar'); runner.run({}).then(() => { const stats = runner.buildStats(); diff --git a/test/test-collection.js b/test/test-collection.js index fd933feb6..995e19290 100644 --- a/test/test-collection.js +++ b/test/test-collection.js @@ -77,47 +77,6 @@ function serialize(collection) { return removeEmptyProps(serialized); } -test('throws if no type is supplied', t => { - const collection = new TestCollection({}); - t.throws(() => { - collection.add({ - title: 'someTitle', - metadata: {} - }); - }, {message: 'Test type must be specified'}); - t.end(); -}); - -test('throws if you try to set a hook as exclusive', t => { - const collection = new TestCollection({}); - t.throws(() => { - collection.add(mockTest({ - type: 'beforeEach', - exclusive: true - })); - }, {message: '"only" cannot be used with a beforeEach hook'}); - t.end(); -}); - -test('throws if you try to set a before hook as always', t => { - const collection = new TestCollection({}); - t.throws(() => { - collection.add(mockTest({ - type: 'before', - always: true - })); - }, {message: '"always" can only be used with after and afterEach hooks'}); - t.end(); -}); - -test('throws if you try to set a test as always', t => { - const collection = new TestCollection({}); - t.throws(() => { - collection.add(mockTest({always: true}, 'test')); - }, {message: '"always" can only be used with after and afterEach hooks'}); - t.end(); -}); - test('hasExclusive is set when an exclusive test is added', t => { const collection = new TestCollection({}); t.false(collection.hasExclusive); diff --git a/test/ts-types/regression-1347.ts b/test/ts-types/regression-1347.ts index b72932b8e..16fdd863f 100644 --- a/test/ts-types/regression-1347.ts +++ b/test/ts-types/regression-1347.ts @@ -1,5 +1,6 @@ import test from '../..' test.cb('test', t => { + const err = t.throws((): void => {throw new Error()}) t.end() }) diff --git a/types/base.d.ts b/types/base.d.ts deleted file mode 100644 index 9e5aa34b1..000000000 --- a/types/base.d.ts +++ /dev/null @@ -1,144 +0,0 @@ -export type ErrorValidator - = (new (...args: any[]) => any) - | RegExp - | string - | ((error: any) => boolean); - -export interface Observable { - subscribe(observer: (value: {}) => void): void; -} -export interface SnapshotOptions { - id?: string; -} -export type Test = (t: TestContext) => PromiseLike | Iterator | Observable | void; -export type GenericTest = (t: GenericTestContext) => PromiseLike | Iterator | Observable | void; -export type CallbackTest = (t: CallbackTestContext) => void; -export type GenericCallbackTest = (t: GenericCallbackTestContext) => void; - -export interface Context { context: T } -export type AnyContext = Context; - -export type ContextualTest = GenericTest; -export type ContextualCallbackTest = GenericCallbackTest; - -export interface AssertContext { - /** - * Passing assertion. - */ - pass(message?: string): void; - /** - * Failing assertion. - */ - fail(message?: string): void; - /** - * Assert that value is truthy. - */ - truthy(value: any, message?: string): void; - /** - * Assert that value is falsy. - */ - falsy(value: any, message?: string): void; - /** - * Assert that value is true. - */ - true(value: any, message?: string): void; - /** - * Assert that value is false. - */ - false(value: any, message?: string): void; - /** - * Assert that value is equal to expected. - */ - is(value: U, expected: U, message?: string): void; - /** - * Assert that value is not equal to expected. - */ - not(value: U, expected: U, message?: string): void; - /** - * Assert that value is deep equal to expected. - */ - deepEqual(value: U, expected: U, message?: string): void; - /** - * Assert that value is not deep equal to expected. - */ - notDeepEqual(value: U, expected: U, message?: string): void; - /** - * Assert that function throws an error or promise rejects. - * @param error Can be a constructor, regex, error message or validation function. - */ - throws(value: PromiseLike, error?: ErrorValidator, message?: string): Promise; - throws(value: () => PromiseLike, error?: ErrorValidator, message?: string): Promise; - throws(value: () => void, error?: ErrorValidator, message?: string): any; - /** - * Assert that function doesn't throw an error or promise resolves. - */ - notThrows(value: PromiseLike, message?: string): Promise; - notThrows(value: () => PromiseLike, message?: string): Promise; - notThrows(value: () => void, message?: string): void; - /** - * Assert that contents matches regex. - */ - regex(contents: string, regex: RegExp, message?: string): void; - /** - * Assert that contents matches a snapshot. - */ - snapshot(contents: any, message?: string): void; - snapshot(contents: any, options: SnapshotOptions, message?: string): void; - /** - * Assert that contents does not match regex. - */ - notRegex(contents: string, regex: RegExp, message?: string): void; - /** - * Assert that error is falsy. - */ - ifError(error: any, message?: string): void; -} -export interface TestContext extends AssertContext { - /** - * Test title. - */ - title: string; - /** - * Plan how many assertion there are in the test. - * The test will fail if the actual assertion count doesn't match planned assertions. - */ - plan(count: number): void; - - skip: AssertContext; - /** - * Log values contextually alongside the test result instead of immediately printing them to `stdout`. - */ - log(...values: any[]): void; -} -export interface CallbackTestContext extends TestContext { - /** - * End the test. - */ - end(): void; -} - -export type GenericTestContext = TestContext & T; -export type GenericCallbackTestContext = CallbackTestContext & T; - -export interface Macro { - (t: T, ...args: any[]): void; - title? (providedTitle: string, ...args: any[]): string; -} -export type Macros = Macro | Macro[]; - -interface RegisterBase { - (title: string, run: GenericTest): void; - (title: string, run: Macros>, ...args: any[]): void; - (run: Macros>, ...args: any[]): void; -} - -interface CallbackRegisterBase { - (title: string, run: GenericCallbackTest): void; - (title: string, run: Macros>, ...args: any[]): void; - (run: Macros>, ...args: any[]): void; -} - -export default test; -export const test: RegisterContextual; -export interface RegisterContextual extends Register> { -} diff --git a/types/make.js b/types/make.js deleted file mode 100644 index 9516fbb61..000000000 --- a/types/make.js +++ /dev/null @@ -1,184 +0,0 @@ -'use strict'; - -// TypeScript definitions are generated here. -// AVA allows chaining of function names, like `test.after.cb.always`. -// The order of these names is not important. -// Writing these definitions by hand is hard. Because of chaining, -// the number of combinations grows fast (2^n). To reduce this number, -// illegal combinations are filtered out in `verify`. -// The order of the options is not important. We could generate full -// definitions for each possible order, but that would give a very big -// output. Instead, we write an alias for different orders. For instance, -// `after.cb` is fully written, and `cb.after` is emitted as an alias -// using `typeof after.cb`. - -const path = require('path'); -const fs = require('fs'); -const isArraySorted = require('is-array-sorted'); -const Runner = require('../lib/runner'); - -const arrayHas = parts => part => parts.indexOf(part) !== -1; - -const base = fs.readFileSync(path.join(__dirname, 'base.d.ts'), 'utf8'); - -// All suported function names -const allParts = Object.keys(new Runner({}).chain).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 -// The prefix is an array of function names -function generatePrefixed(prefix) { - let output = ''; - let children = ''; - - for (const part of allParts) { - const parts = prefix.concat([part]); - - if (prefix.indexOf(part) !== -1 || !verify(parts, true)) { - // Function already in prefix or not allowed here - continue; - } - - // If `parts` is not sorted, we alias it to the sorted chain - if (!isArraySorted(parts)) { - if (exists(parts)) { - parts.sort(); - - let chain; - if (hasChildren(parts)) { - chain = parts.join('_') + ''; - } 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}['${last}']`; - } - - output += `\t${part}: Register_${chain};\n`; - } - - continue; - } - - // Check that `part` is a valid function name. - // `always` is a valid prefix, for instance of `always.after`, - // but not a valid function name. - if (verify(parts, false)) { - if (arrayHas(parts)('todo')) { - // 'todo' functions don't have a function argument, just a string - output += `\t${part}: (name: string) => void;\n`; - } else { - if (arrayHas(parts)('cb')) { - output += `\t${part}: CallbackRegisterBase`; - } else { - output += `\t${part}: RegisterBase`; - } - - 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}`; - } - - output += ';\n'; - } - } - - children += generatePrefixed(parts); - } - - if (output === '') { - return children; - } - - const typeBody = `{\n${output}}\n${children}`; - - if (prefix.length === 0) { - // No prefix, so this is the type for the default export - return `export interface Register extends RegisterBase ${typeBody}`; - } - const namespace = ['Register'].concat(prefix).join('_'); - return `interface ${namespace} ${typeBody}`; -} - -// Checks whether a chain is a valid function name (when `asPrefix === false`) -// or a valid prefix that could contain members. -// For instance, `test.always` is not a valid function name, but it is a valid -// prefix of `test.always.after`. -function verify(parts, asPrefix) { - const has = arrayHas(parts); - - if (has('only') + has('skip') + has('todo') > 1) { - return false; - } - - const beforeAfterCount = has('before') + has('beforeEach') + has('after') + has('afterEach'); - - if (beforeAfterCount > 1) { - return false; - } - - if (beforeAfterCount === 1) { - if (has('only')) { - return false; - } - } - - if (has('always')) { - // `always` can only be used with `after` or `afterEach`. - // Without it can still be a valid prefix - if (has('after') || has('afterEach')) { - return true; - } - - if (!verify(parts.concat(['after']), false) && !verify(parts.concat(['afterEach']), false)) { - // If `after` nor `afterEach` cannot be added to this prefix, - // `always` is not allowed here. - return false; - } - - // Only allowed as a prefix - return 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)) { - // Valid function name - return true; - } - - if (!verify(parts, true)) { - // Not valid prefix - return false; - } - - // Valid prefix, check whether it has members - for (const prefix of allParts) { - if (parts.indexOf(prefix) === -1 && exists(parts.concat([prefix]))) { - return true; - } - } - - return false; -}