diff --git a/.xo-config.json b/.xo-config.json index 3d1a5f220..1a7cd8568 100644 --- a/.xo-config.json +++ b/.xo-config.json @@ -23,13 +23,12 @@ }, "overrides": [ { - "files": "index.d.ts", + "files": [ + "index.d.ts", + "types/*.d.ts" + ], "rules": { - "@typescript-eslint/member-ordering": "off", - "@typescript-eslint/method-signature-style": "off", - "@typescript-eslint/prefer-readonly-parameter-types": "off", - "@typescript-eslint/prefer-function-type": "off", - "@typescript-eslint/unified-signatures": "off" + "import/extensions": "off" } }, { @@ -71,6 +70,12 @@ "import/no-extraneous-dependencies": "off", "import/no-unresolved": "off" } + }, + { + "files": "test/macros/fixtures/macros.js", + "rules": { + "ava/no-identical-title": "off" + } } ] } diff --git a/docs/01-writing-tests.md b/docs/01-writing-tests.md index 08b818cf4..fdbad8145 100644 --- a/docs/01-writing-tests.md +++ b/docs/01-writing-tests.md @@ -294,6 +294,8 @@ console.log('Test file currently being run:', test.meta.file); Additional arguments passed to the test declaration will be passed to the test implementation. This is useful for creating reusable test macros. +You can use plain functions: + ```js function macro(t, input, expected) { t.is(eval(input), expected); @@ -303,7 +305,7 @@ test('2 + 2 = 4', macro, '2 + 2', 4); test('2 * 3 = 6', macro, '2 * 3', 6); ``` -You can build the test title programmatically by attaching a `title` function to the macro: +With AVA 3 you can build the test title programmatically by attaching a `title` function to the macro: ```js function macro(t, input, expected) { @@ -319,21 +321,35 @@ test('providedTitle', macro, '3 * 3', 9); The `providedTitle` argument defaults to `undefined` if the user does not supply a string title. This means you can use a parameter assignment to set the default value. The example above uses the empty string as the default. -You can also pass arrays of macro functions: +However with AVA 4 the preferred approach is to use the `test.macro()` helper: ```js -const safeEval = require('safe-eval'); +import test from 'ava'; -function evalMacro(t, input, expected) { +const macro = test.macro((t, input, expected) => { t.is(eval(input), expected); -} +}); -function safeEvalMacro(t, input, expected) { - t.is(safeEval(input), expected); -} +test('title', macro, '3 * 3', 9); +``` + +Or with a title function: -test([evalMacro, safeEvalMacro], '2 + 2', 4); -test([evalMacro, safeEvalMacro], '2 * 3', 6); +```js +import test from 'ava'; + +const macro = test.macro({ + exec(t, input, expected) { + t.is(eval(input), expected); + }, + title(providedTitle = '', input, expected) { + return `${providedTitle} ${input} = ${expected}`.trim(); + } +}); + +test(macro, '2 + 2', 4); +test(macro, '2 * 3', 6); +test('providedTitle', macro, '3 * 3', 9); ``` We encourage you to use macros instead of building your own test generators ([here is an example](https://github.com/avajs/ava-codemods/blob/47073b5b58aa6f3fb24f98757be5d3f56218d160/test/ok-to-truthy.js#L7-L9) of code that should be replaced with a macro). Macros are designed to perform static analysis of your code, which can lead to better performance, IDE integration, and linter rules. diff --git a/docs/03-assertions.md b/docs/03-assertions.md index 2ef16bdca..2e719ec85 100644 --- a/docs/03-assertions.md +++ b/docs/03-assertions.md @@ -229,7 +229,7 @@ Finally, this returns a boolean indicating whether the assertion passed. ### `.throws(fn, expectation?, message?)` -Assert that an error is thrown. `fn` must be a function which should throw. The thrown value *must* be an error. It is returned so you can run more assertions against it. +Assert that an error is thrown. `fn` must be a function which should throw. The thrown value *must* be an error. It is returned so you can run more assertions against it. If the assertion fails then `null` is returned. `expectation` can be an object with one or more of the following properties: @@ -257,13 +257,11 @@ test('throws', t => { }); ``` -Does not return anything. - ### `.throwsAsync(thrower, expectation?, message?)` Assert that an error is thrown. `thrower` can be an async function which should throw, or a promise that should reject. This assertion must be awaited. -The thrown value *must* be an error. It is returned so you can run more assertions against it. +The thrown value *must* be an error. It is returned so you can run more assertions against it. If the assertion fails then `null` is returned. `expectation` can be an object with one or more of the following properties: @@ -294,8 +292,6 @@ test('rejects', async t => { }); ``` -Does not return anything. - ### `.notThrows(fn, message?)` Assert that no error is thrown. `fn` must be a function which shouldn't throw. Does not return anything. @@ -330,7 +326,7 @@ AVA 3 supports an `options` object that lets you select a specific snapshot, fo In AVA 3, you cannot update snapshots while using `t.snapshot.skip()`. -### `.try(title?, implementation | macro | macro[], ...args?)` +### `.try(title?, implementation | macro, ...args?)` `.try()` allows you to *try* assertions without causing the test to fail. diff --git a/docs/recipes/typescript.md b/docs/recipes/typescript.md index 05f1f8ee8..84cc1db50 100644 --- a/docs/recipes/typescript.md +++ b/docs/recipes/typescript.md @@ -121,7 +121,9 @@ const hasLength = (t: ExecutionContext, input: string, expected: number) => { test('bar has length 3', hasLength, 'bar', 3); ``` -In order to be able to assign the `title` property to a macro you need to type the function: +### AVA 3 + +With AVA 3, in order to be able to assign the `title` property to a macro you need to type the function: ```ts import test, {Macro} from 'ava'; @@ -149,12 +151,46 @@ const macro: CbMacro<[]> = t => { test.cb(macro); ``` +### AVA 4 + +With AVA 4 you can use the `test.macro()` helper to create macros: + +```ts +import test from 'ava'; + +const macro = test.macro((t, input: string, expected: number) => { + t.is(eval(input), expected); +}); + +test('title', macro, '3 * 3', 9); +``` + +Or with a title function: + +```ts +import test from 'ava'; + +const macro = test.macro({ + exec(t, input: string, expected: number) { + t.is(eval(input), expected); + }, + title(providedTitle = '', input, expected) { + return `${providedTitle} ${input} = ${expected}`.trim(); + } +}); + +test(macro, '2 + 2', 4); +test(macro, '2 * 3', 6); +test('providedTitle', macro, '3 * 3', 9); +``` + ## Typing [`t.context`](../01-writing-tests.md#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: +By default, the type of `t.context` will be the empty object (`{}`). AVA exposes an interface `TestInterface` (in AVA 4 this is `TestFn`) 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'; +import anyTest, {TestInterface} from 'ava'; // AVA 3 +// import anyTest, {TestFn as TestInterface} from 'ava'; // AVA 4, usage is the same const test = anyTest as TestInterface<{foo: string}>; @@ -178,7 +214,7 @@ test('an actual test', t => { You can also type the context when creating macros: ```ts -import anyTest, {Macro, TestInterface} from 'ava'; +import anyTest, {Macro, TestInterface} from 'ava'; // AVA 3 interface Context { foo: string diff --git a/index.d.ts b/index.d.ts index a65c67b4a..24b722040 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,685 +1,12 @@ -export interface Subscribable { - subscribe(observer: { - error(error: any): void; - complete(): void; - }): void; -} +import type {TestFn} from './types/test-fn'; -export type Constructor = (new (...args: any[]) => any); - -/** Specify one or more expectations the thrown error must satisfy. */ -export type ThrowsExpectation = { - /** The thrown error must have a code that equals the given string or number. */ - code?: string | number; - - /** The thrown error must be an instance of this constructor. */ - instanceOf?: Constructor; - - /** The thrown error must be strictly equal to this value. */ - is?: Error; - - /** The thrown error must have a message that equals the given string, or matches the regular expression. */ - message?: string | RegExp; - - /** The thrown error must have a name that equals the given string. */ - name?: string; -}; - -export type CommitDiscardOptions = { - /** - * Whether the logs should be included in those of the parent test. - */ - retainLogs?: boolean; -}; - -export interface Assertions { - /** - * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean - * indicating whether the assertion passed. Comes with power-assert. - */ - assert: AssertAssertion; - - /** - * Assert that `actual` is [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to - * `expected`, returning a boolean indicating whether the assertion passed. - */ - deepEqual: DeepEqualAssertion; - - /** - * Assert that `value` is like `selector`, returning a boolean indicating whether the assertion passed. - */ - like: LikeAssertion; - - /** Fail the test, always returning `false`. */ - fail: FailAssertion; - - /** - * Assert that `actual` is strictly false, returning a boolean indicating whether the assertion passed. - */ - false: FalseAssertion; - - /** - * Assert that `actual` is [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy), returning a boolean - * indicating whether the assertion passed. - */ - falsy: FalsyAssertion; - - /** - * Assert that `actual` is [the same - * value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) as `expected`, - * returning a boolean indicating whether the assertion passed. - */ - is: IsAssertion; - - /** - * Assert that `actual` is not [the same - * value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) as `expected`, - * returning a boolean indicating whether the assertion passed. - */ - not: NotAssertion; - - /** - * Assert that `actual` is not [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to - * `expected`, returning a boolean indicating whether the assertion passed. - */ - notDeepEqual: NotDeepEqualAssertion; - - /** - * Assert that `string` does not match the regular expression, returning a boolean indicating whether the assertion - * passed. - */ - notRegex: NotRegexAssertion; - - /** Assert that the function does not throw. */ - notThrows: NotThrowsAssertion; - - /** Assert that the async function does not throw, or that the promise does not reject. Must be awaited. */ - notThrowsAsync: NotThrowsAsyncAssertion; - - /** Count a passing assertion, always returning `true`. */ - pass: PassAssertion; - - /** - * Assert that `string` matches the regular expression, returning a boolean indicating whether the assertion passed. - */ - regex: RegexAssertion; - - /** - * Assert that `expected` is [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to a - * previously recorded [snapshot](https://github.com/concordancejs/concordance#serialization-details), or if - * necessary record a new snapshot. - */ - snapshot: SnapshotAssertion; - - /** - * Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value. - */ - throws: ThrowsAssertion; - - /** - * Assert that the async function throws [an error](https://www.npmjs.com/package/is-error), or the promise rejects - * with one. If so, returns a promise for the error value, which must be awaited. - */ - throwsAsync: ThrowsAsyncAssertion; - - /** - * Assert that `actual` is strictly true, returning a boolean indicating whether the assertion passed. - */ - true: TrueAssertion; - - /** - * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean - * indicating whether the assertion passed. - */ - truthy: TruthyAssertion; -} - -export interface AssertAssertion { - /** - * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean - * indicating whether the assertion passed. Comes with power-assert. - */ - (actual: any, message?: string): boolean; - - /** Skip this assertion. */ - skip(actual: any, message?: string): void; -} - -export interface DeepEqualAssertion { - /** - * Assert that `actual` is [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to - * `expected`, returning a boolean indicating whether the assertion passed. - */ - (actual: Actual, expected: Expected, message?: string): actual is Expected; - - /** Skip this assertion. */ - skip(actual: any, expected: any, message?: string): void; -} - -export interface LikeAssertion { - /** - * Assert that `value` is like `selector`, returning a boolean indicating whether the assertion passed. - */ - >(value: any, selector: Expected, message?: string): value is Expected; - - /** Skip this assertion. */ - skip(value: any, selector: any, message?: string): void; -} - -export interface FailAssertion { - /** Fail the test, always returning `false`. */ - (message?: string): boolean; - - /** Skip this assertion. */ - skip(message?: string): void; -} - -export interface FalseAssertion { - /** - * Assert that `actual` is strictly false, returning a boolean indicating whether the assertion passed. - */ - (actual: any, message?: string): actual is false; - - /** Skip this assertion. */ - skip(actual: any, message?: string): void; -} - -export interface FalsyAssertion { - /** - * Assert that `actual` is [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy), returning a boolean - * indicating whether the assertion passed. - */ - (actual: any, message?: string): boolean; - - /** Skip this assertion. */ - skip(actual: any, message?: string): void; -} - -export interface IsAssertion { - /** - * Assert that `actual` is [the same - * value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) as `expected`, - * returning a boolean indicating whether the assertion passed. - */ - (actual: Actual, expected: Expected, message?: string): actual is Expected; - - /** Skip this assertion. */ - skip(actual: any, expected: any, message?: string): void; -} - -export interface NotAssertion { - /** - * Assert that `actual` is not [the same - * value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) as `expected`, - * returning a boolean indicating whether the assertion passed. - */ - (actual: Actual, expected: Expected, message?: string): boolean; - - /** Skip this assertion. */ - skip(actual: any, expected: any, message?: string): void; -} - -export interface NotDeepEqualAssertion { - /** - * Assert that `actual` is not [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to - * `expected`, returning a boolean indicating whether the assertion passed. - */ - (actual: Actual, expected: Expected, message?: string): boolean; - - /** Skip this assertion. */ - skip(actual: any, expected: any, message?: string): void; -} - -export interface NotRegexAssertion { - /** - * Assert that `string` does not match the regular expression, returning a boolean indicating whether the assertion - * passed. - */ - (string: string, regex: RegExp, message?: string): boolean; - - /** Skip this assertion. */ - skip(string: string, regex: RegExp, message?: string): void; -} - -export interface NotThrowsAssertion { - /** Assert that the function does not throw. */ - (fn: () => any, message?: string): void; - - /** Skip this assertion. */ - skip(fn: () => any, message?: string): void; -} - -export interface NotThrowsAsyncAssertion { - /** Assert that the async function does not throw. You must await the result. */ - (fn: () => PromiseLike, message?: string): Promise; - - /** Assert that the promise does not reject. You must await the result. */ - (promise: PromiseLike, message?: string): Promise; - - /** Skip this assertion. */ - skip(nonThrower: any, message?: string): void; -} - -export interface PassAssertion { - /** Count a passing assertion, always returning `true`. */ - (message?: string): boolean; - - /** Skip this assertion. */ - skip(message?: string): void; -} - -export interface RegexAssertion { - /** - * Assert that `string` matches the regular expression, returning a boolean indicating whether the assertion passed. - */ - (string: string, regex: RegExp, message?: string): boolean; - - /** Skip this assertion. */ - skip(string: string, regex: RegExp, message?: string): void; -} - -export interface SnapshotAssertion { - /** - * Assert that `expected` is [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to a - * previously recorded [snapshot](https://github.com/concordancejs/concordance#serialization-details), or if - * necessary record a new snapshot. - */ - (expected: any, message?: string): void; - - /** Skip this assertion. */ - skip(expected: any, message?: string): void; -} - -export interface ThrowsAssertion { - /** - * Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value. - * The error must satisfy all expectations. - */ - (fn: () => any, expectations?: ThrowsExpectation | null, message?: string): ThrownError; - - /** Skip this assertion. */ - skip(fn: () => any, expectations?: any, message?: string): void; -} - -export interface ThrowsAsyncAssertion { - /** - * Assert that the async function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error - * value. You must await the result. - */ - (fn: () => PromiseLike, expectations?: null, message?: string): Promise; - - /** - * Assert that the async function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error - * value. You must await the result. The error must satisfy all expectations. - */ - (fn: () => PromiseLike, expectations: ThrowsExpectation, message?: string): Promise; - - /** - * Assert that the promise rejects with [an error](https://www.npmjs.com/package/is-error). If so, returns the - * rejection reason. You must await the result. - */ - (promise: PromiseLike, expectations?: null, message?: string): Promise; - - /** - * Assert that the promise rejects with [an error](https://www.npmjs.com/package/is-error). If so, returns the - * rejection reason. You must await the result. The error must satisfy all expectations. - */ - (promise: PromiseLike, expectations: ThrowsExpectation, message?: string): Promise; - - /** Skip this assertion. */ - skip(thrower: any, expectations?: any, message?: string): void; -} - -export interface TrueAssertion { - /** - * Assert that `actual` is strictly true, returning a boolean indicating whether the assertion passed. - */ - (actual: any, message?: string): actual is true; - - /** Skip this assertion. */ - skip(actual: any, message?: string): void; -} - -export interface TruthyAssertion { - /** - * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean - * indicating whether the assertion passed. - */ - (actual: any, message?: string): boolean; - - /** Skip this assertion. */ - skip(actual: any, message?: string): void; -} - -/** The `t` value passed to test & hook implementations. */ -export interface ExecutionContext extends Assertions { - /** Test context, shared with hooks. */ - context: Context; - - /** Title of the test or hook. */ - readonly title: string; - - /** Whether the test has passed. Only accurate in afterEach hooks. */ - readonly passed: boolean; - - log: LogFn; - plan: PlanFn; - teardown: TeardownFn; - timeout: TimeoutFn; - try: TryFn; -} - -export interface LogFn { - /** Log one or more values. */ - (...values: any[]): void; - - /** Skip logging. */ - skip(...values: any[]): void; -} - -export interface PlanFn { - /** - * Plan how many assertion there are in the test. The test will fail if the actual assertion count doesn't match the - * number of planned assertions. See [assertion planning](https://github.com/avajs/ava#assertion-planning). - */ - (count: number): void; - - /** Don't plan assertions. */ - skip(count: number): void; -} - -export interface TimeoutFn { - /** - * Set a timeout for the test, in milliseconds. The test will fail if the timeout is exceeded. - * The timeout is reset each time an assertion is made. - */ - (ms: number, message?: string): void; -} - -export interface TeardownFn { - /** Declare a function to be run after the test has ended. */ - (fn: () => void): void; -} - -export interface TryFn { - /** - * Attempt to run some assertions. The result must be explicitly committed or discarded or else - * the test will fail. A macro may be provided. The title may help distinguish attempts from - * one another. - */ - (title: string, fn: EitherMacro, ...args: Args): Promise; - - /** - * Attempt to run some assertions. The result must be explicitly committed or discarded or else - * the test will fail. A macro may be provided. The title may help distinguish attempts from - * one another. - */ - (title: string, fn: [EitherMacro, ...Array>], ...args: Args): Promise; - - /** - * Attempt to run some assertions. The result must be explicitly committed or discarded or else - * the test will fail. A macro may be provided. - */ - (fn: EitherMacro, ...args: Args): Promise; - - /** - * Attempt to run some assertions. The result must be explicitly committed or discarded or else - * the test will fail. A macro may be provided. - */ - (fn: [EitherMacro, ...Array>], ...args: Args): Promise; -} - -export interface AssertionError extends Error {} - -export interface TryResult { - /** - * Title of the attempt, helping you tell attempts aparts. - */ - title: string; - - /** - * Indicates whether all assertions passed, or at least one failed. - */ - passed: boolean; - - /** - * Errors raised for each failed assertion. - */ - errors: AssertionError[]; - - /** - * Logs created during the attempt using `t.log()`. Contains formatted values. - */ - logs: string[]; - - /** - * Commit the attempt. Counts as one assertion for the plan count. If the - * attempt failed, calling this will also cause your test to fail. - */ - commit(options?: CommitDiscardOptions): void; - - /** - * Discard the attempt. - */ - discard(options?: CommitDiscardOptions): void; -} - -// FIXME(novemberborn) Refactor implementations to be different types returning a promise,, subscribable, or void, not a -// single type returning a union. A union with void as a return type doesn't make sense. -export type ImplementationResult = PromiseLike | Subscribable | boolean | void; -export type Implementation = (t: ExecutionContext) => ImplementationResult; - -/** A reusable test or hook implementation. */ -export type UntitledMacro = (t: ExecutionContext, ...args: Args) => ImplementationResult; - -/** A reusable test or hook implementation. */ -export type Macro = UntitledMacro & { - /** - * Implement this function to generate a test (or hook) title whenever this macro is used. `providedTitle` contains - * the title provided when the test or hook was declared. Also receives the remaining test arguments. - */ - title?: (providedTitle: string | undefined, ...args: Args) => string; -}; - -export type EitherMacro = Macro | UntitledMacro; - -/** Alias for a single macro, or an array of macros. */ -export type OneOrMoreMacros = EitherMacro | [EitherMacro, ...Array>]; - -export interface TestInterface { - /** Declare a concurrent test. */ - (title: string, implementation: Implementation): void; - - /** Declare a concurrent test that uses one or more macros. Additional arguments are passed to the macro. */ - (title: string, macros: OneOrMoreMacros, ...rest: T): void; - - /** Declare a concurrent test that uses one or more macros. The macro is responsible for generating a unique test title. */ - (macros: OneOrMoreMacros, ...rest: T): void; - - /** Declare a hook that is run once, after all tests have passed. */ - after: AfterInterface; - - /** Declare a hook that is run after each passing test. */ - afterEach: AfterInterface; - - /** Declare a hook that is run once, before all tests. */ - before: BeforeInterface; - - /** Declare a hook that is run before each test. */ - beforeEach: BeforeInterface; - - /** Declare a test that is expected to fail. */ - failing: FailingInterface; - - /** Declare tests and hooks that are run serially. */ - serial: SerialInterface; - - only: OnlyInterface; - skip: SkipInterface; - todo: TodoDeclaration; - meta: MetaInterface; -} - -export interface AfterInterface { - /** Declare a hook that is run once, after all tests have passed. */ - (implementation: Implementation): void; - - /** Declare a hook that is run once, after all tests have passed. */ - (title: string, implementation: Implementation): void; - - /** Declare a hook that is run once, after all tests have passed. Additional arguments are passed to the macro. */ - (title: string, macros: OneOrMoreMacros, ...rest: T): void; - - /** Declare a hook that is run once, after all tests have passed. */ - (macros: OneOrMoreMacros, ...rest: T): void; - - /** Declare a hook that is run once, after all tests are done. */ - always: AlwaysInterface; - - skip: HookSkipInterface; -} - -export interface AlwaysInterface { - /** Declare a hook that is run once, after all tests are done. */ - (implementation: Implementation): void; - - /** Declare a hook that is run once, after all tests are done. */ - (title: string, implementation: Implementation): void; - - /** Declare a hook that is run once, after all tests are done. Additional arguments are passed to the macro. */ - (title: string, macros: OneOrMoreMacros, ...rest: T): void; - - /** Declare a hook that is run once, after all tests are done. */ - (macros: OneOrMoreMacros, ...rest: T): void; - - skip: HookSkipInterface; -} - -export interface BeforeInterface { - /** Declare a hook that is run once, before all tests. */ - (implementation: Implementation): void; - - /** Declare a hook that is run once, before all tests. */ - (title: string, implementation: Implementation): void; - - /** Declare a hook that is run once, before all tests. Additional arguments are passed to the macro. */ - (title: string, macros: OneOrMoreMacros, ...rest: T): void; - - /** Declare a hook that is run once, before all tests. */ - (macros: OneOrMoreMacros, ...rest: T): void; - - skip: HookSkipInterface; -} - -export interface FailingInterface { - /** Declare a concurrent test. The test is expected to fail. */ - (title: string, implementation: Implementation): void; - - /** - * Declare a concurrent test that uses one or more macros. Additional arguments are passed to the macro. - * The test is expected to fail. - */ - (title: string, macros: OneOrMoreMacros, ...rest: T): void; - - /** - * Declare a concurrent test that uses one or more macros. The macro is responsible for generating a unique test title. - * The test is expected to fail. - */ - (macros: OneOrMoreMacros, ...rest: T): void; - - only: OnlyInterface; - skip: SkipInterface; -} - -export interface HookSkipInterface { - /** Skip this hook. */ - (implementation: Implementation): void; - - /** Skip this hook. */ - (title: string, implementation: Implementation): void; - - /** Skip this hook. */ - (title: string, macros: OneOrMoreMacros, ...rest: T): void; - - /** Skip this hook. */ - (macros: OneOrMoreMacros, ...rest: T): void; -} - -export interface OnlyInterface { - /** Declare a test. Only this test and others declared with `.only()` are run. */ - (title: string, implementation: Implementation): void; - - /** - * Declare a test that uses one or more macros. Additional arguments are passed to the macro. - * Only this test and others declared with `.only()` are run. - */ - (title: string, macros: OneOrMoreMacros, ...rest: T): void; - - /** - * Declare a test that uses one or more macros. The macro is responsible for generating a unique test title. - * Only this test and others declared with `.only()` are run. - */ - (macros: OneOrMoreMacros, ...rest: T): void; -} - -export interface SerialInterface { - /** Declare a serial test. */ - (title: string, implementation: Implementation): void; - - /** Declare a serial test that uses one or more macros. Additional arguments are passed to the macro. */ - (title: string, macros: OneOrMoreMacros, ...rest: T): void; - - /** - * Declare a serial test that uses one or more macros. The macro is responsible for generating a unique test title. - */ - (macros: OneOrMoreMacros, ...rest: T): void; - - /** Declare a serial hook that is run once, after all tests have passed. */ - after: AfterInterface; - - /** Declare a serial hook that is run after each passing test. */ - afterEach: AfterInterface; - - /** Declare a serial hook that is run once, before all tests. */ - before: BeforeInterface; - - /** Declare a serial hook that is run before each test. */ - beforeEach: BeforeInterface; - - /** Declare a serial test that is expected to fail. */ - failing: FailingInterface; - - only: OnlyInterface; - skip: SkipInterface; - todo: TodoDeclaration; -} - -export interface SkipInterface { - /** Skip this test. */ - (title: string, implementation: Implementation): void; - - /** Skip this test. */ - (title: string, macros: OneOrMoreMacros, ...rest: T): void; - - /** Skip this test. */ - (macros: OneOrMoreMacros, ...rest: T): void; -} - -export interface TodoDeclaration { - /** Declare a test that should be implemented later. */ - (title: string): void; -} - -export interface MetaInterface { - /** Path to the test file being executed. */ - file: string; - - /** Directory where snapshots are stored. */ - snapshotDirectory: string; -} +export * from './types/assertions'; +export * from './types/try-fn'; +export * from './types/test-fn'; +export * from './types/subscribable'; /** Call to declare a test, or chain to declare hooks or test modifiers */ -declare const test: TestInterface; +declare const test: TestFn; /** Call to declare a test, or chain to declare hooks or test modifiers */ export default test; diff --git a/lib/assert.js b/lib/assert.js index 83ee3f62d..ff69b46b9 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -469,7 +469,7 @@ export class Assertions { let [fn, expectations, message] = args; if (!checkMessage('throws', message)) { - return; + return null; } if (typeof fn !== 'function') { @@ -479,14 +479,14 @@ export class Assertions { message: '`t.throws()` must be called with a function', values: [formatWithLabel('Called with:', fn)] })); - return; + return null; } try { expectations = validateExpectations('throws', expectations, args.length, experiments); } catch (error) { fail(error); - return; + return null; } let retval; @@ -501,7 +501,7 @@ export class Assertions { message, values: [formatWithLabel('Function returned a promise. Use `t.throwsAsync()` instead:', retval)] })); - return; + return null; } } catch (error) { actual = error; @@ -513,7 +513,7 @@ export class Assertions { message, values: [formatWithLabel('Function returned:', retval)] })); - return; + return null; } try { @@ -531,11 +531,11 @@ export class Assertions { } }); - this.throwsAsync = withSkip((...args) => { + this.throwsAsync = withSkip(async (...args) => { let [thrower, expectations, message] = args; if (!checkMessage('throwsAsync', message)) { - return Promise.resolve(); + return null; } if (typeof thrower !== 'function' && !isPromise(thrower)) { @@ -545,14 +545,14 @@ export class Assertions { message: '`t.throwsAsync()` must be called with a function or promise', values: [formatWithLabel('Called with:', thrower)] })); - return Promise.resolve(); + return null; } try { expectations = validateExpectations('throwsAsync', expectations, args.length, experiments); } catch (error) { fail(error); - return Promise.resolve(); + return null; } const handlePromise = async (promise, wasReturned) => { @@ -583,6 +583,7 @@ export class Assertions { return await intermediate; } catch { // Don't reject the returned promise, even if the assertion fails. + return null; } }; @@ -605,7 +606,7 @@ export class Assertions { actualStack: actual.stack, values: [formatWithLabel('Function threw synchronously. Use `t.throws()` instead:', actual)] })); - return Promise.resolve(); + return null; } if (isPromise(retval)) { @@ -617,7 +618,7 @@ export class Assertions { message, values: [formatWithLabel('Function returned:', retval)] })); - return Promise.resolve(); + return null; }); this.notThrows = withSkip((fn, message) => { diff --git a/lib/create-chain.js b/lib/create-chain.js index b4d36a912..7f283ad45 100644 --- a/lib/create-chain.js +++ b/lib/create-chain.js @@ -91,6 +91,14 @@ export default function createChain(fn, defaults, meta) { root.todo = startChain('test.todo', fn, {...defaults, type: 'test', todo: true}); root.serial.todo = startChain('test.serial.todo', fn, {...defaults, serial: true, type: 'test', todo: true}); + root.macro = options => { + if (typeof options === 'function') { + return Object.freeze({exec: options}); + } + + return Object.freeze({exec: options.exec, title: options.title}); + }; + root.meta = meta; return root; diff --git a/lib/parse-test-args.js b/lib/parse-test-args.js index cb19954b5..de2f55481 100644 --- a/lib/parse-test-args.js +++ b/lib/parse-test-args.js @@ -1,14 +1,26 @@ -const normalize = title => typeof title === 'string' ? title.trim().replace(/\s+/g, ' ') : title; +const buildTitle = (raw, implementation, args) => { + let value = implementation && implementation.title ? implementation.title(raw, ...args) : raw; + const isValid = typeof value === 'string'; + if (isValid) { + value = value.trim().replace(/\s+/g, ' '); + } + + return { + raw, + value, + isSet: value !== undefined, + isValid, + isEmpty: !isValid || value === '' + }; +}; export default function parseTestArgs(args) { const rawTitle = typeof args[0] === 'string' ? args.shift() : undefined; - const receivedImplementationArray = Array.isArray(args[0]); - const implementations = receivedImplementationArray ? args.shift() : args.splice(0, 1); + const implementation = args.shift(); - const buildTitle = implementation => { - const title = normalize(implementation.title ? implementation.title(rawTitle, ...args) : rawTitle); - return {title, isSet: typeof title !== 'undefined', isValid: typeof title === 'string', isEmpty: !title}; + return { + args, + implementation: implementation && implementation.exec ? implementation.exec : implementation, + title: buildTitle(rawTitle, implementation, args) }; - - return {args, buildTitle, implementations, rawTitle, receivedImplementationArray}; } diff --git a/lib/runner.js b/lib/runner.js index e017e6b67..a0aca6ce1 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -100,94 +100,95 @@ export default class Runner extends Emittery { metadata.taskIndex = this.nextTaskIndex++; - const {args, buildTitle, implementations, rawTitle} = parseTestArgs(testArgs); + const {args, implementation, title} = parseTestArgs(testArgs); if (this.checkSelectedByLineNumbers) { metadata.selected = this.checkSelectedByLineNumbers(); } if (metadata.todo) { - if (implementations.length > 0) { + if (implementation) { throw new TypeError('`todo` tests are not allowed to have an implementation. Use `test.skip()` for tests with an implementation.'); } - if (!rawTitle) { // Either undefined or a string. + if (!title.raw) { // Either undefined or a string. throw new TypeError('`todo` tests require a title'); } - if (!this.registerUniqueTitle(rawTitle)) { - throw new Error(`Duplicate test title: ${rawTitle}`); + if (!this.registerUniqueTitle(title.value)) { + throw new Error(`Duplicate test title: ${title.value}`); } // --match selects TODO tests. - if (this.match.length > 0 && matcher([rawTitle], this.match).length === 1) { + if (this.match.length > 0 && matcher([title.value], this.match).length === 1) { metadata.exclusive = true; this.runOnlyExclusive = true; } - this.tasks.todo.push({title: rawTitle, metadata}); + this.tasks.todo.push({title: title.value, metadata}); this.emit('stateChange', { type: 'declared-test', - title: rawTitle, + title: title.value, knownFailing: false, todo: true }); } else { - if (implementations.length === 0) { + if (!implementation) { throw new TypeError('Expected an implementation. Use `test.todo()` for tests without an implementation.'); } - for (const implementation of implementations) { - let {title, isSet, isValid, isEmpty} = buildTitle(implementation); + if (Array.isArray(implementation)) { + throw new TypeError('AVA 4 no longer supports multiple implementations.'); + } + + if (title.isSet && !title.isValid) { + throw new TypeError('Test & hook titles must be strings'); + } - if (isSet && !isValid) { - throw new TypeError('Test & hook titles must be strings'); + let fallbackTitle = title.value; + if (title.isEmpty) { + if (metadata.type === 'test') { + throw new TypeError('Tests must have a title'); + } else if (metadata.always) { + fallbackTitle = `${metadata.type}.always hook`; + } else { + fallbackTitle = `${metadata.type} hook`; } + } - if (isEmpty) { - if (metadata.type === 'test') { - throw new TypeError('Tests must have a title'); - } else if (metadata.always) { - title = `${metadata.type}.always hook`; - } else { - title = `${metadata.type} hook`; - } + if (metadata.type === 'test' && !this.registerUniqueTitle(title.value)) { + throw new Error(`Duplicate test title: ${title.value}`); + } + + const task = { + title: title.value || fallbackTitle, + implementation, + args, + metadata: {...metadata} + }; + + if (metadata.type === 'test') { + if (this.match.length > 0) { + // --match overrides .only() + task.metadata.exclusive = matcher([title.value], this.match).length === 1; } - if (metadata.type === 'test' && !this.registerUniqueTitle(title)) { - throw new Error(`Duplicate test title: ${title}`); + if (task.metadata.exclusive) { + this.runOnlyExclusive = true; } - const task = { - title, - implementation, - args, - metadata: {...metadata} - }; + this.tasks[metadata.serial ? 'serial' : 'concurrent'].push(task); - if (metadata.type === 'test') { - if (this.match.length > 0) { - // --match overrides .only() - task.metadata.exclusive = matcher([title], this.match).length === 1; - } - - if (task.metadata.exclusive) { - this.runOnlyExclusive = true; - } - - this.tasks[metadata.serial ? 'serial' : 'concurrent'].push(task); - - this.snapshots.touch(title, metadata.taskIndex); - - this.emit('stateChange', { - type: 'declared-test', - title, - knownFailing: metadata.failing, - todo: false - }); - } else if (!metadata.skipped) { - this.tasks[metadata.type + (metadata.always ? 'Always' : '')].push(task); - } + this.snapshots.touch(title.value, metadata.taskIndex); + + this.emit('stateChange', { + type: 'declared-test', + title: title.value, + knownFailing: metadata.failing, + todo: false + }); + } else if (!metadata.skipped) { + this.tasks[metadata.type + (metadata.always ? 'Always' : '')].push(task); } } }, { diff --git a/lib/test.js b/lib/test.js index 14d96475f..313f3115a 100644 --- a/lib/test.js +++ b/lib/test.js @@ -81,90 +81,86 @@ class ExecutionContext extends Assertions { throw error; } - const {args, buildTitle, implementations, receivedImplementationArray} = parseTestArgs(attemptArgs); + const {args, implementation, title} = parseTestArgs(attemptArgs); - if (implementations.length === 0) { + if (!implementation) { throw new TypeError('Expected an implementation.'); } - const attemptPromises = implementations.map((implementation, index) => { - let {title, isSet, isValid, isEmpty} = buildTitle(implementation); + if (Array.isArray(implementation)) { + throw new TypeError('AVA 4 no longer supports t.try() with multiple implementations.'); + } - if (!isSet || isEmpty) { - title = `${test.title} ─ attempt ${test.attemptCount + 1 + index}`; - } else if (isValid) { - title = `${test.title} ─ ${title}`; - } else { - throw new TypeError('`t.try()` titles must be strings'); // Throw synchronously! - } + let attemptTitle; + if (!title.isSet || title.isEmpty) { + attemptTitle = `${test.title} ─ attempt ${test.attemptCount + 1}`; + } else if (title.isValid) { + attemptTitle = `${test.title} ─ ${title.value}`; + } else { + throw new TypeError('`t.try()` titles must be strings'); + } - if (!test.registerUniqueTitle(title)) { - throw new Error(`Duplicate test title: ${title}`); - } + if (!test.registerUniqueTitle(attemptTitle)) { + throw new Error(`Duplicate test title: ${attemptTitle}`); + } - return {implementation, title}; - }).map(async ({implementation, title}) => { - let committed = false; - let discarded = false; - - const {assertCount, deferredSnapshotRecordings, errors, logs, passed, snapshotCount, startingSnapshotCount} = await test.runAttempt(title, t => implementation(t, ...args)); - - return { - errors, - logs: [...logs], // Don't allow modification of logs. - passed, - title, - commit: ({retainLogs = true} = {}) => { - if (committed) { - return; - } + let committed = false; + let discarded = false; - if (discarded) { - test.saveFirstError(new Error('Can’t commit a result that was previously discarded')); - return; - } + const {assertCount, deferredSnapshotRecordings, errors, logs, passed, snapshotCount, startingSnapshotCount} = await test.runAttempt(attemptTitle, t => implementation(t, ...args)); - committed = true; - test.finishAttempt({ - assertCount, - commit: true, - deferredSnapshotRecordings, - errors, - logs, - passed, - retainLogs, - snapshotCount, - startingSnapshotCount - }); - }, - discard: ({retainLogs = false} = {}) => { - if (committed) { - test.saveFirstError(new Error('Can’t discard a result that was previously committed')); - return; - } + return { + errors, + logs: [...logs], // Don't allow modification of logs. + passed, + title: attemptTitle, + commit: ({retainLogs = true} = {}) => { + if (committed) { + return; + } - if (discarded) { - return; - } + if (discarded) { + test.saveFirstError(new Error('Can’t commit a result that was previously discarded')); + return; + } - discarded = true; - test.finishAttempt({ - assertCount: 0, - commit: false, - deferredSnapshotRecordings, - errors, - logs, - passed, - retainLogs, - snapshotCount, - startingSnapshotCount - }); + committed = true; + test.finishAttempt({ + assertCount, + commit: true, + deferredSnapshotRecordings, + errors, + logs, + passed, + retainLogs, + snapshotCount, + startingSnapshotCount + }); + }, + discard: ({retainLogs = false} = {}) => { + if (committed) { + test.saveFirstError(new Error('Can’t discard a result that was previously committed')); + return; } - }; - }); - const results = await Promise.all(attemptPromises); - return receivedImplementationArray ? results : results[0]; + if (discarded) { + return; + } + + discarded = true; + test.finishAttempt({ + assertCount: 0, + commit: false, + deferredSnapshotRecordings, + errors, + logs, + passed, + retainLogs, + snapshotCount, + startingSnapshotCount + }); + } + }; }; } diff --git a/package.json b/package.json index 2b22505d6..446735df7 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,9 @@ "files": [ "entrypoints", "lib", - "*.d.ts" + "types", + "index.d.ts", + "plugin.d.ts" ], "keywords": [ "🦄", diff --git a/test-d/context.ts b/test-d/context.ts index 144d93940..1e1327433 100644 --- a/test-d/context.ts +++ b/test-d/context.ts @@ -1,17 +1,16 @@ import {expectError, expectType} from 'tsd'; -import anyTest, {Macro, TestInterface} from '..'; +import anyTest, {ExecutionContext, TestFn} from '..'; interface Context { foo: string; } -const test = anyTest as TestInterface; // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion +const test = anyTest as TestFn; // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion -const macro: Macro<[number], Context> = (t, expected) => { +const macro = test.macro((t, expected: number) => { expectType(t.context.foo); - expectType(expected); -}; +}); test.beforeEach(t => { expectType(t.context); @@ -22,3 +21,12 @@ expectError(test('foo is bar', macro, 'bar')); anyTest('default context is unknown', t => { expectType(t.context); }); + +// See https://github.com/avajs/ava/issues/2253 +interface Covariant extends Context { + bar: number; +} + +const test2 = anyTest as TestFn; // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion +const hook = (t: ExecutionContext) => {}; +test2.beforeEach(hook); diff --git a/test-d/implementation-result.ts b/test-d/implementation-result.ts new file mode 100644 index 000000000..b3117613e --- /dev/null +++ b/test-d/implementation-result.ts @@ -0,0 +1,25 @@ +import test from '..'; + +test('return a promise-like', t => { + return { + then(resolve) { + resolve?.(); + } + }; +}); + +test('return a subscribable', t => { + return { + subscribe({complete}) { + complete(); + } + }; +}); + +test.after('return anything else', t => { + return { + foo: 'bar', + subscribe() {}, + then() {} + }; +}); diff --git a/test-d/macros.ts b/test-d/macros.ts index 533a42717..95d14d668 100644 --- a/test-d/macros.ts +++ b/test-d/macros.ts @@ -1,75 +1,96 @@ import {expectType} from 'tsd'; -import test, {ExecutionContext, Macro} from '..'; +import test, {ExecutionContext} from '..'; -// Explicitly type as a macro. +// Typed arguments through generics. { - const hasLength: Macro<[string, number]> = (t, input, expected) => { + const hasLength = test.macro<[string, number]>((t, input, expected) => { expectType(input); expectType(expected); - }; + }); test('bar has length 3', hasLength, 'bar', 3); - test('bar has length 3', [hasLength], 'bar', 3); } -// Infer macro { - const hasLength = (t: ExecutionContext, input: string, expected: number) => {}; + const hasLength = test.macro<[string, number]>({ + exec(t, input, expected) { + expectType(input); + expectType(expected); + }, + title(providedTitle, input, expected) { + expectType(input); + expectType(expected); + return 'title'; + } + }); test('bar has length 3', hasLength, 'bar', 3); - test('bar has length 3', [hasLength], 'bar', 3); } -// Multiple macros +// Typed arguments in execution function. { - const hasLength = (t: ExecutionContext, input: string, expected: number) => {}; - const hasCodePoints = (t: ExecutionContext, input: string, expected: number) => {}; + const hasLength = test.macro((t, input: string, expected: number) => {}); - test('bar has length 3', [hasLength, hasCodePoints], 'bar', 3); + test('bar has length 3', hasLength, 'bar', 3); } -// No title { - const hasLength: Macro<[string, number]> = (t, input, expected) => {}; - const hasCodePoints: Macro<[string, number]> = (t, input, expected) => {}; + const hasLength = test.macro({ + exec(t, input: string, expected: number) {}, + title(providedTitle, input, expected) { + expectType(input); + expectType(expected); + return 'title'; + } + }); + + test('bar has length 3', hasLength, 'bar', 3); +} + +// Untyped arguments +{ + const hasLength = test.macro((t, input, expected) => { + expectType(input); + expectType(expected); + }); + + test('bar has length 3', hasLength, 'bar', 3); +} + +// Usable without title, even if the macro lacks a title function. +{ + const hasLength = test.macro<[string, number]>((t, input, expected) => {}); test(hasLength, 'bar', 3); - test([hasLength, hasCodePoints], 'bar', 3); } // No arguments { - const pass: Macro<[]> = (t, ...args) => { // eslint-disable-line @typescript-eslint/ban-types - expectType<[]>(args); // eslint-disable-line @typescript-eslint/ban-types - }; - - pass.title = (providedTitle, ...args) => { - expectType(providedTitle); - expectType<[]>(args); // eslint-disable-line @typescript-eslint/ban-types - return ''; - }; + const pass = test.macro<[]>({ // eslint-disable-line @typescript-eslint/ban-types + exec(t, ...args) { + expectType<[]>(args); // eslint-disable-line @typescript-eslint/ban-types + }, + title(providedTitle, ...args) { + expectType(providedTitle); + expectType<[]>(args); // eslint-disable-line @typescript-eslint/ban-types + return ''; + } + }); test(pass); } -// Inline +// Inline function with explicit argument types. test('has length 3', (t: ExecutionContext, input: string, expected: number) => {}, 'bar', 3); -test((t: ExecutionContext, input: string, expected: number) => {}, 'bar', 3); - -// Completely infer parameters +// Completely inferred arguments for inline functions. test('has length 3', (t, input, expected) => { expectType(input); expectType(expected); }, 'foo', 3); -test((t, input, expected) => { - expectType(input); - expectType(expected); -}, 'foo', 3); - -test.skip((t, input, expected) => { +test.skip('skip', (t, input, expected) => { expectType(input); expectType(expected); }, 'foo', 3); diff --git a/test-d/throws.ts b/test-d/throws.ts index fa9dcd78f..ea25e1305 100644 --- a/test-d/throws.ts +++ b/test-d/throws.ts @@ -12,13 +12,15 @@ class CustomError extends Error { } test('throws', t => { - expectType(t.throws(() => {})); - const error2: CustomError = t.throws(() => {}); - expectType(error2); - expectType(t.throws(() => {})); + expectType(t.throws(() => {})); + const error2: CustomError | null = t.throws(() => {}); + expectType(error2); + expectType(t.throws(() => {})); }); test('throwsAsync', async t => { - expectType(await t.throwsAsync(Promise.reject())); - expectType(await t.throwsAsync(Promise.reject())); + expectType(await t.throwsAsync(async () => {})); + expectType(await t.throwsAsync(async () => {})); + expectType(await t.throwsAsync(Promise.reject())); + expectType(await t.throwsAsync(Promise.reject())); }); diff --git a/test-d/try-commit.ts b/test-d/try-commit.ts index 055c3e931..a773dc867 100644 --- a/test-d/try-commit.ts +++ b/test-d/try-commit.ts @@ -29,16 +29,6 @@ test('attempt with title', async t => { attempt.commit(); }); -test('multiple attempts', async t => { - const attempts = [ - ...await t.try([tt => tt.pass(), tt => tt.pass()]), - ...await t.try('title', [tt => tt.pass(), tt => tt.pass()]) - ]; - for (const attempt of attempts) { - attempt.commit(); - } -}); - { const lengthCheck = (t: ExecutionContext, a: string, b: number): void => { t.is(a.length, b); @@ -58,31 +48,23 @@ test('multiple attempts', async t => { test('all possible variants to pass to t.try', async t => { // No params t.try(tt => tt.pass()); - /* Fails as expected */ // t.try([]); - t.try([tt => tt.pass()]); - t.try([tt => tt.pass(), tt => tt.fail()]); t.try('test', tt => tt.pass()); - /* Fails as expected */ // t.try('test', []); - t.try('test', [tt => tt.pass()]); - t.try('test', [tt => tt.pass(), tt => tt.fail()]); // Some params t.try((tt, a, b) => tt.is(a.length, b), 'hello', 5); - /* Fails as expected */ // t.try([], 'hello', 5); - t.try([(tt, a, b) => tt.is(a.length, b)], 'hello', 5); - t.try([(tt, a, b) => tt.is(a.length, b), (tt, a, b) => tt.is(a.slice(b), '')], 'hello', 5); t.try('test', (tt, a, b) => tt.is(a.length, b), 'hello', 5); - /* Fails as expected */ // t.try('test', [], 'hello', 5); - t.try('test', [(tt, a, b) => tt.is(a.length, b)], 'hello', 5); - t.try('test', [(tt, a, b) => tt.is(a.length, b), (tt, a, b) => tt.is(a.slice(b), '')], 'hello', 5); // Macro with title - const macro1: Macro<[string, number]> = (tt, a, b) => tt.is(a.length, b); - macro1.title = (title, a, b) => `${title ? `${String(title)} ` : ''}str: "${String(a)}" with len: "${String(b)}"`; - const macro2: Macro<[string, number]> = (tt, a, b) => tt.is(a.slice(b), ''); + const macro1 = test.macro<[string, number]>({ + exec: (tt, a, b) => tt.is(a.length, b), + title: (title, a, b) => `${title ? `${String(title)} ` : ''}str: "${String(a)}" with len: "${String(b)}"` + }); + const macro2 = test.macro<[string, number]>((tt, a, b) => tt.is(a.slice(b), '')); - t.try([macro1, macro2], 'hello', 5); - t.try('title', [macro1, macro2], 'hello', 5); + t.try(macro1, 'hello', 5); + t.try(macro2, 'hello', 5); + t.try('title', macro1, 'hello', 5); + t.try('title', macro2, 'hello', 5); }); diff --git a/test-tap/assert.js b/test-tap/assert.js index 491cd459f..4891b52ce 100644 --- a/test-tap/assert.js +++ b/test-tap/assert.js @@ -112,6 +112,16 @@ function failsWith(t, fn, subset, {expectBoolean = true} = {}) { } function throwsAsyncFails(t, fn, subset) { + return add(() => { + lastFailure = null; + return fn().then(retval => { + t.equal(retval, null); + assertFailure(t, subset); + }); + }); +} + +function notThrowsAsyncFails(t, fn, subset) { return add(() => { lastFailure = null; return fn().then(retval => { @@ -160,6 +170,8 @@ function throwsAsyncPasses(t, fn) { }); } +const notThrowsAsyncPasses = throwsAsyncPasses; + test('.pass()', t => { passes(t, () => { return assertions.pass(); @@ -1521,31 +1533,31 @@ test('.notThrows()', gather(t => { test('.notThrowsAsync()', gather(t => { // Passes because the promise is resolved - throwsAsyncPasses(t, () => assertions.notThrowsAsync(Promise.resolve())); + notThrowsAsyncPasses(t, () => assertions.notThrowsAsync(Promise.resolve())); - throwsAsyncPasses(t, () => { + notThrowsAsyncPasses(t, () => { return assertions.notThrowsAsync(Promise.resolve()); }); // Fails because the promise is rejected - throwsAsyncFails(t, () => assertions.notThrowsAsync(Promise.reject(new Error())), { + notThrowsAsyncFails(t, () => assertions.notThrowsAsync(Promise.reject(new Error())), { assertion: 'notThrowsAsync', message: '', values: [{label: 'Promise rejected with:', formatted: /Error/}] }); // Passes because the function returned a resolved promise - throwsAsyncPasses(t, () => assertions.notThrowsAsync(() => Promise.resolve())); + notThrowsAsyncPasses(t, () => assertions.notThrowsAsync(() => Promise.resolve())); // Fails because the function returned a rejected promise - throwsAsyncFails(t, () => assertions.notThrowsAsync(() => Promise.reject(new Error())), { + notThrowsAsyncFails(t, () => assertions.notThrowsAsync(() => Promise.reject(new Error())), { assertion: 'notThrowsAsync', message: '', values: [{label: 'Returned promise rejected with:', formatted: /Error/}] }); // Fails because the function throws synchronously - throwsAsyncFails(t, () => assertions.notThrowsAsync(() => { + notThrowsAsyncFails(t, () => assertions.notThrowsAsync(() => { throw new Error('sync'); }, 'message'), { assertion: 'notThrowsAsync', @@ -1556,7 +1568,7 @@ test('.notThrowsAsync()', gather(t => { }); // Fails because the function did not return a promise - throwsAsyncFails(t, () => assertions.notThrowsAsync(() => {}, 'message'), { + notThrowsAsyncFails(t, () => assertions.notThrowsAsync(() => {}, 'message'), { assertion: 'notThrowsAsync', message: 'message', values: [ @@ -1564,7 +1576,7 @@ test('.notThrowsAsync()', gather(t => { ] }); - throwsAsyncFails(t, () => assertions.notThrowsAsync(Promise.resolve(), null), { + notThrowsAsyncFails(t, () => assertions.notThrowsAsync(Promise.resolve(), null), { assertion: 'notThrowsAsync', improperUsage: true, message: 'The assertion message must be a string', diff --git a/test-tap/runner.js b/test-tap/runner.js index 967729498..90cae952c 100644 --- a/test-tap/runner.js +++ b/test-tap/runner.js @@ -684,86 +684,6 @@ test('match applies to macros', t => { }); }); -test('arrays of macros', t => { - const expectedArgsA = [ - ['A'], - ['B'], - ['C'] - ]; - - const expectedArgsB = [ - ['A'], - ['B'], - ['D'] - ]; - - function macroFnA(a, ...rest) { - t.same(rest, expectedArgsA.shift()); - a.pass(); - } - - macroFnA.title = prefix => `${prefix}.A`; - - function macroFnB(a, ...rest) { - t.same(rest, expectedArgsB.shift()); - a.pass(); - } - - macroFnB.title = prefix => `${prefix}.B`; - - return promiseEnd(new Runner({file: import.meta.url}), runner => { - runner.on('stateChange', evt => { - if (evt.type === 'test-passed') { - t.pass(); - } - }); - - runner.chain('A', [macroFnA, macroFnB], 'A'); - runner.chain('B', [macroFnA, macroFnB], 'B'); - runner.chain('C', macroFnA, 'C'); - runner.chain('D', macroFnB, 'D'); - }).then(() => { - t.equal(expectedArgsA.length, 0); - t.equal(expectedArgsB.length, 0); - }); -}); - -test('match applies to arrays of macros', t => { - t.plan(1); - - // Foo - function fooMacro(a) { - t.fail(); - a.pass(); - } - - fooMacro.title = (title, firstArg) => `${firstArg}foo`; - - function barMacro(avaT) { - avaT.pass(); - } - - barMacro.title = (title, firstArg) => `${firstArg}bar`; - - function bazMacro(a) { - t.fail(); - a.pass(); - } - - bazMacro.title = (title, firstArg) => `${firstArg}baz`; - - return promiseEnd(new Runner({file: import.meta.url, match: ['foobar']}), runner => { - runner.on('stateChange', evt => { - if (evt.type === 'test-passed') { - t.equal(evt.title, 'foobar'); - } - }); - - runner.chain([fooMacro, barMacro, bazMacro], 'foo'); - runner.chain([fooMacro, barMacro, bazMacro], 'bar'); - }); -}); - test('silently skips other tests when .only is used', t => { t.plan(1); return promiseEnd(new Runner({file: import.meta.url}), runner => { diff --git a/test-tap/test-try-commit.js b/test-tap/test-try-commit.js index 152a9f9e2..36324e5a8 100644 --- a/test-tap/test-try-commit.js +++ b/test-tap/test-try-commit.js @@ -369,68 +369,6 @@ test('try-commit accepts macros', async t => { t.ok(result.passed); }); -test('try-commit accepts multiple macros', async t => { - const ava = newAva(); - const result = await ava(async a => { - const [result1, result2] = await a.try([ - b => { - t.equal(b.title, 'test ─ attempt 1'); - b.pass(); - }, - b => { - t.equal(b.title, 'test ─ attempt 2'); - b.fail(); - } - ]); - t.ok(result1.passed); - result1.commit(); - t.notOk(result2.passed); - result2.discard(); - - const [result3, result4] = await a.try([ - b => { - t.equal(b.title, 'test ─ attempt 3'); - b.pass(); - }, - b => { - t.equal(b.title, 'test ─ attempt 4'); - b.fail(); - } - ]); - t.ok(result3.passed); - result3.commit(); - t.notOk(result4.passed); - result4.discard(); - }).run(); - - t.ok(result.passed); -}); - -test('try-commit returns results in the same shape as when implementations are passed', async t => { - const ava = newAva(); - const result = await ava(async a => { - const [result1, result2, result3] = await Promise.all([ - a.try(b => b.pass()), - a.try([b => b.pass()]), - a.try([b => b.pass(), b => b.fail()]) - ]); - - t.match(result1, {passed: true}); - result1.commit(); - - t.equal(result2.length, 1); - t.match(result2, [{passed: true}]); - result2[0].commit(); - - t.equal(result3.length, 2); - t.match(result3, [{passed: true}, {passed: false}]); - result3[0].commit(); - result3[1].discard(); - }).run(); - - t.ok(result.passed); -}); - test('try-commit abides timeout', async t => { const ava = newAva(); const result1 = await ava(async a => { diff --git a/test/macros/fixtures/macros.js b/test/macros/fixtures/macros.js new file mode 100644 index 000000000..e51c2839e --- /dev/null +++ b/test/macros/fixtures/macros.js @@ -0,0 +1,18 @@ +/* eslint-disable ava/test-title, ava/no-unknown-modifiers, ava/use-t */ +import test from 'ava'; + +const withoutTitle = test.macro((t, arg) => { + t.is(arg, 'arg'); +}); +const withTitle = test.macro({ + exec(t, arg) { + t.is(arg, 'arg'); + }, + title(provided, arg) { + return `${provided || ''} ${arg}`; + } +}); + +test('without title', withoutTitle, 'arg'); +test('with title', withTitle, 'arg'); +test(withTitle, 'arg'); diff --git a/test/macros/fixtures/package.json b/test/macros/fixtures/package.json new file mode 100644 index 000000000..82d1f8494 --- /dev/null +++ b/test/macros/fixtures/package.json @@ -0,0 +1,8 @@ +{ + "type": "module", + "ava": { + "files": [ + "*" + ] + } +} diff --git a/test/macros/test.js b/test/macros/test.js new file mode 100644 index 000000000..9f48e5190 --- /dev/null +++ b/test/macros/test.js @@ -0,0 +1,8 @@ +import test from '@ava/test'; + +import {fixture} from '../helpers/exec.js'; + +test('a-okay', async t => { + const result = await fixture([]); + t.is(result.stats.passed.length, 3); +}); diff --git a/test/multiple-implementations/fixtures/package.json b/test/multiple-implementations/fixtures/package.json new file mode 100644 index 000000000..82d1f8494 --- /dev/null +++ b/test/multiple-implementations/fixtures/package.json @@ -0,0 +1,8 @@ +{ + "type": "module", + "ava": { + "files": [ + "*" + ] + } +} diff --git a/test/multiple-implementations/fixtures/test.js b/test/multiple-implementations/fixtures/test.js new file mode 100644 index 000000000..204b902dc --- /dev/null +++ b/test/multiple-implementations/fixtures/test.js @@ -0,0 +1,3 @@ +import test from 'ava'; + +test('title', []); diff --git a/test/multiple-implementations/fixtures/try.js b/test/multiple-implementations/fixtures/try.js new file mode 100644 index 000000000..4b5a1af89 --- /dev/null +++ b/test/multiple-implementations/fixtures/try.js @@ -0,0 +1,5 @@ +import test from 'ava'; + +test('try', async t => { + await t.try([]); +}); diff --git a/test/multiple-implementations/test.js b/test/multiple-implementations/test.js new file mode 100644 index 000000000..4cefc477b --- /dev/null +++ b/test/multiple-implementations/test.js @@ -0,0 +1,13 @@ +import test from '@ava/test'; + +import {fixture} from '../helpers/exec.js'; + +test('test()', async t => { + const result = await t.throwsAsync(fixture(['test.js'])); + t.regex(result.stdout, /AVA 4 no longer supports multiple implementations/); +}); + +test('t.try()', async t => { + const result = await t.throwsAsync(fixture(['try.js'])); + t.regex(result.stdout, /AVA 4 no longer supports t\.try\(\) with multiple implementations/); +}); diff --git a/types/assertions.d.ts b/types/assertions.d.ts new file mode 100644 index 000000000..005c0197c --- /dev/null +++ b/types/assertions.d.ts @@ -0,0 +1,327 @@ +export type ErrorConstructor = new (...args: any[]) => Error; + +/** Specify one or more expectations the thrown error must satisfy. */ +export type ThrowsExpectation = { + /** The thrown error must have a code that equals the given string or number. */ + code?: string | number; + + /** The thrown error must be an instance of this constructor. */ + instanceOf?: ErrorConstructor; + + /** The thrown error must be strictly equal to this value. */ + is?: Error; + + /** The thrown error must have a message that equals the given string, or matches the regular expression. */ + message?: string | RegExp; + + /** The thrown error must have a name that equals the given string. */ + name?: string; +}; + +export interface Assertions { + /** + * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean + * indicating whether the assertion passed. Comes with power-assert. + */ + assert: AssertAssertion; + + /** + * Assert that `actual` is [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to + * `expected`, returning a boolean indicating whether the assertion passed. + */ + deepEqual: DeepEqualAssertion; + + /** + * Assert that `value` is like `selector`, returning a boolean indicating whether the assertion passed. + */ + like: LikeAssertion; + + /** Fail the test, always returning `false`. */ + fail: FailAssertion; + + /** + * Assert that `actual` is strictly false, returning a boolean indicating whether the assertion passed. + */ + false: FalseAssertion; + + /** + * Assert that `actual` is [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy), returning a boolean + * indicating whether the assertion passed. + */ + falsy: FalsyAssertion; + + /** + * Assert that `actual` is [the same + * value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) as `expected`, + * returning a boolean indicating whether the assertion passed. + */ + is: IsAssertion; + + /** + * Assert that `actual` is not [the same + * value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) as `expected`, + * returning a boolean indicating whether the assertion passed. + */ + not: NotAssertion; + + /** + * Assert that `actual` is not [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to + * `expected`, returning a boolean indicating whether the assertion passed. + */ + notDeepEqual: NotDeepEqualAssertion; + + /** + * Assert that `string` does not match the regular expression, returning a boolean indicating whether the assertion + * passed. + */ + notRegex: NotRegexAssertion; + + /** Assert that the function does not throw. */ + notThrows: NotThrowsAssertion; + + /** Assert that the async function does not throw, or that the promise does not reject. Must be awaited. */ + notThrowsAsync: NotThrowsAsyncAssertion; + + /** Count a passing assertion, always returning `true`. */ + pass: PassAssertion; + + /** + * Assert that `string` matches the regular expression, returning a boolean indicating whether the assertion passed. + */ + regex: RegexAssertion; + + /** + * Assert that `expected` is [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to a + * previously recorded [snapshot](https://github.com/concordancejs/concordance#serialization-details), or if + * necessary record a new snapshot. + */ + snapshot: SnapshotAssertion; + + /** + * Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value. + */ + throws: ThrowsAssertion; + + /** + * Assert that the async function throws [an error](https://www.npmjs.com/package/is-error), or the promise rejects + * with one. If so, returns a promise for the error value, which must be awaited. + */ + throwsAsync: ThrowsAsyncAssertion; + + /** + * Assert that `actual` is strictly true, returning a boolean indicating whether the assertion passed. + */ + true: TrueAssertion; + + /** + * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean + * indicating whether the assertion passed. + */ + truthy: TruthyAssertion; +} + +export interface AssertAssertion { + /** + * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean + * indicating whether the assertion passed. Comes with power-assert. + */ + (actual: any, message?: string): boolean; + + /** Skip this assertion. */ + skip(actual: any, message?: string): void; +} + +export interface DeepEqualAssertion { + /** + * Assert that `actual` is [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to + * `expected`, returning a boolean indicating whether the assertion passed. + */ + (actual: Actual, expected: Expected, message?: string): actual is Expected; + + /** Skip this assertion. */ + skip(actual: any, expected: any, message?: string): void; +} + +export interface LikeAssertion { + /** + * Assert that `value` is like `selector`, returning a boolean indicating whether the assertion passed. + */ + >(value: any, selector: Expected, message?: string): value is Expected; + + /** Skip this assertion. */ + skip(value: any, selector: any, message?: string): void; +} + +export interface FailAssertion { + /** Fail the test, always returning `false`. */ + (message?: string): boolean; + + /** Skip this assertion. */ + skip(message?: string): void; +} + +export interface FalseAssertion { + /** + * Assert that `actual` is strictly false, returning a boolean indicating whether the assertion passed. + */ + (actual: any, message?: string): actual is false; + + /** Skip this assertion. */ + skip(actual: any, message?: string): void; +} + +export interface FalsyAssertion { + /** + * Assert that `actual` is [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy), returning a boolean + * indicating whether the assertion passed. + */ + (actual: any, message?: string): boolean; + + /** Skip this assertion. */ + skip(actual: any, message?: string): void; +} + +export interface IsAssertion { + /** + * Assert that `actual` is [the same + * value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) as `expected`, + * returning a boolean indicating whether the assertion passed. + */ + (actual: Actual, expected: Expected, message?: string): actual is Expected; + + /** Skip this assertion. */ + skip(actual: any, expected: any, message?: string): void; +} + +export interface NotAssertion { + /** + * Assert that `actual` is not [the same + * value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) as `expected`, + * returning a boolean indicating whether the assertion passed. + */ + (actual: Actual, expected: Expected, message?: string): boolean; + + /** Skip this assertion. */ + skip(actual: any, expected: any, message?: string): void; +} + +export interface NotDeepEqualAssertion { + /** + * Assert that `actual` is not [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to + * `expected`, returning a boolean indicating whether the assertion passed. + */ + (actual: Actual, expected: Expected, message?: string): boolean; + + /** Skip this assertion. */ + skip(actual: any, expected: any, message?: string): void; +} + +export interface NotRegexAssertion { + /** + * Assert that `string` does not match the regular expression, returning a boolean indicating whether the assertion + * passed. + */ + (string: string, regex: RegExp, message?: string): boolean; + + /** Skip this assertion. */ + skip(string: string, regex: RegExp, message?: string): void; +} + +export interface NotThrowsAssertion { + /** Assert that the function does not throw. */ + (fn: () => any, message?: string): void; + + /** Skip this assertion. */ + skip(fn: () => any, message?: string): void; +} + +export interface NotThrowsAsyncAssertion { + /** Assert that the async function does not throw. You must await the result. */ + (fn: () => PromiseLike, message?: string): Promise; + + /** Assert that the promise does not reject. You must await the result. */ + (promise: PromiseLike, message?: string): Promise; // eslint-disable-line @typescript-eslint/unified-signatures + + /** Skip this assertion. */ + skip(nonThrower: any, message?: string): void; +} + +export interface PassAssertion { + /** Count a passing assertion, always returning `true`. */ + (message?: string): boolean; + + /** Skip this assertion. */ + skip(message?: string): void; +} + +export interface RegexAssertion { + /** + * Assert that `string` matches the regular expression, returning a boolean indicating whether the assertion passed. + */ + (string: string, regex: RegExp, message?: string): boolean; + + /** Skip this assertion. */ + skip(string: string, regex: RegExp, message?: string): void; +} + +export interface SnapshotAssertion { + /** + * Assert that `expected` is [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to a + * previously recorded [snapshot](https://github.com/concordancejs/concordance#serialization-details), or if + * necessary record a new snapshot. + */ + (expected: any, message?: string): void; + + /** Skip this assertion. */ + skip(expected: any, message?: string): void; +} + +export interface ThrowsAssertion { + /** + * Assert that the function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error value. + * The error must satisfy all expectations. Returns null when the assertion fails. + */ + (fn: () => any, expectations?: ThrowsExpectation, message?: string): ThrownError | null; + + /** Skip this assertion. */ + skip(fn: () => any, expectations?: any, message?: string): void; +} + +export interface ThrowsAsyncAssertion { + /** + * Assert that the async function throws [an error](https://www.npmjs.com/package/is-error). If so, returns the error + * value. Returns null when the assertion fails. You must await the result. The error must satisfy all expectations. + */ + (fn: () => PromiseLike, expectations?: ThrowsExpectation, message?: string): Promise; + + /** + * Assert that the promise rejects with [an error](https://www.npmjs.com/package/is-error). If so, returns the + * rejection reason. Returns null when the assertion fails. You must await the result. The error must satisfy all + * expectations. + */ + (promise: PromiseLike, expectations?: ThrowsExpectation, message?: string): Promise; // eslint-disable-line @typescript-eslint/unified-signatures + + /** Skip this assertion. */ + skip(thrower: any, expectations?: any, message?: string): void; +} + +export interface TrueAssertion { + /** + * Assert that `actual` is strictly true, returning a boolean indicating whether the assertion passed. + */ + (actual: any, message?: string): actual is true; + + /** Skip this assertion. */ + skip(actual: any, message?: string): void; +} + +export interface TruthyAssertion { + /** + * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean + * indicating whether the assertion passed. + */ + (actual: any, message?: string): boolean; + + /** Skip this assertion. */ + skip(actual: any, message?: string): void; +} diff --git a/types/subscribable.ts b/types/subscribable.ts new file mode 100644 index 000000000..3a3399bca --- /dev/null +++ b/types/subscribable.ts @@ -0,0 +1,6 @@ +export interface Subscribable { + subscribe(observer: { + error(error: any): void; + complete(): void; + }): void; +} diff --git a/types/test-fn.d.ts b/types/test-fn.d.ts new file mode 100644 index 000000000..a876666df --- /dev/null +++ b/types/test-fn.d.ts @@ -0,0 +1,232 @@ +import type {Assertions} from './assertions'; +import type {Subscribable} from './subscribable'; +import type {TryFn} from './try-fn'; + +/** The `t` value passed to test & hook implementations. */ +export interface ExecutionContext extends Assertions { + /** Test context, shared with hooks. */ + context: Context; + + /** Title of the test or hook. */ + readonly title: string; + + /** Whether the test has passed. Only accurate in afterEach hooks. */ + readonly passed: boolean; + + readonly log: LogFn; + readonly plan: PlanFn; + readonly teardown: TeardownFn; + readonly timeout: TimeoutFn; + readonly try: TryFn; +} + +export interface LogFn { + /** Log one or more values. */ + (...values: any[]): void; + + /** Skip logging. */ + skip(...values: any[]): void; +} + +export interface PlanFn { + /** + * Plan how many assertion there are in the test. The test will fail if the actual assertion count doesn't match the + * number of planned assertions. See [assertion planning](https://github.com/avajs/ava#assertion-planning). + */ + (count: number): void; + + /** Don't plan assertions. */ + skip(count: number): void; +} + +/** + * Set a timeout for the test, in milliseconds. The test will fail if the timeout is exceeded. + * The timeout is reset each time an assertion is made. + */ +export type TimeoutFn = (ms: number, message?: string) => void; + +/** Declare a function to be run after the test has ended. */ +export type TeardownFn = (fn: () => void) => void; + +export type ImplementationFn = + ((t: ExecutionContext, ...args: Args) => PromiseLike) | + ((t: ExecutionContext, ...args: Args) => Subscribable) | + ((t: ExecutionContext, ...args: Args) => void); + +export type TitleFn = (providedTitle: string | undefined, ...args: Args) => string; + +/** A reusable test or hook implementation. */ +export type Macro = { + /** The function that is executed when the macro is used. */ + readonly exec: ImplementationFn; + + /** Generates a test title when this macro is used. */ + readonly title?: TitleFn; +}; + +/** A test or hook implementation. */ +export type Implementation = ImplementationFn | Macro; + +export interface TestFn { + after: AfterFn; + afterEach: AfterFn; + before: BeforeFn; + beforeEach: BeforeFn; + failing: FailingFn; + macro: MacroFn; + meta: Meta; + only: OnlyFn; + serial: SerialFn; + skip: SkipFn; + todo: TodoFn; + + /** Declare a concurrent test. Additional arguments are passed to the implementation or macro. */ + (title: string, implementation: Implementation, ...args: Args): void; + + /** + * Declare a concurrent test that uses a macro. Additional arguments are passed to the macro. + * The macro is responsible for generating a unique test title. + */ + (macro: Macro, ...args: Args): void; +} + +export interface AfterFn { + always: AlwaysInterface; + skip: HookSkipFn; + + /** + * Declare a hook that is run once, after all tests have passed. + * Additional arguments are passed to the implementation or macro. + */ + (title: string, implementation: Implementation, ...args: Args): void; + + /** + * Declare a hook that is run once, after all tests have passed. + * Additional arguments are passed to the implementation or macro. + */ + (implementation: Implementation, ...args: Args): void; + +} + +export interface AlwaysInterface { + skip: HookSkipFn; + + /** + * Declare a hook that is run once, after all tests are done. + * Additional arguments are passed to the implementation or macro. + */ + (title: string, implementation: Implementation, ...args: Args): void; + + /** + * Declare a hook that is run once, after all tests are done. + * Additional arguments are passed to the implementation or macro. + */ + (implementation: Implementation, ...args: Args): void; +} + +export interface BeforeFn { + skip: HookSkipFn; + + /** + * Declare a hook that is run once, before all tests. + * Additional arguments are passed to the implementation or macro. + */ + (title: string, implementation: Implementation, ...args: Args): void; + + /** + * Declare a hook that is run once, before all tests. + * Additional arguments are passed to the implementation or macro. + */ + (implementation: Implementation, ...args: Args): void; +} + +export interface FailingFn { + only: OnlyFn; + skip: SkipFn; + + /** + * Declare a concurrent test that is expected to fail. + * Additional arguments are passed to the implementation or macro. + */ + (title: string, implementation: Implementation, ...args: Args): void; + + /** + * Declare a concurrent test, using a macro, that is expected to fail. + * Additional arguments are passed to the macro. The macro is responsible for generating a unique test title. + */ + (macro: Macro, ...args: Args): void; +} + +export interface HookSkipFn { + /** Skip this hook. */ + (title: string, implementation: Implementation, ...args: Args): void; + + /** Skip this hook. */ + (implementation: Implementation, ...args: Args): void; +} + +export interface OnlyFn { + /** + * Declare a test. Only this test and others declared with `.only()` are run. + * Additional arguments are passed to the implementation or macro. + */ + (title: string, implementation: Implementation, ...args: Args): void; + + /** + * Declare a test that uses a macro. Only this test and others declared with `.only()` are run. + * Additional arguments are passed to the macro. The macro is responsible for generating a unique test title. + */ + (macro: Macro, ...args: Args): void; +} + +export interface SerialFn { + after: AfterFn; + afterEach: AfterFn; + before: BeforeFn; + beforeEach: BeforeFn; + failing: FailingFn; + only: OnlyFn; + skip: SkipFn; + todo: TodoFn; + + /** Declare a serial test. Additional arguments are passed to the implementation or macro. */ + (title: string, implementation: Implementation, ...args: Args): void; + + /** + * Declare a serial test that uses a macro. The macro is responsible for generating a unique test title. + */ + (macro: Macro, ...args: Args): void; +} + +export interface SkipFn { + /** Skip this test. */ + (title: string, implementation: Implementation, ...args: Args): void; + + /** Skip this test. */ + (macro: Macro, ...args: Args): void; +} + +/** Declare a test that should be implemented later. */ +export type TodoFn = (title: string) => void; + +export type MacroDeclarationOptions = { + /** The function that is executed when the macro is used. */ + exec: ImplementationFn; + + /** The function responsible for generating a unique title when the macro is used. */ + title: TitleFn; +}; + +export interface MacroFn { + /** Declare a reusable test implementation. */ + (/** The function that is executed when the macro is used. */ exec: ImplementationFn): Macro; + (declaration: MacroDeclarationOptions): Macro; // eslint-disable-line @typescript-eslint/unified-signatures +} + +export interface Meta { + /** Path to the test file being executed. */ + file: string; + + /** Directory where snapshots are stored. */ + snapshotDirectory: string; +} diff --git a/types/try-fn.d.ts b/types/try-fn.d.ts new file mode 100644 index 000000000..41a1664d5 --- /dev/null +++ b/types/try-fn.d.ts @@ -0,0 +1,58 @@ +import type {Implementation} from './test-fn'; + +export type CommitDiscardOptions = { + /** + * Whether the logs should be included in those of the parent test. + */ + retainLogs?: boolean; +}; + +export interface AssertionError extends Error {} + +export interface TryResult { + /** + * Title of the attempt, helping you tell attempts aparts. + */ + title: string; + + /** + * Indicates whether all assertions passed, or at least one failed. + */ + passed: boolean; + + /** + * Errors raised for each failed assertion. + */ + errors: AssertionError[]; + + /** + * Logs created during the attempt using `t.log()`. Contains formatted values. + */ + logs: string[]; + + /** + * Commit the attempt. Counts as one assertion for the plan count. If the + * attempt failed, calling this will also cause your test to fail. + */ + commit(options?: CommitDiscardOptions): void; + + /** + * Discard the attempt. + */ + discard(options?: CommitDiscardOptions): void; +} + +export interface TryFn { + /** + * Attempt to run some assertions. The result must be explicitly committed or discarded or else + * the test will fail. The title may help distinguish attempts from one another. + */ + (title: string, fn: Implementation, ...args: Args): Promise; + + /** + * Attempt to run some assertions. The result must be explicitly committed or discarded or else + * the test will fail. + */ + (fn: Implementation, ...args: Args): Promise; +} +