Type additional arguments used by macros

Co-authored-by: Nikita Volodin [Никита Володин] <volodin.n@gmail.com>
This commit is contained in:
Mark Wubben 2018-11-11 15:08:32 +00:00 committed by GitHub
parent a82fee56ff
commit 6f54db8419
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 210 additions and 75 deletions

View file

@ -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<Context>;
const macro: Macro<Context> = (t, expected: string) => {
const macro: Macro<[string], Context> = (t, expected: string) => {
t.is(t.context.foo, expected);
};

199
index.d.ts vendored
View file

@ -383,34 +383,66 @@ export type Implementation<Context = {}> = (t: ExecutionContext<Context>) => Imp
export type CbImplementation<Context = {}> = (t: CbExecutionContext<Context>) => ImplementationResult;
/** A reusable test or hook implementation. */
export interface Macro<Context = {}> {
(t: ExecutionContext<Context>, ...args: Array<any>): ImplementationResult;
export interface Macro<Args extends any[], Context = {}> {
(t: ExecutionContext<Context>, ...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<any>) => string;
title?: (providedTitle: string | undefined, ...args: Args) => string;
}
/** Alias for a single macro, or an array of macros. */
export type OneOrMoreMacros<Args extends any[], Context> = Macro<Args, Context> | [Macro<Args, Context>, ...Macro<Args, Context>[]];
/** A reusable test or hook implementation, for tests & hooks declared with the `.cb` modifier. */
export interface CbMacro<Context = {}> {
(t: CbExecutionContext<Context>, ...args: Array<any>): ImplementationResult;
title?: (providedTitle: string | undefined, ...args: Array<any>) => string;
export interface CbMacro<Args extends any[], Context = {}> {
(t: CbExecutionContext<Context>, ...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<Args extends any[], Context> = CbMacro<Args, Context> | [CbMacro<Args, Context>, ...CbMacro<Args, Context>[]];
/** Infers the types of the additional arguments the macro implementations should be called with. */
export type InferArgs<OneOrMore> =
OneOrMore extends Macro<infer Args, any> ? Args :
OneOrMore extends Macro<infer Args, any>[] ? Args :
OneOrMore extends CbMacro<infer Args, any> ? Args :
OneOrMore extends CbMacro<infer Args, any>[] ? Args :
never;
export type TitleOrMacro<Context> = string | OneOrMoreMacros<any, Context>
export type MacroOrFirstArg<TitleOrMacro, Context> =
TitleOrMacro extends string ? OneOrMoreMacros<any, Context> :
TitleOrMacro extends OneOrMoreMacros<any, Context> ? InferArgs<TitleOrMacro>[0] :
never
export type TitleOrCbMacro<Context> = string | OneOrMoreCbMacros<any, Context>
export type CbMacroOrFirstArg<TitleOrMacro, Context> =
TitleOrMacro extends string ? OneOrMoreCbMacros<any, Context> :
TitleOrMacro extends OneOrMoreCbMacros<any, Context> ? InferArgs<TitleOrMacro>[0] :
never
export type RestArgs<TitleOrMacro, MacroOrFirstArg, Context> =
MacroOrFirstArg extends OneOrMoreMacros<any, Context> ? InferArgs<MacroOrFirstArg> :
MacroOrFirstArg extends OneOrMoreCbMacros<any, Context> ? InferArgs<MacroOrFirstArg> :
TitleOrMacro extends OneOrMoreMacros<any, Context> ? Tail<InferArgs<TitleOrMacro>> :
TitleOrMacro extends OneOrMoreCbMacros<any, Context> ? Tail<InferArgs<TitleOrMacro>> :
never
export interface TestInterface<Context = {}> {
/** Declare a concurrent test. */
(title: string, implementation: Implementation<Context>): void;
/** Declare a concurrent test that uses one or more macros. Additional arguments are passed to the macro. */
(title: string, macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
<ToM extends TitleOrMacro<Context>, MoA extends MacroOrFirstArg<ToM, Context>>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs<ToM, MoA, Context>): 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<Context> | Macro<Context>[], ...args: Array<any>): 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<Context>;
@ -442,14 +474,14 @@ export interface AfterInterface<Context = {}> {
/** Declare a hook that is run once, after all tests have passed. */
(implementation: Implementation<Context>): void;
/** Declare a hook that is run once, after all tests have passed. Additional argumens are passed to the macro. */
(macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
/** Declare a hook that is run once, after all tests have passed. */
(title: string, implementation: Implementation<Context>): void;
/** Declare a hook that is run once, after all tests have passed. Additional argumens are passed to the macro. */
(title: string, macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
/** Declare a hook that is run once, after all tests have passed. Additional arguments are passed to the macro. */
<ToM extends TitleOrMacro<Context>, MoA extends MacroOrFirstArg<ToM, Context>>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs<ToM, MoA, Context>): 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<Context>;
@ -464,14 +496,14 @@ export interface AlwaysInterface<Context = {}> {
/** Declare a hook that is run once, after all tests are done. */
(implementation: Implementation<Context>): void;
/** Declare a hook that is run once, after all tests are done. Additional argumens are passed to the macro. */
(macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
/** Declare a hook that is run once, after all tests are done. */
(title: string, implementation: Implementation<Context>): void;
/** Declare a hook that is run once, after all tests are done. Additional argumens are passed to the macro. */
(title: string, macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
/** Declare a hook that is run once, after all tests are done. Additional arguments are passed to the macro. */
<ToM extends TitleOrMacro<Context>, MoA extends MacroOrFirstArg<ToM, Context>>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs<ToM, MoA, Context>): 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<Context>;
@ -483,14 +515,14 @@ export interface BeforeInterface<Context = {}> {
/** Declare a hook that is run once, before all tests. */
(implementation: Implementation<Context>): void;
/** Declare a hook that is run once, before all tests. Additional argumens are passed to the macro. */
(macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
/** Declare a hook that is run once, before all tests. */
(title: string, implementation: Implementation<Context>): void;
/** Declare a hook that is run once, before all tests. Additional argumens are passed to the macro. */
(title: string, macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
/** Declare a hook that is run once, before all tests. Additional arguments are passed to the macro. */
<ToM extends TitleOrMacro<Context>, MoA extends MacroOrFirstArg<ToM, Context>>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs<ToM, MoA, Context>): 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<Context>;
@ -503,16 +535,16 @@ export interface CbInterface<Context = {}> {
(title: string, implementation: CbImplementation<Context>): 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<Context> | CbMacro<Context>[], ...args: Array<any>): void;
<ToM extends TitleOrCbMacro<Context>, MoA extends CbMacroOrFirstArg<ToM, Context>>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs<ToM, MoA, Context>): 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<Context> | CbMacro<Context>[], ...args: Array<any>): void;
(macro: OneOrMoreCbMacros<[], Context>): void
/** Declare a test that is expected to fail. */
failing: CbFailingInterface<Context>;
@ -529,14 +561,13 @@ export interface CbFailingInterface<Context = {}> {
* 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<Context> | CbMacro<Context>[], ...args: Array<any>): void;
<ToM extends TitleOrCbMacro<Context>, MoA extends CbMacroOrFirstArg<ToM, Context>>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs<ToM, MoA, Context>): 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<Context> | CbMacro<Context>[], ...args: Array<any>): void;
(macro: OneOrMoreCbMacros<[], Context>): void
only: CbOnlyInterface<Context>;
skip: CbSkipInterface<Context>;
@ -552,14 +583,13 @@ export interface CbOnlyInterface<Context = {}> {
* 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<Context> | CbMacro<Context>[], ...args: Array<any>): void;
<ToM extends TitleOrCbMacro<Context>, MoA extends CbMacroOrFirstArg<ToM, Context>>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs<ToM, MoA, Context>): 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<Context> | CbMacro<Context>[], ...args: Array<any>): void;
(macro: OneOrMoreCbMacros<[], Context>): void
}
export interface CbSkipInterface<Context = {}> {
@ -567,10 +597,10 @@ export interface CbSkipInterface<Context = {}> {
(title: string, implementation: CbImplementation<Context>): void;
/** Skip this test. */
(title: string, macro: CbMacro<Context> | CbMacro<Context>[], ...args: Array<any>): void;
<ToM extends TitleOrCbMacro<Context>, MoA extends CbMacroOrFirstArg<ToM, Context>>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs<ToM, MoA, Context>): void;
/** Skip this test. */
(macro: CbMacro<Context> | CbMacro<Context>[], ...args: Array<any>): void;
(macro: OneOrMoreCbMacros<[], Context>): void
}
export interface FailingInterface<Context = {}> {
@ -581,13 +611,13 @@ export interface FailingInterface<Context = {}> {
* 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<Context> | Macro<Context>[], ...args: Array<any>): void;
<ToM extends TitleOrMacro<Context>, MoA extends MacroOrFirstArg<ToM, Context>>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs<ToM, MoA, Context>): 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<Context> | Macro<Context>[], ...args: Array<any>): void;
(macro: OneOrMoreMacros<[], Context>): void
only: OnlyInterface<Context>;
skip: SkipInterface<Context>;
@ -597,12 +627,6 @@ export interface HookCbInterface<Context = {}> {
/** Declare a hook that must call `t.end()` when it's done. */
(implementation: CbImplementation<Context>): 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<Context> | CbMacro<Context>[], ...args: Array<any>): void;
/** Declare a hook that must call `t.end()` when it's done. */
(title: string, implementation: CbImplementation<Context>): void;
@ -610,7 +634,12 @@ export interface HookCbInterface<Context = {}> {
* 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<Context> | CbMacro<Context>[], ...args: Array<any>): void;
<ToM extends TitleOrCbMacro<Context>, MoA extends CbMacroOrFirstArg<ToM, Context>>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs<ToM, MoA, Context>): void;
/**
* Declare a hook that uses one or more macros. The macros must call `t.end()` when they're done.
*/
(macro: OneOrMoreCbMacros<[], Context>): void
skip: HookCbSkipInterface<Context>;
}
@ -619,28 +648,28 @@ export interface HookCbSkipInterface<Context = {}> {
/** Skip this hook. */
(implementation: CbImplementation<Context>): void;
/** Skip this hook. */
(macro: CbMacro<Context> | CbMacro<Context>[], ...args: Array<any>): void;
/** Skip this hook. */
(title: string, implementation: CbImplementation<Context>): void;
/** Skip this hook. */
(title: string, macro: CbMacro<Context> | CbMacro<Context>[], ...args: Array<any>): void;
<ToM extends TitleOrCbMacro<Context>, MoA extends CbMacroOrFirstArg<ToM, Context>>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs<ToM, MoA, Context>): void;
/** Skip this hook. */
(macro: OneOrMoreCbMacros<[], Context>): void
}
export interface HookSkipInterface<Context = {}> {
/** Skip this hook. */
(implementation: Implementation<Context>): void;
/** Skip this hook. */
(macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
/** Skip this hook. */
(title: string, implementation: Implementation<Context>): void;
/** Skip this hook. */
(title: string, macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
<ToM extends TitleOrMacro<Context>, MoA extends MacroOrFirstArg<ToM, Context>>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs<ToM, MoA, Context>): void;
/** Skip this hook. */
(macro: OneOrMoreMacros<[], Context>): void
}
export interface OnlyInterface<Context = {}> {
@ -651,13 +680,13 @@ export interface OnlyInterface<Context = {}> {
* 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<Context> | Macro<Context>[], ...args: Array<any>): void;
<ToM extends TitleOrMacro<Context>, MoA extends MacroOrFirstArg<ToM, Context>>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs<ToM, MoA, Context>): 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<Context> | Macro<Context>[], ...args: Array<any>): void;
(macro: OneOrMoreMacros<[], Context>): void
}
export interface SerialInterface<Context = {}> {
@ -665,13 +694,12 @@ export interface SerialInterface<Context = {}> {
(title: string, implementation: Implementation<Context>): void;
/** Declare a serial test that uses one or more macros. Additional arguments are passed to the macro. */
(title: string, macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
<ToM extends TitleOrMacro<Context>, MoA extends MacroOrFirstArg<ToM, Context>>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs<ToM, MoA, Context>): 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<Context> | Macro<Context>[], ...args: Array<any>): void;
(macro: OneOrMoreMacros<[], Context>): void
/** Declare a serial hook that is run once, after all tests have passed. */
after: AfterInterface<Context>;
@ -701,10 +729,10 @@ export interface SkipInterface<Context = {}> {
(title: string, implementation: Implementation<Context>): void;
/** Skip this test. */
(title: string, macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
<ToM extends TitleOrMacro<Context>, MoA extends MacroOrFirstArg<ToM, Context>>(titleOrMacro: ToM, macroOrArg: MoA, ...rest: RestArgs<ToM, MoA, Context>): void;
/** Skip this test. */
(macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): 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 <https://github.com/tycho01/typical/blob/25f11ed92c960aab1ebbf47fd6ead9e0ae51d947/src/array/Tail.ts>.
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<T extends any[]> =
((...args: T) => any) extends ((head: any, ...tail: infer R) => any) ? R : never;

View file

@ -6,7 +6,7 @@ interface Context {
const test = anyTest as TestInterface<Context>;
const macro: Macro<Context> = (t, expected: string) => {
const macro: Macro<[string], Context> = (t, expected) => {
t.is(t.context.foo, expected);
};

64
test/ts-types/macros.ts Normal file
View file

@ -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)
}