diff --git a/experimental.d.ts b/experimental.d.ts new file mode 100644 index 000000000..7f0e98d08 --- /dev/null +++ b/experimental.d.ts @@ -0,0 +1,366 @@ +export { + AssertAssertion, + AssertionError, + Assertions, + CommitDiscardOptions, + Constructor, + DeepEqualAssertion, + FailAssertion, + FalseAssertion, + FalsyAssertion, + ImplementationResult, + IsAssertion, + LogFn, + MetaInterface, + NotAssertion, + NotDeepEqualAssertion, + NotRegexAssertion, + NotThrowsAssertion, + NotThrowsAsyncAssertion, + PassAssertion, + PlanFn, + RegexAssertion, + SnapshotAssertion, + SnapshotOptions, + Subscribable, + ThrowsAssertion, + ThrowsAsyncAssertion, + ThrowsExpectation, + TimeoutFn, + TrueAssertion, + TruthyAssertion, + TryResult +} from '.'; + +import { + Assertions, + ImplementationResult, + MetaInterface, + LogFn, + PlanFn, + TimeoutFn, + TryResult +} from '.'; + +export type ExecutionContext = Assertions & ExtraAssertions & { + /** 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; + timeout: TimeoutFn; + try: TryFn; +}; + +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, implementation: Implementation): Promise; + + /** + * 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, implementation: ImplementationWithArgs, ...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, macro: Macro<[], Context, ExtraAssertions>): 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. + */ + (title: string, macro: Macro, ...args: Args): Promise; + + /** + * Attempt to run some assertions. The result must be explicitly committed or discarded or else + * the test will fail. + */ + (implementation: Implementation): Promise; + + /** + * Attempt to run some assertions. The result must be explicitly committed or discarded or else + * the test will fail. + */ + (implementation: ImplementationWithArgs, ...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. + */ + (macro: Macro<[], Context, ExtraAssertions>): 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. + */ + (macro: Macro, ...args: Args): Promise; +} + +export type Implementation = (t: ExecutionContext) => ImplementationResult; +export type ImplementationWithArgs = (t: ExecutionContext, ...args: Args) => ImplementationResult; + +export type Macro = { + exec (t: ExecutionContext, ...args: Args): ImplementationResult; + title? (providedTitle?: string, ...args: Args): string; +}; + +export interface MacroInterface { + (implementation: ImplementationWithArgs): Macro; + (macro: Macro): Macro; +} + +export interface TestInterface { + /** Declare a concurrent test. */ + (title: string, implementation: Implementation): void; + + /** Declare a concurrent test. */ + (title: string, implementation: ImplementationWithArgs, ...args: Args): void; + + /** Declare a concurrent test. */ + (title: string, macro: Macro<[], Context, ExtraAssertions>): void; + + /** Declare a concurrent test. */ + (title: string, macro: Macro, ...args: Args): void; + + /** Declare a concurrent test. */ + (macro: Macro<[], Context, ExtraAssertions>): void; + + /** Declare a concurrent test. */ + (macro: Macro, ...args: Args): 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; + + /** Create a macro you can reuse in multiple tests. */ + macro: MacroInterface; + + /** 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. */ + (implementation: ImplementationWithArgs, ...args: Args): 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. */ + (title: string, implementation: ImplementationWithArgs, ...args: Args): 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. */ + (implementation: ImplementationWithArgs, ...args: Args): 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. */ + (title: string, implementation: ImplementationWithArgs, ...args: Args): 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. */ + (implementation: ImplementationWithArgs, ...args: Args): 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. */ + (title: string, implementation: ImplementationWithArgs, ...args: Args): void; + + skip: HookSkipInterface; +} + +export interface FailingInterface { + /** Declare a test that is is expected to fail. */ + (title: string, implementation: Implementation): void; + + /** Declare a test that is is expected to fail. */ + (title: string, implementation: ImplementationWithArgs, ...args: Args): void; + + /** Declare a test that is is expected to fail. */ + (title: string, macro: Macro<[], Context, ExtraAssertions>): void; + + /** Declare a test that is is expected to fail. */ + (title: string, macro: Macro, ...args: Args): void; + + /** Declare a test that is is expected to fail. */ + (macro: Macro<[], Context, ExtraAssertions>): void; + + /** Declare a test that is is expected to fail. */ + (macro: Macro, ...args: Args): void; + + only: OnlyInterface; + skip: SkipInterface; +} + +export interface HookSkipInterface { + /** Skip this hook. */ + (implementation: Implementation): void; + + /** Skip this hook. */ + (implementation: ImplementationWithArgs, ...args: Args): void; + + /** Skip this hook. */ + (title: string, implementation: Implementation): void; + + /** Skip this hook. */ + (title: string, implementation: ImplementationWithArgs, ...args: Args): 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. Only this test and others declared with `.only()` are run. */ + (title: string, implementation: ImplementationWithArgs, ...args: Args): void; + + /** Declare a test. Only this test and others declared with `.only()` are run. */ + (title: string, macro: Macro<[], Context, ExtraAssertions>): void; + + /** Declare a test. Only this test and others declared with `.only()` are run. */ + (title: string, macro: Macro, ...args: Args): void; + + /** Declare a test. Only this test and others declared with `.only()` are run. */ + (macro: Macro<[], Context, ExtraAssertions>): void; + + /** Declare a test. Only this test and others declared with `.only()` are run. */ + (macro: Macro, ...args: Args): void; +} + +export interface SerialInterface { + /** Declare a serial test. */ + (title: string, implementation: Implementation): void; + + /** Declare a serial test. */ + (title: string, implementation: ImplementationWithArgs, ...args: Args): void; + + /** Declare a serial test. */ + (title: string, macro: Macro<[], Context, ExtraAssertions>): void; + + /** Declare a serial test. */ + (title: string, macro: Macro, ...args: Args): void; + + /** Declare a serial test. */ + (macro: Macro<[], Context, ExtraAssertions>): void; + + /** Declare a serial test. */ + (macro: Macro, ...args: Args): 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; + + /** Create a macro you can reuse in multiple tests. */ + macro: MacroInterface; + + /** 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, implementation: ImplementationWithArgs, ...args: Args): void; + + /** Skip this test. */ + (title: string, macro: Macro<[], Context, ExtraAssertions>): void; + + /** Skip this test. */ + (title: string, macro: Macro, ...args: Args): void; + + /** Skip this test. */ + (macro: Macro<[], Context, ExtraAssertions>): void; + + /** Skip this test. */ + (macro: Macro, ...args: Args): void; +} + +export interface TodoDeclaration { + /** Declare a test that should be implemented later. */ + (title: string): void; +} + +export interface ForkableSerialInterface extends SerialInterface { + /** Create a new serial() function with its own hooks. */ + fork(): ForkableSerialInterface; +} + +export interface ForkableTestInterface extends TestInterface { + /** Create a new test() function with its own hooks. */ + fork(): ForkableTestInterface; + + /** Declare tests and hooks that are run serially. */ + serial: ForkableSerialInterface; +} + +/** Call to declare a test, or chain to declare hooks or test modifiers */ +declare const test: TestInterface & { + /** Create a new test() function with its own hooks. */ + make(): ForkableTestInterface; +}; + +/** Call to declare a test, or chain to declare hooks or test modifiers */ +export default test; diff --git a/experimental.js b/experimental.js new file mode 100644 index 000000000..8d333aca8 --- /dev/null +++ b/experimental.js @@ -0,0 +1,9 @@ +'use strict'; +const path = require('path'); + +// Ensure the same AVA install is loaded by the test file as by the test worker +if (process.env.AVA_PATH && process.env.AVA_PATH !== __dirname) { + module.exports = require(path.join(process.env.AVA_PATH, 'experimental.js')); +} else { + module.exports = require('./lib/worker/main').experimental(); +} diff --git a/index.js b/index.js index 24f28865b..9dfe6f3e3 100644 --- a/index.js +++ b/index.js @@ -4,5 +4,5 @@ if (process.env.AVA_PATH && process.env.AVA_PATH !== __dirname) { module.exports = require(process.env.AVA_PATH); } else { - module.exports = require('./lib/worker/main'); + module.exports = require('./lib/worker/main').ava3(); } diff --git a/lib/create-chain.js b/lib/create-chain.js index ce52b1876..8fda2b7cc 100644 --- a/lib/create-chain.js +++ b/lib/create-chain.js @@ -1,113 +1,167 @@ 'use strict'; const chainRegistry = new WeakMap(); -function startChain(name, call, defaults) { +function startChain(name, {annotations, declare, type}) { const fn = (...args) => { - call({...defaults}, args); + declare(type, annotations, args); }; Object.defineProperty(fn, 'name', {value: name}); - chainRegistry.set(fn, {call, defaults, fullName: name}); + chainRegistry.set(fn, { + declare(flags, args) { + declare(type, {...annotations, ...flags}, args); + }, + fullName: name + }); return fn; } -function extendChain(previous, name, flag) { - if (!flag) { - flag = name; - } - +function extendChain(previous, name, flag = name) { const fn = (...args) => { - callWithFlag(previous, flag, args); + declareWithFlag(previous, flag, args); }; const fullName = `${chainRegistry.get(previous).fullName}.${name}`; Object.defineProperty(fn, 'name', {value: fullName}); previous[name] = fn; - chainRegistry.set(fn, {flag, fullName, prev: previous}); + chainRegistry.set(fn, {flag, fullName, previous}); return fn; } -function callWithFlag(previous, flag, args) { +function declareWithFlag(previous, flag, args) { const combinedFlags = {[flag]: true}; do { const step = chainRegistry.get(previous); - if (step.call) { - step.call({...step.defaults, ...combinedFlags}, args); - previous = null; - } else { + if (step.flag) { combinedFlags[step.flag] = true; - previous = step.prev; + previous = step.previous; + } else { + step.declare(combinedFlags, args); + break; } } while (previous); } -function createHookChain(hook, isAfterHook) { +function createHookChain({allowCallbacks, isAfterHook = false}, hook) { // Hook chaining rules: // * `always` comes immediately after "after hooks" // * `skip` must come at the end // * no `only` // * no repeating - extendChain(hook, 'cb', 'callback'); extendChain(hook, 'skip', 'skipped'); - extendChain(hook.cb, 'skip', 'skipped'); if (isAfterHook) { extendChain(hook, 'always'); - extendChain(hook.always, 'cb', 'callback'); extendChain(hook.always, 'skip', 'skipped'); - extendChain(hook.always.cb, 'skip', 'skipped'); + } + + if (allowCallbacks) { + extendChain(hook, 'cb', 'callback'); + extendChain(hook.cb, 'skip', 'skipped'); + if (isAfterHook) { + extendChain(hook.always, 'cb', 'callback'); + extendChain(hook.always.cb, 'skip', 'skipped'); + } } return hook; } -function createChain(fn, defaults, meta) { +function createChain({ + allowCallbacks = true, + allowExperimentalMacros = false, + allowImplementationTitleFns = true, + allowMultipleImplementations = true, + annotations, + declare: declareWithOptions, + meta +}) { + const options = { + allowCallbacks, + allowExperimentalMacros, + allowImplementationTitleFns, + allowMultipleImplementations + }; + + const declare = (type, declaredAnnotations, args) => { + declareWithOptions({ + annotations: {...annotations, ...declaredAnnotations}, + args, + options, + type + }); + }; + + const macro = definition => { + if (typeof definition === 'function') { + return {exec: definition}; + } + + if (typeof definition === 'object' && definition !== null) { + const {exec, title} = definition; + if (typeof exec !== 'function') { + throw new TypeError('Macro object must have an exec() function'); + } + + if (title !== undefined && typeof title !== 'function') { + throw new Error('’title’ property of macro object must be a function'); + } + + return {exec, title}; + } + }; + // Test chaining rules: // * `serial` must come at the start // * `only` and `skip` must come at the end // * `failing` must come at the end, but can be followed by `only` and `skip` // * `only` and `skip` cannot be chained together // * no repeating - const root = startChain('test', fn, {...defaults, type: 'test'}); - extendChain(root, 'cb', 'callback'); + const root = startChain('test', {declare, type: 'test'}); extendChain(root, 'failing'); extendChain(root, 'only', 'exclusive'); extendChain(root, 'serial'); extendChain(root, 'skip', 'skipped'); - extendChain(root.cb, 'failing'); - extendChain(root.cb, 'only', 'exclusive'); - extendChain(root.cb, 'skip', 'skipped'); - extendChain(root.cb.failing, 'only', 'exclusive'); - extendChain(root.cb.failing, 'skip', 'skipped'); extendChain(root.failing, 'only', 'exclusive'); extendChain(root.failing, 'skip', 'skipped'); - extendChain(root.serial, 'cb', 'callback'); extendChain(root.serial, 'failing'); extendChain(root.serial, 'only', 'exclusive'); extendChain(root.serial, 'skip', 'skipped'); - extendChain(root.serial.cb, 'failing'); - extendChain(root.serial.cb, 'only', 'exclusive'); - extendChain(root.serial.cb, 'skip', 'skipped'); - extendChain(root.serial.cb.failing, 'only', 'exclusive'); - extendChain(root.serial.cb.failing, 'skip', 'skipped'); extendChain(root.serial.failing, 'only', 'exclusive'); extendChain(root.serial.failing, 'skip', 'skipped'); - root.after = createHookChain(startChain('test.after', fn, {...defaults, type: 'after'}), true); - root.afterEach = createHookChain(startChain('test.afterEach', fn, {...defaults, type: 'afterEach'}), true); - root.before = createHookChain(startChain('test.before', fn, {...defaults, type: 'before'}), false); - root.beforeEach = createHookChain(startChain('test.beforeEach', fn, {...defaults, type: 'beforeEach'}), false); + if (allowCallbacks) { + extendChain(root, 'cb', 'callback'); + extendChain(root.cb, 'failing'); + extendChain(root.cb, 'only', 'exclusive'); + extendChain(root.cb, 'skip', 'skipped'); + extendChain(root.cb.failing, 'only', 'exclusive'); + extendChain(root.cb.failing, 'skip', 'skipped'); + extendChain(root.serial, 'cb', 'callback'); + extendChain(root.serial.cb, 'failing'); + extendChain(root.serial.cb, 'only', 'exclusive'); + extendChain(root.serial.cb, 'skip', 'skipped'); + extendChain(root.serial.cb.failing, 'only', 'exclusive'); + extendChain(root.serial.cb.failing, 'skip', 'skipped'); + } + + root.after = createHookChain({allowCallbacks, isAfterHook: true}, startChain('test.after', {declare, type: 'after'})); + root.afterEach = createHookChain({allowCallbacks, isAfterHook: true}, startChain('test.afterEach', {declare, type: 'afterEach'})); + root.before = createHookChain({allowCallbacks}, startChain('test.before', {declare, type: 'before'})); + root.beforeEach = createHookChain({allowCallbacks}, startChain('test.beforeEach', {declare, type: 'beforeEach'})); - root.serial.after = createHookChain(startChain('test.after', fn, {...defaults, serial: true, type: 'after'}), true); - root.serial.afterEach = createHookChain(startChain('test.afterEach', fn, {...defaults, serial: true, type: 'afterEach'}), true); - root.serial.before = createHookChain(startChain('test.before', fn, {...defaults, serial: true, type: 'before'}), false); - root.serial.beforeEach = createHookChain(startChain('test.beforeEach', fn, {...defaults, serial: true, type: 'beforeEach'}), false); + root.serial.after = createHookChain({allowCallbacks, isAfterHook: true}, startChain('test.after', {annotations: {serial: true}, declare, type: 'after'})); + root.serial.afterEach = createHookChain({allowCallbacks, isAfterHook: true}, startChain('test.afterEach', {annotations: {serial: true}, declare, type: 'afterEach'})); + root.serial.before = createHookChain({allowCallbacks}, startChain('test.before', {annotations: {serial: true}, declare, type: 'before'})); + root.serial.beforeEach = createHookChain({allowCallbacks}, startChain('test.beforeEach', {annotations: {serial: true}, declare, type: 'beforeEach'})); + root.serial.macro = macro; // "todo" tests cannot be chained. Allow todo tests to be flagged as needing // to be serial. - 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.todo = startChain('test.todo', {declare, type: 'todo'}); + root.serial.todo = startChain('test.serial.todo', {annotations: {serial: true}, declare, type: 'todo'}); + root.macro = macro; root.meta = meta; return root; diff --git a/lib/load-config.js b/lib/load-config.js index f0807f9d3..aeb1eb03a 100644 --- a/lib/load-config.js +++ b/lib/load-config.js @@ -7,7 +7,7 @@ const pkgConf = require('pkg-conf'); const NO_SUCH_FILE = Symbol('no ava.config.js file'); const MISSING_DEFAULT_EXPORT = Symbol('missing default export'); -const EXPERIMENTS = new Set(); +const EXPERIMENTS = new Set(['experimentalTestInterfaces']); // *Very* rudimentary support for loading ava.config.js files containing an `export default` statement. const evaluateJsConfig = configFile => { diff --git a/lib/parse-test-args.js b/lib/parse-test-args.js index 5ea5f0aa4..27f593a84 100644 --- a/lib/parse-test-args.js +++ b/lib/parse-test-args.js @@ -1,11 +1,48 @@ 'use strict'; -function parseTestArgs(args) { +const macroTitleFns = new WeakMap(); + +function parseTestArgs(args, { + allowExperimentalMacros, + allowImplementationTitleFns, + allowMultipleImplementations +}) { const rawTitle = typeof args[0] === 'string' ? args.shift() : undefined; const receivedImplementationArray = Array.isArray(args[0]); const implementations = receivedImplementationArray ? args.shift() : args.splice(0, 1); + if (receivedImplementationArray && !allowMultipleImplementations) { + throw new Error('test(), test.serial() and hooks no longer take arrays of implementations or macros'); + } + + if (allowExperimentalMacros) { + // TODO: Clean this up after removing the legacy implementation which + // allows multiple implementations. + const [possibleMacro] = implementations; + if (possibleMacro !== null && typeof possibleMacro === 'object' && typeof possibleMacro.exec === 'function') { + // Never call exec() on the macro object. + let {exec} = possibleMacro; + if (typeof possibleMacro.title === 'function') { + // Wrap so we can store the title function against *this use* of the macro. + exec = exec.bind(null); + macroTitleFns.set(exec, possibleMacro.title); + } + + implementations[0] = exec; + } + } + const buildTitle = implementation => { - const title = implementation.title ? implementation.title(rawTitle, ...args) : rawTitle; + let title = rawTitle; + if (implementation.title) { + if (!allowImplementationTitleFns) { + throw new Error('Test and hook implementations can no longer have a title function'); + } + + title = implementation.title(rawTitle, ...args); + } else if (macroTitleFns.has(implementation)) { + title = macroTitleFns.get(implementation)(rawTitle, ...args); + } + return {title, isSet: typeof title !== 'undefined', isValid: typeof title === 'string', isEmpty: !title}; }; diff --git a/lib/runner.js b/lib/runner.js index f1a221a36..d389ed122 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -7,6 +7,7 @@ const parseTestArgs = require('./parse-test-args'); const snapshotManager = require('./snapshot-manager'); const serializeError = require('./serialize-error'); const Runnable = require('./test'); +const {Task, TaskList} = require('./task-list'); class Runner extends Emittery { constructor(options = {}) { @@ -29,17 +30,7 @@ class Runner extends Emittery { this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this); this.interrupted = false; this.snapshots = null; - this.tasks = { - after: [], - afterAlways: [], - afterEach: [], - afterEachAlways: [], - before: [], - beforeEach: [], - concurrent: [], - serial: [], - todo: [] - }; + this.tasks = new TaskList(); const uniqueTestTitles = new Set(); this.registerUniqueTitle = title => { @@ -53,121 +44,152 @@ class Runner extends Emittery { let hasStarted = false; let scheduledStart = false; - const meta = Object.freeze({ - file: options.file, - get snapshotDirectory() { - const {file, snapshotDir: fixedLocation, projectDir} = options; - return snapshotManager.determineSnapshotDir({file, fixedLocation, projectDir}); - } - }); - this.chain = createChain((metadata, testArgs) => { // eslint-disable-line complexity - if (hasStarted) { - throw new Error('All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.'); - } - - if (!scheduledStart) { - scheduledStart = true; - process.nextTick(() => { - hasStarted = true; - this.start(); - }); - } - - const {args, buildTitle, implementations, rawTitle} = parseTestArgs(testArgs); - - if (metadata.todo) { - if (implementations.length > 0) { - throw new TypeError('`todo` tests are not allowed to have an implementation. Use `test.skip()` for tests with an implementation.'); + const chainOptions = { + annotations: { + always: false, + callback: false, + exclusive: false, + failing: false, + inline: false, // Default value; only attempts created by `t.try()` have this annotation set to `true`. + serial: false, + skipped: false + }, + meta: Object.freeze({ + file: options.file, + get snapshotDirectory() { + const {file, snapshotDir: fixedLocation, projectDir} = options; + return snapshotManager.determineSnapshotDir({file, fixedLocation, projectDir}); } - - if (!rawTitle) { // Either undefined or a string. - throw new TypeError('`todo` tests require a title'); + }), + declare: ({ // eslint-disable-line complexity + annotations, + args: declarationArguments, + options, + type + }) => { + if (hasStarted) { + throw new Error('All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.'); } - if (!this.registerUniqueTitle(rawTitle)) { - throw new Error(`Duplicate test title: ${rawTitle}`); + if (!scheduledStart) { + scheduledStart = true; + process.nextTick(() => { + hasStarted = true; + this.start(); + }); } - if (this.match.length > 0) { - // --match selects TODO tests. - if (matcher([rawTitle], this.match).length === 1) { - metadata.exclusive = true; - this.runOnlyExclusive = true; - } - } + const {args, buildTitle, implementations, rawTitle} = parseTestArgs(declarationArguments, options); - this.tasks.todo.push({title: rawTitle, metadata}); - this.emit('stateChange', { - type: 'declared-test', - title: rawTitle, - knownFailing: false, - todo: true - }); - } else { - if (implementations.length === 0) { - throw new TypeError('Expected an implementation. Use `test.todo()` for tests without an implementation.'); - } + if (type === 'todo') { + if (implementations.length > 0) { + throw new TypeError('`todo` tests are not allowed to have an implementation. Use `test.skip()` for tests with an implementation.'); + } - for (const implementation of implementations) { - let {title, isSet, isValid, isEmpty} = buildTitle(implementation); + if (!rawTitle) { // Either undefined or a string. + throw new TypeError('`todo` tests require a title'); + } - if (isSet && !isValid) { - throw new TypeError('Test & hook titles must be strings'); + if (!this.registerUniqueTitle(rawTitle)) { + throw new Error(`Duplicate test title: ${rawTitle}`); } - 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 (this.match.length > 0) { + // --match selects TODO tests. + if (matcher([rawTitle], this.match).length === 1) { + annotations.exclusive = true; + this.runOnlyExclusive = true; } } - if (metadata.type === 'test' && !this.registerUniqueTitle(title)) { - throw new Error(`Duplicate test title: ${title}`); + this.tasks.add(Task.todo({annotations, title: rawTitle})); + this.emit('stateChange', { + type: 'declared-test', + title: rawTitle, + knownFailing: false, + todo: true + }); + } else { + if (implementations.length === 0) { + throw new TypeError('Expected an implementation. Use `test.todo()` for tests without an implementation.'); } - const task = { - title, - implementation, - args, - metadata: {...metadata} - }; - - if (metadata.type === 'test') { - if (this.match.length > 0) { - // --match overrides .only() - task.metadata.exclusive = matcher([title], this.match).length === 1; + for (const implementation of implementations) { + let {title, isSet, isValid, isEmpty} = buildTitle(implementation); + + if (isSet && !isValid) { + throw new TypeError('Test & hook titles must be strings'); } - if (task.metadata.exclusive) { - this.runOnlyExclusive = true; + if (isEmpty) { + if (type === 'test') { + throw new TypeError('Tests must have a title'); + } else if (annotations.always) { + title = `${type}.always hook`; + } else { + title = `${type} hook`; + } + } + + if (type === 'test' && !this.registerUniqueTitle(title)) { + throw new Error(`Duplicate test title: ${title}`); } - this.tasks[metadata.serial ? 'serial' : 'concurrent'].push(task); - 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); + if (type === 'test') { + let {exclusive} = annotations; + if (this.match.length > 0) { + // --match overrides .only() + exclusive = matcher([title], this.match).length === 1; + } + + if (exclusive) { + this.runOnlyExclusive = true; + } + + this.tasks.add(Task.test({ + annotations: {...annotations, exclusive}, + args, + implementation, + options, + title + })); + this.emit('stateChange', { + type: 'declared-test', + title, + knownFailing: annotations.failing, + todo: false + }); + } else { + this.tasks.add(Task[type]({ + annotations, + args, + implementation, + options, + title + })); + } } } } - }, { - serial: false, - exclusive: false, - skipped: false, - todo: false, - failing: false, - callback: false, - inline: false, // Set for attempt metadata created by `t.try()` - always: false - }, meta); + }; + + this.chain = createChain({ + allowCallbacks: true, + allowExperimentalMacros: false, + allowImplementationTitleFns: true, + allowMultipleImplementations: true, + ...chainOptions + }); + + if (this.experiments.experimentalTestInterfaces) { + this.experimentalChain = createChain({ + allowCallbacks: false, + allowExperimentalMacros: true, + allowImplementationTitleFns: false, + allowMultipleImplementations: false, + ...chainOptions + }); + } } compareTestSnapshot(options) { @@ -237,11 +259,11 @@ class Runner extends Emittery { let waitForSerial = Promise.resolve(); await runnables.reduce((previous, runnable) => { - if (runnable.metadata.serial || this.serial) { + if (runnable.annotations.serial || this.serial) { waitForSerial = previous.then(() => { // Serial runnables run as long as there was no previous failure, unless // the runnable should always be run. - return (allPassed || runnable.metadata.always) && runAndStoreResult(runnable); + return (allPassed || runnable.annotations.always) && runAndStoreResult(runnable); }); return waitForSerial; } @@ -253,7 +275,7 @@ class Runner extends Emittery { // runnables have completed, as long as there was no previous failure // (or if the runnable should always be run). One concurrent runnable's // failure does not prevent the next runnable from running. - return (allPassed || runnable.metadata.always) && runAndStoreResult(runnable); + return (allPassed || runnable.annotations.always) && runAndStoreResult(runnable); }) ]); }, waitForSerial); @@ -270,8 +292,10 @@ class Runner extends Emittery { return result; } - async runHooks(tasks, contextRef, titleSuffix, testPassed) { - const hooks = tasks.map(task => new Runnable({ + async runHooks(type, contextRef, titleSuffix, testPassed) { + const hooks = [...this.tasks.select(type)].map(task => new Runnable({ + ...task.options, + annotations: task.annotations, contextRef, experiments: this.experiments, failWithoutAssertions: false, @@ -280,7 +304,6 @@ class Runner extends Emittery { t => task.implementation.apply(null, [t].concat(task.args)), compareTestSnapshot: this.boundCompareTestSnapshot, updateSnapshots: this.updateSnapshots, - metadata: task.metadata, powerAssert: this.powerAssert, title: `${task.title}${titleSuffix || ''}`, testPassed @@ -310,12 +333,16 @@ class Runner extends Emittery { async runTest(task, contextRef) { const hookSuffix = ` for ${task.title}`; - let hooksOk = await this.runHooks(this.tasks.beforeEach, contextRef, hookSuffix); + let hooksOk = await this.runHooks('beforeEach', contextRef, hookSuffix); let testOk = false; if (hooksOk) { // Only run the test if all `beforeEach` hooks passed. const test = new Runnable({ + allowExperimentalMacros: task.allowExperimentalMacros, + allowImplementationTitleFns: task.allowImplementationTitleFns, + allowMultipleImplementations: task.allowMultipleImplementations, + annotations: task.annotations, contextRef, experiments: this.experiments, failWithoutAssertions: this.failWithoutAssertions, @@ -324,7 +351,6 @@ class Runner extends Emittery { t => task.implementation.apply(null, [t].concat(task.args)), compareTestSnapshot: this.boundCompareTestSnapshot, updateSnapshots: this.updateSnapshots, - metadata: task.metadata, powerAssert: this.powerAssert, title: task.title, registerUniqueTitle: this.registerUniqueTitle @@ -338,73 +364,64 @@ class Runner extends Emittery { type: 'test-passed', title: result.title, duration: result.duration, - knownFailing: result.metadata.failing, + knownFailing: result.annotations.failing, logs: result.logs }); - hooksOk = await this.runHooks(this.tasks.afterEach, contextRef, hookSuffix, testOk); + hooksOk = await this.runHooks('afterEach', contextRef, hookSuffix, testOk); } else { this.emit('stateChange', { type: 'test-failed', title: result.title, err: serializeError('Test failure', true, result.error), duration: result.duration, - knownFailing: result.metadata.failing, + knownFailing: result.annotations.failing, logs: result.logs }); // Don't run `afterEach` hooks if the test failed. } } - const alwaysOk = await this.runHooks(this.tasks.afterEachAlways, contextRef, hookSuffix, testOk); + const alwaysOk = await this.runHooks('afterEachAlways', contextRef, hookSuffix, testOk); return alwaysOk && hooksOk && testOk; } async start() { - const concurrentTests = []; - const serialTests = []; - for (const task of this.tasks.serial) { - if (this.runOnlyExclusive && !task.metadata.exclusive) { + let concurrentTests = []; + let serialTests = []; + for (const task of this.tasks.select('test')) { + if (this.runOnlyExclusive && !task.annotations.exclusive) { continue; } this.emit('stateChange', { type: 'selected-test', title: task.title, - knownFailing: task.metadata.failing, - skip: task.metadata.skipped, + knownFailing: task.annotations.failing, + skip: task.annotations.skipped, todo: false }); - if (!task.metadata.skipped) { - serialTests.push(task); - } - } - - for (const task of this.tasks.concurrent) { - if (this.runOnlyExclusive && !task.metadata.exclusive) { + if (task.annotations.skipped) { continue; } - this.emit('stateChange', { - type: 'selected-test', - title: task.title, - knownFailing: task.metadata.failing, - skip: task.metadata.skipped, - todo: false - }); - - if (!task.metadata.skipped) { - if (this.serial) { - serialTests.push(task); - } else { - concurrentTests.push(task); - } + if (task.annotations.serial) { + serialTests.push(task); + } else { + concurrentTests.push(task); } } - for (const task of this.tasks.todo) { - if (this.runOnlyExclusive && !task.metadata.exclusive) { + // Reassign the concurrent tasks, but always run them after the explicitly + // serial ones. + if (this.serial) { + serialTests = [...serialTests, ...concurrentTests]; + concurrentTests = []; + } + + for (const task of this.tasks.select('todo')) { + if (this.runOnlyExclusive && !task.annotations.exclusive) { continue; } @@ -426,7 +443,7 @@ class Runner extends Emittery { const contextRef = new ContextRef(); // Note that the hooks and tests always begin running asynchronously. - const beforePromise = this.runHooks(this.tasks.before, contextRef); + const beforePromise = this.runHooks('before', contextRef); const serialPromise = beforePromise.then(beforeHooksOk => { // eslint-disable-line promise/prefer-await-to-then // Don't run tests if a `before` hook failed. if (!beforeHooksOk) { @@ -476,11 +493,11 @@ class Runner extends Emittery { const ok = await concurrentPromise; // Only run `after` hooks if all hooks and tests passed. if (ok) { - await this.runHooks(this.tasks.after, contextRef); + await this.runHooks('after', contextRef); } // Always run `after.always` hooks. - await this.runHooks(this.tasks.afterAlways, contextRef); + await this.runHooks('afterAlways', contextRef); process.removeListener('beforeExit', beforeExitHandler); await this.emit('finish'); } catch (error) { diff --git a/lib/task-list.js b/lib/task-list.js new file mode 100644 index 000000000..6e685e3a9 --- /dev/null +++ b/lib/task-list.js @@ -0,0 +1,143 @@ +class Task { + static after({annotations, args, implementation, options, title}) { + return new Task(annotations.always ? 'afterAlways' : 'after', {annotations, args, implementation, options, title}); + } + + static afterEach({annotations, args, implementation, options, title}) { + return new Task(annotations.always ? 'afterEachAlways' : 'afterEach', {annotations, args, implementation, options, title}); + } + + static before({annotations, args, implementation, options, title}) { + return new Task('before', {annotations, args, implementation, options, title}); + } + + static beforeEach({annotations, args, implementation, options, title}) { + return new Task('beforeEach', {annotations, args, implementation, options, title}); + } + + static test({annotations, args, implementation, options, title}) { + return new Task('test', {annotations, args, implementation, options, title}); + } + + static todo({annotations, title}) { + return new Task('todo', {annotations, title}); + } + + constructor(type, {annotations, args, implementation, options, title}) { + this.annotations = annotations; + this.args = args; + this.implementation = implementation; + this.options = options; + this.title = title; + this.type = type; + + this.previous = null; + } +} + +exports.Task = Task; + +class TaskList { + constructor(forkedFrom) { + // Hooks are kept as a reverse linked list, so that forks can easily extend them. + this.lastAfter = forkedFrom ? forkedFrom.lastAfter : null; + this.lastAfterAlways = forkedFrom ? forkedFrom.lastAfterAlways : null; + this.lastAfterEach = forkedFrom ? forkedFrom.lastAfterEach : null; + this.lastAfterEachAlways = forkedFrom ? forkedFrom.lastAfterEachAlways : null; + this.lastBefore = forkedFrom ? forkedFrom.lastBefore : null; + this.lastBeforeEach = forkedFrom ? forkedFrom.lastBeforeEach : null; + + this.test = []; + this.todo = []; + } + + add(task) { + switch (task.type) { + case 'after': + task.previous = this.lastAfter; + this.lastAfter = task; + break; + case 'afterAlways': + task.previous = this.lastAfterAlways; + this.lastAfterAlways = task; + break; + case 'afterEach': + task.previous = this.lastAfterEach; + this.lastAfterEach = task; + break; + case 'afterEachAlways': + task.previous = this.lastAfterEachAlways; + this.lastAfterEachAlways = task; + break; + case 'before': + task.previous = this.lastBefore; + this.lastBefore = task; + break; + case 'beforeEach': + task.previous = this.lastBeforeEach; + this.lastBeforeEach = task; + break; + case 'test': + this.test.push(task); + break; + case 'todo': + this.todo.push(task); + break; + default: + throw new TypeError(`Unhandled type ${task.type}`); + } + } + + fork() { + return new TaskList(this); + } + + * select(type) { + if (type === 'test' || type === 'todo') { + for (const task of this[type]) { + yield task; + } + + return; + } + + let last; + switch (type) { + case 'after': + last = this.lastAfter; + break; + case 'afterAlways': + last = this.lastAfterAlways; + break; + case 'afterEach': + last = this.lastAfterEach; + break; + case 'afterEachAlways': + last = this.lastAfterEachAlways; + break; + case 'before': + last = this.lastBefore; + break; + case 'beforeEach': + last = this.lastBeforeEach; + break; + default: + throw new TypeError(`Unknown type ${type}`); + } + + const collected = []; + while (last !== null) { + if (!last.annotations.skipped) { + collected.push(last); + } + + last = last.previous; + } + + for (let i = collected.length - 1; i >= 0; i--) { + yield collected[i]; + } + } +} + +exports.TaskList = TaskList; diff --git a/lib/test.js b/lib/test.js index 99e7bfb4a..688f9c836 100644 --- a/lib/test.js +++ b/lib/test.js @@ -69,7 +69,11 @@ class ExecutionContext extends assert.Assertions { }; this.try = async (...attemptArgs) => { - const {args, buildTitle, implementations, receivedImplementationArray} = parseTestArgs(attemptArgs); + const {args, buildTitle, implementations, receivedImplementationArray} = parseTestArgs(attemptArgs, { + allowExperimentalMacros: test.allowExperimentalMacros, + allowImplementationTitleFns: test.allowImplementationTitleFns, + allowMultipleImplementations: test.allowMultipleImplementations + }); if (implementations.length === 0) { throw new TypeError('Expected an implementation.'); @@ -189,23 +193,60 @@ class ExecutionContext extends assert.Assertions { class Test { constructor(options) { - this.contextRef = options.contextRef; - this.experiments = options.experiments || {}; - this.failWithoutAssertions = options.failWithoutAssertions; - this.fn = options.fn; - this.metadata = options.metadata; - this.powerAssert = options.powerAssert; - this.title = options.title; - this.testPassed = options.testPassed; - this.registerUniqueTitle = options.registerUniqueTitle; + const { + allowExperimentalMacros = false, + allowImplementationTitleFns = true, + allowMultipleImplementations = true, + annotations, + compareTestSnapshot, + contextRef, + experiments = {}, + failWithoutAssertions = true, + fn, + nextSnapshotIndex = 0, + powerAssert, + registerUniqueTitle, + testPassed = false, + title, + updateSnapshots = false + } = options; + const {snapshotBelongsTo = title} = options; + + this.allowExperimentalMacros = allowExperimentalMacros; + this.allowImplementationTitleFns = allowImplementationTitleFns; + this.allowMultipleImplementations = allowMultipleImplementations; + this.annotations = annotations; + this.assertCount = 0; + this.assertError = undefined; + this.attemptCount = 0; + this.calledEnd = false; + this.contextRef = contextRef; + this.duration = null; + this.endCallbackFinisher = null; + this.experiments = experiments; + this.failWithoutAssertions = failWithoutAssertions; + this.finishDueToAttributedError = null; + this.finishDueToInactivity = null; + this.finishDueToTimeout = null; + this.finishing = false; + this.fn = fn; this.logs = []; - - const {snapshotBelongsTo = this.title, nextSnapshotIndex = 0} = options; - this.snapshotBelongsTo = snapshotBelongsTo; this.nextSnapshotIndex = nextSnapshotIndex; + this.pendingAssertionCount = 0; + this.pendingAttemptCount = 0; + this.pendingThrowsAssertion = null; + this.planCount = null; + this.powerAssert = powerAssert; + this.registerUniqueTitle = registerUniqueTitle; + this.snapshotBelongsTo = snapshotBelongsTo; this.snapshotCount = 0; + this.startedAt = 0; + this.testPassed = testPassed; + this.timeoutMs = 0; + this.timeoutTimer = null; + this.title = title; - const deferRecording = this.metadata.inline; + const {inline: deferRecording} = this.annotations; this.deferredSnapshotRecordings = []; this.compareWithSnapshot = ({expected, id, message}) => { this.snapshotCount++; @@ -215,7 +256,7 @@ class Test { const index = id ? 0 : this.nextSnapshotIndex++; const label = id ? '' : message || `Snapshot ${index + 1}`; // Human-readable labels start counting at 1. - const {record, ...result} = options.compareTestSnapshot({belongsTo, deferRecording, expected, index, label}); + const {record, ...result} = compareTestSnapshot({belongsTo, deferRecording, expected, index, label}); if (record) { this.deferredSnapshotRecordings.push(record); } @@ -224,7 +265,7 @@ class Test { }; this.skipSnapshot = () => { - if (options.updateSnapshots) { + if (updateSnapshots) { this.addFailedAssertion(new Error('Snapshot assertions cannot be skipped when updating snapshots')); } else { this.nextSnapshotIndex++; @@ -244,11 +285,11 @@ class Test { const {contextRef, snapshotBelongsTo, nextSnapshotIndex, snapshotCount: startingSnapshotCount} = this; const attempt = new Test({ ...options, - fn, - metadata: {...options.metadata, callback: false, failing: false, inline: true}, + annotations: {...annotations, callback: false, failing: false, inline: true}, contextRef: contextRef.copy(), - snapshotBelongsTo, + fn, nextSnapshotIndex, + snapshotBelongsTo, title }); @@ -256,34 +297,16 @@ class Test { const errors = error ? [error] : []; return {assertCount, deferredSnapshotRecordings, errors, logs, passed, snapshotCount, startingSnapshotCount}; }; - - this.assertCount = 0; - this.assertError = undefined; - this.attemptCount = 0; - this.calledEnd = false; - this.duration = null; - this.endCallbackFinisher = null; - this.finishDueToAttributedError = null; - this.finishDueToInactivity = null; - this.finishDueToTimeout = null; - this.finishing = false; - this.pendingAssertionCount = 0; - this.pendingAttemptCount = 0; - this.pendingThrowsAssertion = null; - this.planCount = null; - this.startedAt = 0; - this.timeoutMs = 0; - this.timeoutTimer = null; } bindEndCallback() { - if (this.metadata.callback) { + if (this.annotations.callback) { return (error, savedError) => { this.endCallback(error, savedError); }; } - if (this.metadata.inline) { + if (this.annotations.inline) { throw new Error('`t.end()` is not supported inside `t.try()`'); } else { throw new Error('`t.end()` is not supported in this context. To use `t.end()` as a callback, you must use "callback mode" via `test.cb(testName, fn)`'); @@ -594,7 +617,7 @@ class Test { promise = Promise.resolve(result.retval); } - if (this.metadata.callback) { + if (this.annotations.callback) { if (returnedObservable || returnedPromise) { const asyncType = returnedObservable ? 'observables' : 'promises'; this.saveFirstError(new Error(`Do not return ${asyncType} from tests declared via \`test.cb(…)\`. Use \`test.cb(…)\` for legacy callback APIs. When using promises, observables or async functions, use \`test(…)\`.`)); @@ -676,7 +699,7 @@ class Test { let error = this.assertError; let passed = !error; - if (this.metadata.failing) { + if (this.annotations.failing) { passed = !passed; if (passed) { @@ -687,11 +710,11 @@ class Test { } return { + annotations: this.annotations, deferredSnapshotRecordings: this.deferredSnapshotRecordings, duration: this.duration, error, logs: this.logs, - metadata: this.metadata, passed, snapshotCount: this.snapshotCount, assertCount: this.assertCount, diff --git a/lib/worker/main.js b/lib/worker/main.js index 80f0497c7..52d9c5f06 100644 --- a/lib/worker/main.js +++ b/lib/worker/main.js @@ -1,21 +1,28 @@ 'use strict'; const runner = require('./subprocess').getRunner(); -const makeCjsExport = () => { +const makeCjsExport = (chain = runner.chain) => { function test(...args) { - return runner.chain(...args); + return chain(...args); } - return Object.assign(test, runner.chain); + return Object.assign(test, chain); }; // Support CommonJS modules by exporting a test function that can be fully // chained. Also support ES module loaders by exporting __esModule and a -// default. Support `import * as ava from 'ava'` use cases by exporting a -// `test` member. Do all this whilst preventing `test.test.test() or -// `test.default.test()` chains, though in CommonJS `test.test()` is -// unavoidable. -module.exports = Object.assign(makeCjsExport(), { +// default. +exports.ava3 = () => Object.assign(makeCjsExport(), { __esModule: true, - default: runner.chain + default: makeCjsExport() }); + +// Only export a test function that can be fully chained. This will be the +// behavior in AVA 4. +exports.experimental = () => { + if (!runner.experimentalChain) { + throw new Error('You must enable the ’experimentalTestInterfaces’ experiment'); + } + + return makeCjsExport(runner.experimentalChain); +}; diff --git a/package.json b/package.json index 5f54037eb..7feaa67a6 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "lib", "*.js", "!*.config.js", - "index.d.ts" + "index.d.ts", + "experimental.d.ts" ], "keywords": [ "🦄", diff --git a/test-d/experimental-forkable.ts b/test-d/experimental-forkable.ts new file mode 100644 index 000000000..8ad145bd6 --- /dev/null +++ b/test-d/experimental-forkable.ts @@ -0,0 +1,13 @@ +import {expectType} from 'tsd'; +import test, {ForkableTestInterface, ForkableSerialInterface} from '../experimental'; + +const foo = test.make(); +expectType(foo); + +const bar = foo.fork(); +expectType(bar); + +const baz = foo.serial.fork(); +expectType(baz); +const thud = baz.fork(); +expectType(thud); diff --git a/test-d/experimental-macro.ts b/test-d/experimental-macro.ts new file mode 100644 index 000000000..47e2ff485 --- /dev/null +++ b/test-d/experimental-macro.ts @@ -0,0 +1,80 @@ +import {expectError} from 'tsd'; +import test, {TestInterface} from '../experimental'; + +{ + const macro = test.macro(t => t.pass()); + test(macro); + test('title', macro); + test.serial(macro); + test.serial('title', macro); + expectError(test(macro, 'foo')); + expectError(test('title', macro, 'foo')); + expectError(test.serial(macro, 'foo')); + expectError(test.serial('title', macro, 'foo')); +} + +{ + const macro = test.serial.macro(t => t.pass()); + test(macro); + test.serial(macro); +} + +{ + const macro = test.macro<[string]>((t, string) => t.is(string, 'foo')); + test(macro, 'foo'); + test('title', macro, 'foo'); + test.serial(macro, 'foo'); + test.serial('title', macro, 'foo'); + expectError(test(macro)); + expectError(test('title', macro)); + expectError(test.serial(macro)); + expectError(test.serial('title', macro)); +} + +{ + const macro = test.macro<[string]>({ + exec: (t, string) => t.is(string, 'foo') + }); + test(macro, 'foo'); + test('title', macro, 'foo'); + test.serial(macro, 'foo'); + test.serial('title', macro, 'foo'); + expectError(test.serial(macro)); + expectError(test.serial('title', macro)); +} + +{ + const macro = test.macro<[string]>({ + exec: (t, string) => t.is(string, 'foo'), + title: (prefix, string) => `${prefix ?? 'title'} ${string}` + }); + test(macro, 'foo'); + test('title', macro, 'foo'); + test.serial(macro, 'foo'); + test.serial('title', macro, 'foo'); + expectError(test(macro)); + expectError(test('title', macro)); + expectError(test.serial(macro)); + expectError(test.serial('title', macro)); +} + +test.serial.macro<[], { str: string }>(t => t.is(t.context.str, 'foo')); +test.serial.macro<[string], { str: string }>((t, string) => t.is(t.context.str, string)); +(test as TestInterface<{ str: string }>).macro(t => t.is(t.context.str, 'foo')); +(test as TestInterface<{ str: string }>).macro<[string]>((t, string) => t.is(t.context.str, string)); + +{ + const macro = test.macro<[], { foo: string }>(t => t.is(t.context.foo, 'foo')); + // ;(test as TestInterface<{foo: string, bar: string}>)(macro) + expectError((test as TestInterface<{bar: string}>)(macro)); +} + +{ + const macro = test.macro(t => t.pass()); + expectError(test.before(macro)); + expectError(test.beforeEach(macro)); + expectError(test.after(macro)); + expectError(test.after.always(macro)); + expectError(test.afterEach(macro)); + expectError(test.afterEach.always(macro)); +} diff --git a/test-d/experimental-try-commit.ts b/test-d/experimental-try-commit.ts new file mode 100644 index 000000000..01ec999da --- /dev/null +++ b/test-d/experimental-try-commit.ts @@ -0,0 +1,66 @@ +import {expectType} from 'tsd'; +import test, {ExecutionContext, Macro} from '../experimental'; + +test('attempt', async t => { + const attempt = await t.try( + (u, a, b) => { + expectType(u); + expectType(a); + expectType(b); + }, + 'string', + 6 + ); + attempt.commit(); +}); + +test('attempt with title', async t => { + const attempt = await t.try( + 'attempt title', + (u, a, b) => { + expectType(u); + expectType(a); + expectType(b); + }, + 'string', + 6 + ); + attempt.commit(); +}); + +{ + const lengthCheck = (t: ExecutionContext, a: string, b: number): void => { + t.is(a.length, b); + }; + + test('attempt with helper', async t => { + const attempt = await t.try(lengthCheck, 'string', 6); + attempt.commit(); + }); + + test('attempt with title', async t => { + const attempt = await t.try('title', lengthCheck, 'string', 6); + attempt.commit(); + }); +} + +test('all possible variants to pass to t.try', async t => { + // No params + t.try(tt => tt.pass()); + + t.try('test', tt => tt.pass()); + + // Some params + t.try((tt, a, b) => tt.is(a.length, b), 'hello', 5); + + t.try('test', (tt, a, b) => tt.is(a.length, b), 'hello', 5); + + // Macro with title + const macro1 = test.macro({ + exec: (tt, a, b) => tt.is(a.length, b), + title: (title, a, b) => `${title ? `${String(title)} ` : ''}str: "${String(a)}" with len: "${String(b)}"` + }); + + t.try(macro1, 'hello', 5); + t.try('title', macro1, 'hello', 5); +}); diff --git a/test-d/experimental.ts b/test-d/experimental.ts new file mode 100644 index 000000000..1c46a687a --- /dev/null +++ b/test-d/experimental.ts @@ -0,0 +1,106 @@ +import {expectError} from 'tsd'; +import test, {ExecutionContext, Implementation, ImplementationWithArgs} from '../experimental'; + +test('title', t => t.pass()); + +expectError(test<[string]>('explicit argument type', t => t.pass(), 42)); + +expectError(test<[string]>('missing argument', (t: ExecutionContext) => t.pass())); + +test('optional arguments', t => t.pass()); +test('optional arguments, with values', t => t.pass(), 'foo', 'bar'); + +expectError(test('argument type inferred from implementation', (t, string) => t.is(string, 'foo'), 42)); + +expectError(test('argument type inferred in implementation', (t, string) => t.is(string, 'foo'), 42)); + +{ + const implementation: Implementation = t => t.pass(); + expectError(test('unexpected arguments', implementation, 'foo')); +} + +{ + const implementation: ImplementationWithArgs<[string]> = (t, string) => t.is(string, 'foo'); + test('unexpected arguments', implementation, 'foo'); +} + +test.failing<[string]>('failing test with arguments', (t, string) => t.is(string, 'foo'), 'foo'); +test.only<[string]>('only test with arguments', (t, string) => t.is(string, 'foo'), 'foo'); +test.skip<[string]>('serial test with arguments', (t, string) => t.is(string, 'foo'), 'foo'); + +test.after.always.skip<[string]>('after.always hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.after.always.skip<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.after.always<[string]>('after.always hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.after.always<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.after.skip<[string]>('after hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.after.skip<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.after<[string]>('after hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.after<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.afterEach.always.skip<[string]>('after.always hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.afterEach.always.skip<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.afterEach.always<[string]>('after.always hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.afterEach.always<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.afterEach.skip<[string]>('after hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.afterEach.skip<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.afterEach<[string]>('after hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.afterEach<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.before.skip<[string]>('before hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.before.skip<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.before<[string]>('before hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.before<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.beforeEach.skip<[string]>('before hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.beforeEach.skip<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.beforeEach<[string]>('before hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.beforeEach<[string]>((t, string) => t.is(string, 'foo'), 'foo'); + +test.serial('title', t => t.pass()); + +expectError(test.serial<[string]>('explicit argument type', t => t.pass(), 42)); + +expectError(test.serial<[string]>('missing argument', (t: ExecutionContext) => t.pass())); + +test.serial('optional arguments', t => t.pass()); +test.serial('optional arguments, with values', t => t.pass(), 'foo', 'bar'); + +expectError(test.serial('argument type inferred from implementation', (t, string) => t.is(string, 'foo'), 42)); + +expectError(test.serial('argument type inferred in implementation', (t, string) => t.is(string, 'foo'), 42)); + +{ + const implementation: Implementation = t => t.pass(); + expectError(test.serial('unexpected arguments', implementation, 'foo')); +} + +{ + const implementation: ImplementationWithArgs<[string]> = (t, string) => t.is(string, 'foo'); + test.serial('unexpected arguments', implementation, 'foo'); +} + +test.serial.failing<[string]>('failing test with arguments', (t, string) => t.is(string, 'foo'), 'foo'); +test.serial.only<[string]>('only test with arguments', (t, string) => t.is(string, 'foo'), 'foo'); +test.serial.skip<[string]>('serial test with arguments', (t, string) => t.is(string, 'foo'), 'foo'); + +test.serial.after.always.skip<[string]>('after.always hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.serial.after.always.skip<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.serial.after.always<[string]>('after.always hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.serial.after.always<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.serial.after.skip<[string]>('after hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.serial.after.skip<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.serial.after<[string]>('after hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.serial.after<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.serial.afterEach.always.skip<[string]>('after.always hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.serial.afterEach.always.skip<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.serial.afterEach.always<[string]>('after.always hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.serial.afterEach.always<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.serial.afterEach.skip<[string]>('after hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.serial.afterEach.skip<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.serial.afterEach<[string]>('after hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.serial.afterEach<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.serial.before.skip<[string]>('before hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.serial.before.skip<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.serial.before<[string]>('before hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.serial.before<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.serial.beforeEach.skip<[string]>('before hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.serial.beforeEach.skip<[string]>((t, string) => t.is(string, 'foo'), 'foo'); +test.serial.beforeEach<[string]>('before hook with args', (t, string) => t.is(string, 'foo'), 'foo'); +test.serial.beforeEach<[string]>((t, string) => t.is(string, 'foo'), 'foo'); diff --git a/test-tap/helper/ava-test.js b/test-tap/helper/ava-test.js index 47f99e0b1..90f658696 100644 --- a/test-tap/helper/ava-test.js +++ b/test-tap/helper/ava-test.js @@ -14,48 +14,48 @@ function withExperiments(experiments = {}) { function ava(fn, contextRef, title = 'test') { return new Test({ + annotations: {type: 'test', callback: false}, contextRef: contextRef || new ContextRef(), experiments, failWithoutAssertions: true, fn, registerUniqueTitle, - metadata: {type: 'test', callback: false}, title }); } ava.failing = (fn, contextRef) => { return new Test({ + annotations: {type: 'test', callback: false, failing: true}, contextRef: contextRef || new ContextRef(), experiments, failWithoutAssertions: true, fn, registerUniqueTitle, - metadata: {type: 'test', callback: false, failing: true}, title: 'test.failing' }); }; ava.cb = (fn, contextRef) => { return new Test({ + annotations: {type: 'test', callback: true}, contextRef: contextRef || new ContextRef(), experiments, failWithoutAssertions: true, fn, registerUniqueTitle, - metadata: {type: 'test', callback: true}, title: 'test.cb' }); }; ava.cb.failing = (fn, contextRef) => { return new Test({ + annotations: {type: 'test', callback: true, failing: true}, contextRef: contextRef || new ContextRef(), experiments, failWithoutAssertions: true, fn, registerUniqueTitle, - metadata: {type: 'test', callback: true, failing: true}, title: 'test.cb.failing' }); }; diff --git a/test-tap/observable.js b/test-tap/observable.js index 6ae86bc32..db99676c7 100644 --- a/test-tap/observable.js +++ b/test-tap/observable.js @@ -8,20 +8,20 @@ const Test = require('../lib/test'); function ava(fn) { return new Test({ + annotations: {type: 'test', callback: false}, contextRef: null, failWithoutAssertions: true, fn, - metadata: {type: 'test', callback: false}, title: '[anonymous]' }); } ava.cb = function (fn) { return new Test({ + annotations: {type: 'test', callback: true}, contextRef: null, failWithoutAssertions: true, fn, - metadata: {type: 'test', callback: true}, title: '[anonymous]' }); }; diff --git a/test-tap/promise.js b/test-tap/promise.js index b38bf2444..1ab192004 100644 --- a/test-tap/promise.js +++ b/test-tap/promise.js @@ -7,20 +7,20 @@ const Test = require('../lib/test'); function ava(fn) { return new Test({ + annotations: {type: 'test', callback: false}, contextRef: null, failWithoutAssertions: true, fn, - metadata: {type: 'test', callback: false}, title: '[anonymous]' }); } ava.cb = function (fn) { return new Test({ + annotations: {type: 'test', callback: true}, contextRef: null, failWithoutAssertions: true, fn, - metadata: {type: 'test', callback: true}, title: '[anonymous]' }); }; diff --git a/test-tap/test.js b/test-tap/test.js index ab7dce4ae..532cc6c8c 100644 --- a/test-tap/test.js +++ b/test-tap/test.js @@ -162,11 +162,11 @@ test('end can be used as callback with a non-error as its error argument', t => test('title returns the test title', t => { t.plan(1); return new Test({ + annotations: {type: 'test', callback: false}, fn(a) { t.is(a.title, 'foo'); a.pass(); }, - metadata: {type: 'test', callback: false}, title: 'foo' }).run(); }); @@ -535,6 +535,7 @@ test('no crash when adding assertions after the test has ended', t => { test('contextRef', t => { new Test({ + annotations: {type: 'test'}, contextRef: { get() { return {foo: 'bar'}; @@ -546,7 +547,6 @@ test('contextRef', t => { t.strictDeepEqual(a.context, {foo: 'bar'}); t.end(); }, - metadata: {type: 'test'}, onResult() {}, title: 'foo' }).run(); @@ -678,9 +678,9 @@ test('snapshot assertion can be skipped', t => { }); return new Test({ + annotations: {}, compareTestSnapshot: options => manager.compare(options), updateSnapshots: false, - metadata: {}, title: 'passes', fn(t) { t.snapshot.skip({not: {a: 'match'}}); @@ -694,8 +694,8 @@ test('snapshot assertion can be skipped', t => { test('snapshot assertion cannot be skipped when updating snapshots', t => { return new Test({ + annotations: {}, updateSnapshots: true, - metadata: {}, title: 'passes', fn(t) { t.snapshot.skip({not: {a: 'match'}}); diff --git a/test-tap/try-snapshot.js b/test-tap/try-snapshot.js index 57a2ca1d3..b1e36d9e8 100644 --- a/test-tap/try-snapshot.js +++ b/test-tap/try-snapshot.js @@ -10,10 +10,10 @@ const ContextRef = require('../lib/context-ref'); function setup(title, manager, fn) { return new Test({ + annotations: {type: 'test', callback: false}, experiments: {}, fn, failWithoutAssertions: true, - metadata: {type: 'test', callback: false}, contextRef: new ContextRef(), registerUniqueTitle: () => true, title,