diff --git a/docs/recipes/typescript.md b/docs/recipes/typescript.md index 0642f8bef..2e703e212 100644 --- a/docs/recipes/typescript.md +++ b/docs/recipes/typescript.md @@ -58,15 +58,27 @@ test(async t => { ## Using [macros](../01-writing-tests.md#reusing-test-logic-through-macros) +Macros can receive additional arguments. AVA can infer these to ensure you're using the macro correctly: + +```ts +import test, {ExecutionContext} from 'ava'; + +const hasLength = (t: ExecutionContext, input: string, expected: number) => { + t.is(input.length, expected); +}; + +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: ```ts import test, {Macro} from 'ava'; -const macro: Macro = (t, input: string, expected: number) => { +const macro: Macro<[string, number]> = (t, input, expected) => { t.is(eval(input), expected); }; -macro.title = (providedTitle = '', input: string, expected: number) => `${providedTitle} ${input} = ${expected}`.trim(); +macro.title = (providedTitle = '', input, expected) => `${providedTitle} ${input} = ${expected}`.trim(); test(macro, '2 + 2', 4); test(macro, '2 * 3', 6); @@ -78,7 +90,7 @@ You'll need a different type if you're expecting your macro to be used with a ca ```ts import test, {CbMacro} from 'ava'; -const macro: CbMacro = t => { +const macro: CbMacro<[]> = t => { t.pass(); setTimeout(t.end, 100); }; @@ -123,7 +135,7 @@ interface Context { const test = anyTest as TestInterface; -const macro: Macro = (t, expected: string) => { +const macro: Macro<[string], Context> = (t, expected: string) => { t.is(t.context.foo, expected); }; diff --git a/index.d.ts b/index.d.ts index f6032f1e2..a17918142 100644 --- a/index.d.ts +++ b/index.d.ts @@ -383,34 +383,66 @@ export type Implementation = (t: ExecutionContext) => Imp export type CbImplementation = (t: CbExecutionContext) => ImplementationResult; /** A reusable test or hook implementation. */ -export interface Macro { - (t: ExecutionContext, ...args: Array): ImplementationResult; +export interface Macro { + (t: ExecutionContext, ...args: Args): ImplementationResult; /** * 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: Array) => string; + title?: (providedTitle: string | undefined, ...args: Args) => string; } +/** Alias for a single macro, or an array of macros. */ +export type OneOrMoreMacros = Macro | [Macro, ...Macro[]]; + /** A reusable test or hook implementation, for tests & hooks declared with the `.cb` modifier. */ -export interface CbMacro { - (t: CbExecutionContext, ...args: Array): ImplementationResult; - title?: (providedTitle: string | undefined, ...args: Array) => string; +export interface CbMacro { + (t: CbExecutionContext, ...args: Args): ImplementationResult; + title?: (providedTitle: string | undefined, ...args: Args) => string; } +/** Alias for a single macro, or an array of macros, used for tests & hooks declared with the `.cb` modifier. */ +export type OneOrMoreCbMacros = CbMacro | [CbMacro, ...CbMacro[]]; + +/** Infers the types of the additional arguments the macro implementations should be called with. */ +export type InferArgs = + OneOrMore extends Macro ? Args : + OneOrMore extends Macro[] ? Args : + OneOrMore extends CbMacro ? Args : + OneOrMore extends CbMacro[] ? Args : + never; + +export type TitleOrMacro = string | OneOrMoreMacros + +export type MacroOrFirstArg = + TitleOrMacro extends string ? OneOrMoreMacros : + TitleOrMacro extends OneOrMoreMacros ? InferArgs[0] : + never + +export type TitleOrCbMacro = string | OneOrMoreCbMacros + +export type CbMacroOrFirstArg = + TitleOrMacro extends string ? OneOrMoreCbMacros : + TitleOrMacro extends OneOrMoreCbMacros ? InferArgs[0] : + never + +export type RestArgs = + MacroOrFirstArg extends OneOrMoreMacros ? InferArgs : + MacroOrFirstArg extends OneOrMoreCbMacros ? InferArgs : + TitleOrMacro extends OneOrMoreMacros ? Tail> : + TitleOrMacro extends OneOrMoreCbMacros ? Tail> : + never + 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, macro: Macro | Macro[], ...args: Array): void; + , MoA extends MacroOrFirstArg>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs): void; - /** - * Declare a concurrent test that uses one or more macros. Additional arguments are passed to the macro. The macro - * is responsible for generating a unique test title. - */ - (macro: Macro | Macro[], ...args: Array): void; + /** Declare a concurrent test that uses one or more macros. The macro is responsible for generating a unique test title. */ + (macro: OneOrMoreMacros<[], Context>): void /** Declare a hook that is run once, after all tests have passed. */ after: AfterInterface; @@ -442,14 +474,14 @@ 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. Additional argumens are passed to the macro. */ - (macro: Macro | Macro[], ...args: Array): 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 argumens are passed to the macro. */ - (title: string, macro: Macro | Macro[], ...args: Array): void; + /** Declare a hook that is run once, after all tests have passed. Additional arguments are passed to the macro. */ + , MoA extends MacroOrFirstArg>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs): void; + + /** Declare a hook that is run once, after all tests have passed. */ + (macro: OneOrMoreMacros<[], Context>): void /** Declare a hook that is run once, after all tests are done. */ always: AlwaysInterface; @@ -464,14 +496,14 @@ 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. Additional argumens are passed to the macro. */ - (macro: Macro | Macro[], ...args: Array): 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 argumens are passed to the macro. */ - (title: string, macro: Macro | Macro[], ...args: Array): void; + /** Declare a hook that is run once, after all tests are done. Additional arguments are passed to the macro. */ + , MoA extends MacroOrFirstArg>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs): void; + + /** Declare a hook that is run once, after all tests are done. */ + (macro: OneOrMoreMacros<[], Context>): void /** Declare a hook that must call `t.end()` when it's done. */ cb: HookCbInterface; @@ -483,14 +515,14 @@ 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. Additional argumens are passed to the macro. */ - (macro: Macro | Macro[], ...args: Array): 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 argumens are passed to the macro. */ - (title: string, macro: Macro | Macro[], ...args: Array): void; + /** Declare a hook that is run once, before all tests. Additional arguments are passed to the macro. */ + , MoA extends MacroOrFirstArg>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs): void; + + /** Declare a hook that is run once, before all tests. */ + (macro: OneOrMoreMacros<[], Context>): void /** Declare a hook that must call `t.end()` when it's done. */ cb: HookCbInterface; @@ -503,16 +535,16 @@ export interface CbInterface { (title: string, implementation: CbImplementation): void; /** - * Declare a test that uses one or more macros. The macros must call `t.end()` when they're done. + * Declare a concurrent test that uses one or more macros. The macros must call `t.end()` when they're done. * Additional arguments are passed to the macro. */ - (title: string, macro: CbMacro | CbMacro[], ...args: Array): void; + , MoA extends CbMacroOrFirstArg>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs): void; /** - * Declare a test that uses one or more macros. The macros must call `t.end()` when they're done. - * Additional arguments are passed to the macro. The macro is responsible for generating a unique test title. + * Declare a concurrent test that uses one or more macros. The macros must call `t.end()` when they're done. + * The macro is responsible for generating a unique test title. */ - (macro: CbMacro | CbMacro[], ...args: Array): void; + (macro: OneOrMoreCbMacros<[], Context>): void /** Declare a test that is expected to fail. */ failing: CbFailingInterface; @@ -529,14 +561,13 @@ export interface CbFailingInterface { * Declare a test that uses one or more macros. The macros must call `t.end()` when they're done. * Additional arguments are passed to the macro. The test is expected to fail. */ - (title: string, macro: CbMacro | CbMacro[], ...args: Array): void; + , MoA extends CbMacroOrFirstArg>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs): void; /** * Declare a test that uses one or more macros. The macros must call `t.end()` when they're done. - * Additional arguments are passed to the macro. The macro is responsible for generating a unique test title. * The test is expected to fail. */ - (macro: CbMacro | CbMacro[], ...args: Array): void; + (macro: OneOrMoreCbMacros<[], Context>): void only: CbOnlyInterface; skip: CbSkipInterface; @@ -552,14 +583,13 @@ export interface CbOnlyInterface { * Declare a test that uses one or more macros. The macros must call `t.end()` when they're done. * Additional arguments are passed to the macro. Only this test and others declared with `.only()` are run. */ - (title: string, macro: CbMacro | CbMacro[], ...args: Array): void; + , MoA extends CbMacroOrFirstArg>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs): void; /** * Declare a test that uses one or more macros. The macros must call `t.end()` when they're done. - * Additional arguments are passed to the macro. The macro is responsible for generating a unique test title. - * Only this test and others declared with `.only()` are run. + * Additional arguments are passed to the macro. Only this test and others declared with `.only()` are run. */ - (macro: CbMacro | CbMacro[], ...args: Array): void; + (macro: OneOrMoreCbMacros<[], Context>): void } export interface CbSkipInterface { @@ -567,10 +597,10 @@ export interface CbSkipInterface { (title: string, implementation: CbImplementation): void; /** Skip this test. */ - (title: string, macro: CbMacro | CbMacro[], ...args: Array): void; + , MoA extends CbMacroOrFirstArg>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs): void; /** Skip this test. */ - (macro: CbMacro | CbMacro[], ...args: Array): void; + (macro: OneOrMoreCbMacros<[], Context>): void } export interface FailingInterface { @@ -581,13 +611,13 @@ export interface FailingInterface { * 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, macro: Macro | Macro[], ...args: Array): void; + , MoA extends MacroOrFirstArg>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs): void; /** - * Declare a concurrent test that uses one or more macros. Additional arguments are passed to the macro. The macro - * is responsible for generating a unique test title. The test is expected to fail. + * 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. */ - (macro: Macro | Macro[], ...args: Array): void; + (macro: OneOrMoreMacros<[], Context>): void only: OnlyInterface; skip: SkipInterface; @@ -597,20 +627,19 @@ export interface HookCbInterface { /** Declare a hook that must call `t.end()` when it's done. */ (implementation: CbImplementation): void; + /** Declare a hook that must call `t.end()` when it's done. */ + (title: string, implementation: CbImplementation): void; + /** * Declare a hook that uses one or more macros. The macros must call `t.end()` when they're done. * Additional arguments are passed to the macro. */ - (macro: CbMacro | CbMacro[], ...args: Array): void; - - /** Declare a hook that must call `t.end()` when it's done. */ - (title: string, implementation: CbImplementation): void; + , MoA extends CbMacroOrFirstArg>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs): void; /** * Declare a hook that uses one or more macros. The macros must call `t.end()` when they're done. - * Additional arguments are passed to the macro. */ - (title: string, macro: CbMacro | CbMacro[], ...args: Array): void; + (macro: OneOrMoreCbMacros<[], Context>): void skip: HookCbSkipInterface; } @@ -620,13 +649,13 @@ export interface HookCbSkipInterface { (implementation: CbImplementation): void; /** Skip this hook. */ - (macro: CbMacro | CbMacro[], ...args: Array): void; + (title: string, implementation: CbImplementation): void; /** Skip this hook. */ - (title: string, implementation: CbImplementation): void; + , MoA extends CbMacroOrFirstArg>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs): void; /** Skip this hook. */ - (title: string, macro: CbMacro | CbMacro[], ...args: Array): void; + (macro: OneOrMoreCbMacros<[], Context>): void } export interface HookSkipInterface { @@ -634,13 +663,13 @@ export interface HookSkipInterface { (implementation: Implementation): void; /** Skip this hook. */ - (macro: Macro | Macro[], ...args: Array): void; + (title: string, implementation: Implementation): void; /** Skip this hook. */ - (title: string, implementation: Implementation): void; + , MoA extends MacroOrFirstArg>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs): void; /** Skip this hook. */ - (title: string, macro: Macro | Macro[], ...args: Array): void; + (macro: OneOrMoreMacros<[], Context>): void } export interface OnlyInterface { @@ -651,13 +680,13 @@ export interface OnlyInterface { * 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, macro: Macro | Macro[], ...args: Array): void; + , MoA extends MacroOrFirstArg>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs): void; /** - * Declare a test that uses one or more macros. Additional arguments are passed to the macro. The macro - * is responsible for generating a unique test title. Only this test and others declared with `.only()` are run. + * 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. */ - (macro: Macro | Macro[], ...args: Array): void; + (macro: OneOrMoreMacros<[], Context>): void } export interface SerialInterface { @@ -665,13 +694,12 @@ export interface SerialInterface { (title: string, implementation: Implementation): void; /** Declare a serial test that uses one or more macros. Additional arguments are passed to the macro. */ - (title: string, macro: Macro | Macro[], ...args: Array): void; + , MoA extends MacroOrFirstArg>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs): void; /** - * Declare a serial test that uses one or more macros. Additional arguments are passed to the macro. The macro - * is responsible for generating a unique test title. + * Declare a serial test that uses one or more macros. The macro is responsible for generating a unique test title. */ - (macro: Macro | Macro[], ...args: Array): void; + (macro: OneOrMoreMacros<[], Context>): void /** Declare a serial hook that is run once, after all tests have passed. */ after: AfterInterface; @@ -701,10 +729,10 @@ export interface SkipInterface { (title: string, implementation: Implementation): void; /** Skip this test. */ - (title: string, macro: Macro | Macro[], ...args: Array): void; + , MoA extends MacroOrFirstArg>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs): void; /** Skip this test. */ - (macro: Macro | Macro[], ...args: Array): void; + (macro: OneOrMoreMacros<[], Context>): void } export interface TodoDeclaration { @@ -747,3 +775,34 @@ export const skip: SkipInterface; /** Declare a test that should be implemented later. */ export const todo: TodoDeclaration; + + +/* +Tail type from . + +MIT License + +Copyright (c) 2017 Thomas Crockett + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** Get all but the first element of a tuple. */ +export type Tail = + ((...args: T) => any) extends ((head: any, ...tail: infer R) => any) ? R : never; diff --git a/test/ts-types/context.ts b/test/ts-types/context.ts index 61b568638..ff4962b94 100644 --- a/test/ts-types/context.ts +++ b/test/ts-types/context.ts @@ -6,7 +6,7 @@ interface Context { const test = anyTest as TestInterface; -const macro: Macro = (t, expected: string) => { +const macro: Macro<[string], Context> = (t, expected) => { t.is(t.context.foo, expected); }; diff --git a/test/ts-types/macros.ts b/test/ts-types/macros.ts new file mode 100644 index 000000000..89d19e07d --- /dev/null +++ b/test/ts-types/macros.ts @@ -0,0 +1,64 @@ +import test, {ExecutionContext, Macro} from '../..'; + +// Explicitly type as a macro. +{ + const hasLength: Macro<[string, number]> = (t, input, expected) => { + t.is(input.length, 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) => { + t.is(input.length, expected); + }; + + test('bar has length 3', hasLength, 'bar', 3); + test('bar has length 3', [hasLength], 'bar', 3); +} + +// Multiple macros +{ + const hasLength = (t: ExecutionContext, input: string, expected: number) => { + t.is(input.length, expected); + }; + const hasCodePoints = (t: ExecutionContext, input: string, expected: number) => { + t.is(Array.from(input).length, expected); + }; + + test('bar has length 3', [hasLength, hasCodePoints], 'bar', 3); +} + +// No title +{ + const hasLength: Macro<[string, number]> = (t, input, expected) => { + t.is(input.length, expected); + }; + const hasCodePoints: Macro<[string, number]> = (t, input, expected) => { + t.is(Array.from(input).length, expected); + }; + + test(hasLength, 'bar', 3); + test([hasLength, hasCodePoints], 'bar', 3); +} + +// No arguments +{ + const pass: Macro<[]> = t => t.pass() + pass.title = () => 'pass' + test(pass) +} + +// Inline +{ + test('has length 3', (t: ExecutionContext, input: string, expected: number) => { + t.is(input.length, expected) + }, 'bar', 3) + + test((t: ExecutionContext, input: string, expected: number) => { + t.is(input.length, expected) + }, 'bar', 3) +}