Skip to content

Commit e182490

Browse files
committed
New experimental test interfaces experiment!
With this commit, test.cb() has been removed. See #2387
1 parent 990b017 commit e182490

File tree

6 files changed

+167
-125
lines changed

6 files changed

+167
-125
lines changed

experimental.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict';
2+
const path = require('path');
3+
4+
// Ensure the same AVA install is loaded by the test file as by the test worker
5+
if (process.env.AVA_PATH && process.env.AVA_PATH !== __dirname) {
6+
module.exports = require(path.join(process.env.AVA_PATH, 'experimental.js'));
7+
} else {
8+
module.exports = require('./lib/worker/main').experimental();
9+
}

index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
if (process.env.AVA_PATH && process.env.AVA_PATH !== __dirname) {
55
module.exports = require(process.env.AVA_PATH);
66
} else {
7-
module.exports = require('./lib/worker/main');
7+
module.exports = require('./lib/worker/main').ava3();
88
}

lib/create-chain.js

+37-29
Original file line numberDiff line numberDiff line change
@@ -42,71 +42,79 @@ function callWithFlag(previous, flag, args) {
4242
} while (previous);
4343
}
4444

45-
function createHookChain(hook, isAfterHook) {
45+
function createHookChain({allowCallbacks, isAfterHook = false}, hook) {
4646
// Hook chaining rules:
4747
// * `always` comes immediately after "after hooks"
4848
// * `skip` must come at the end
4949
// * no `only`
5050
// * no repeating
51-
extendChain(hook, 'cb', 'callback');
5251
extendChain(hook, 'skip', 'skipped');
53-
extendChain(hook.cb, 'skip', 'skipped');
5452
if (isAfterHook) {
5553
extendChain(hook, 'always');
56-
extendChain(hook.always, 'cb', 'callback');
5754
extendChain(hook.always, 'skip', 'skipped');
58-
extendChain(hook.always.cb, 'skip', 'skipped');
55+
}
56+
57+
if (allowCallbacks) {
58+
extendChain(hook, 'cb', 'callback');
59+
extendChain(hook.cb, 'skip', 'skipped');
60+
if (isAfterHook) {
61+
extendChain(hook.always, 'cb', 'callback');
62+
extendChain(hook.always.cb, 'skip', 'skipped');
63+
}
5964
}
6065

6166
return hook;
6267
}
6368

64-
function createChain(fn, defaults, meta) {
69+
function createChain({allowCallbacks = true, declare, defaults, meta}) {
6570
// Test chaining rules:
6671
// * `serial` must come at the start
6772
// * `only` and `skip` must come at the end
6873
// * `failing` must come at the end, but can be followed by `only` and `skip`
6974
// * `only` and `skip` cannot be chained together
7075
// * no repeating
71-
const root = startChain('test', fn, {...defaults, type: 'test'});
72-
extendChain(root, 'cb', 'callback');
76+
const root = startChain('test', declare, {...defaults, type: 'test'});
7377
extendChain(root, 'failing');
7478
extendChain(root, 'only', 'exclusive');
7579
extendChain(root, 'serial');
7680
extendChain(root, 'skip', 'skipped');
77-
extendChain(root.cb, 'failing');
78-
extendChain(root.cb, 'only', 'exclusive');
79-
extendChain(root.cb, 'skip', 'skipped');
80-
extendChain(root.cb.failing, 'only', 'exclusive');
81-
extendChain(root.cb.failing, 'skip', 'skipped');
8281
extendChain(root.failing, 'only', 'exclusive');
8382
extendChain(root.failing, 'skip', 'skipped');
84-
extendChain(root.serial, 'cb', 'callback');
8583
extendChain(root.serial, 'failing');
8684
extendChain(root.serial, 'only', 'exclusive');
8785
extendChain(root.serial, 'skip', 'skipped');
88-
extendChain(root.serial.cb, 'failing');
89-
extendChain(root.serial.cb, 'only', 'exclusive');
90-
extendChain(root.serial.cb, 'skip', 'skipped');
91-
extendChain(root.serial.cb.failing, 'only', 'exclusive');
92-
extendChain(root.serial.cb.failing, 'skip', 'skipped');
9386
extendChain(root.serial.failing, 'only', 'exclusive');
9487
extendChain(root.serial.failing, 'skip', 'skipped');
9588

96-
root.after = createHookChain(startChain('test.after', fn, {...defaults, type: 'after'}), true);
97-
root.afterEach = createHookChain(startChain('test.afterEach', fn, {...defaults, type: 'afterEach'}), true);
98-
root.before = createHookChain(startChain('test.before', fn, {...defaults, type: 'before'}), false);
99-
root.beforeEach = createHookChain(startChain('test.beforeEach', fn, {...defaults, type: 'beforeEach'}), false);
89+
if (allowCallbacks) {
90+
extendChain(root, 'cb', 'callback');
91+
extendChain(root.cb, 'failing');
92+
extendChain(root.cb, 'only', 'exclusive');
93+
extendChain(root.cb, 'skip', 'skipped');
94+
extendChain(root.cb.failing, 'only', 'exclusive');
95+
extendChain(root.cb.failing, 'skip', 'skipped');
96+
extendChain(root.serial, 'cb', 'callback');
97+
extendChain(root.serial.cb, 'failing');
98+
extendChain(root.serial.cb, 'only', 'exclusive');
99+
extendChain(root.serial.cb, 'skip', 'skipped');
100+
extendChain(root.serial.cb.failing, 'only', 'exclusive');
101+
extendChain(root.serial.cb.failing, 'skip', 'skipped');
102+
}
103+
104+
root.after = createHookChain({allowCallbacks, isAfterHook: true}, startChain('test.after', declare, {...defaults, type: 'after'}));
105+
root.afterEach = createHookChain({allowCallbacks, isAfterHook: true}, startChain('test.afterEach', declare, {...defaults, type: 'afterEach'}));
106+
root.before = createHookChain({allowCallbacks}, startChain('test.before', declare, {...defaults, type: 'before'}));
107+
root.beforeEach = createHookChain({allowCallbacks}, startChain('test.beforeEach', declare, {...defaults, type: 'beforeEach'}));
100108

101-
root.serial.after = createHookChain(startChain('test.after', fn, {...defaults, serial: true, type: 'after'}), true);
102-
root.serial.afterEach = createHookChain(startChain('test.afterEach', fn, {...defaults, serial: true, type: 'afterEach'}), true);
103-
root.serial.before = createHookChain(startChain('test.before', fn, {...defaults, serial: true, type: 'before'}), false);
104-
root.serial.beforeEach = createHookChain(startChain('test.beforeEach', fn, {...defaults, serial: true, type: 'beforeEach'}), false);
109+
root.serial.after = createHookChain({allowCallbacks, isAfterHook: true}, startChain('test.after', declare, {...defaults, serial: true, type: 'after'}));
110+
root.serial.afterEach = createHookChain({allowCallbacks, isAfterHook: true}, startChain('test.afterEach', declare, {...defaults, serial: true, type: 'afterEach'}));
111+
root.serial.before = createHookChain({allowCallbacks}, startChain('test.before', declare, {...defaults, serial: true, type: 'before'}));
112+
root.serial.beforeEach = createHookChain({allowCallbacks}, startChain('test.beforeEach', declare, {...defaults, serial: true, type: 'beforeEach'}));
105113

106114
// "todo" tests cannot be chained. Allow todo tests to be flagged as needing
107115
// to be serial.
108-
root.todo = startChain('test.todo', fn, {...defaults, type: 'test', todo: true});
109-
root.serial.todo = startChain('test.serial.todo', fn, {...defaults, serial: true, type: 'test', todo: true});
116+
root.todo = startChain('test.todo', declare, {...defaults, type: 'test', todo: true});
117+
root.serial.todo = startChain('test.serial.todo', declare, {...defaults, serial: true, type: 'test', todo: true});
110118

111119
root.meta = meta;
112120

lib/load-config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const pkgConf = require('pkg-conf');
77

88
const NO_SUCH_FILE = Symbol('no ava.config.js file');
99
const MISSING_DEFAULT_EXPORT = Symbol('missing default export');
10-
const EXPERIMENTS = new Set();
10+
const EXPERIMENTS = new Set(['experimentalTestInterfaces']);
1111

1212
// *Very* rudimentary support for loading ava.config.js files containing an `export default` statement.
1313
const evaluateJsConfig = configFile => {

lib/runner.js

+104-89
Original file line numberDiff line numberDiff line change
@@ -53,117 +53,132 @@ class Runner extends Emittery {
5353

5454
let hasStarted = false;
5555
let scheduledStart = false;
56-
const meta = Object.freeze({
57-
file: options.file
58-
});
59-
this.chain = createChain((metadata, testArgs) => { // eslint-disable-line complexity
60-
if (hasStarted) {
61-
throw new Error('All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.');
62-
}
63-
64-
if (!scheduledStart) {
65-
scheduledStart = true;
66-
process.nextTick(() => {
67-
hasStarted = true;
68-
this.start();
69-
});
70-
}
71-
72-
const {args, buildTitle, implementations, rawTitle} = parseTestArgs(testArgs);
73-
74-
if (metadata.todo) {
75-
if (implementations.length > 0) {
76-
throw new TypeError('`todo` tests are not allowed to have an implementation. Use `test.skip()` for tests with an implementation.');
56+
const chainOptions = {
57+
declare: (metadata, testArgs) => { // eslint-disable-line complexity
58+
if (hasStarted) {
59+
throw new Error('All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.');
7760
}
7861

79-
if (!rawTitle) { // Either undefined or a string.
80-
throw new TypeError('`todo` tests require a title');
62+
if (!scheduledStart) {
63+
scheduledStart = true;
64+
process.nextTick(() => {
65+
hasStarted = true;
66+
this.start();
67+
});
8168
}
8269

83-
if (!this.registerUniqueTitle(rawTitle)) {
84-
throw new Error(`Duplicate test title: ${rawTitle}`);
85-
}
70+
const {args, buildTitle, implementations, rawTitle} = parseTestArgs(testArgs);
8671

87-
if (this.match.length > 0) {
88-
// --match selects TODO tests.
89-
if (matcher([rawTitle], this.match).length === 1) {
90-
metadata.exclusive = true;
91-
this.runOnlyExclusive = true;
72+
if (metadata.todo) {
73+
if (implementations.length > 0) {
74+
throw new TypeError('`todo` tests are not allowed to have an implementation. Use `test.skip()` for tests with an implementation.');
9275
}
93-
}
9476

95-
this.tasks.todo.push({title: rawTitle, metadata});
96-
this.emit('stateChange', {
97-
type: 'declared-test',
98-
title: rawTitle,
99-
knownFailing: false,
100-
todo: true
101-
});
102-
} else {
103-
if (implementations.length === 0) {
104-
throw new TypeError('Expected an implementation. Use `test.todo()` for tests without an implementation.');
105-
}
106-
107-
for (const implementation of implementations) {
108-
let {title, isSet, isValid, isEmpty} = buildTitle(implementation);
77+
if (!rawTitle) { // Either undefined or a string.
78+
throw new TypeError('`todo` tests require a title');
79+
}
10980

110-
if (isSet && !isValid) {
111-
throw new TypeError('Test & hook titles must be strings');
81+
if (!this.registerUniqueTitle(rawTitle)) {
82+
throw new Error(`Duplicate test title: ${rawTitle}`);
11283
}
11384

114-
if (isEmpty) {
115-
if (metadata.type === 'test') {
116-
throw new TypeError('Tests must have a title');
117-
} else if (metadata.always) {
118-
title = `${metadata.type}.always hook`;
119-
} else {
120-
title = `${metadata.type} hook`;
85+
if (this.match.length > 0) {
86+
// --match selects TODO tests.
87+
if (matcher([rawTitle], this.match).length === 1) {
88+
metadata.exclusive = true;
89+
this.runOnlyExclusive = true;
12190
}
12291
}
12392

124-
if (metadata.type === 'test' && !this.registerUniqueTitle(title)) {
125-
throw new Error(`Duplicate test title: ${title}`);
93+
this.tasks.todo.push({title: rawTitle, metadata});
94+
this.emit('stateChange', {
95+
type: 'declared-test',
96+
title: rawTitle,
97+
knownFailing: false,
98+
todo: true
99+
});
100+
} else {
101+
if (implementations.length === 0) {
102+
throw new TypeError('Expected an implementation. Use `test.todo()` for tests without an implementation.');
126103
}
127104

128-
const task = {
129-
title,
130-
implementation,
131-
args,
132-
metadata: {...metadata}
133-
};
134-
135-
if (metadata.type === 'test') {
136-
if (this.match.length > 0) {
137-
// --match overrides .only()
138-
task.metadata.exclusive = matcher([title], this.match).length === 1;
105+
for (const implementation of implementations) {
106+
let {title, isSet, isValid, isEmpty} = buildTitle(implementation);
107+
108+
if (isSet && !isValid) {
109+
throw new TypeError('Test & hook titles must be strings');
139110
}
140111

141-
if (task.metadata.exclusive) {
142-
this.runOnlyExclusive = true;
112+
if (isEmpty) {
113+
if (metadata.type === 'test') {
114+
throw new TypeError('Tests must have a title');
115+
} else if (metadata.always) {
116+
title = `${metadata.type}.always hook`;
117+
} else {
118+
title = `${metadata.type} hook`;
119+
}
120+
}
121+
122+
if (metadata.type === 'test' && !this.registerUniqueTitle(title)) {
123+
throw new Error(`Duplicate test title: ${title}`);
143124
}
144125

145-
this.tasks[metadata.serial ? 'serial' : 'concurrent'].push(task);
146-
this.emit('stateChange', {
147-
type: 'declared-test',
126+
const task = {
148127
title,
149-
knownFailing: metadata.failing,
150-
todo: false
151-
});
152-
} else if (!metadata.skipped) {
153-
this.tasks[metadata.type + (metadata.always ? 'Always' : '')].push(task);
128+
implementation,
129+
args,
130+
metadata: {...metadata}
131+
};
132+
133+
if (metadata.type === 'test') {
134+
if (this.match.length > 0) {
135+
// --match overrides .only()
136+
task.metadata.exclusive = matcher([title], this.match).length === 1;
137+
}
138+
139+
if (task.metadata.exclusive) {
140+
this.runOnlyExclusive = true;
141+
}
142+
143+
this.tasks[metadata.serial ? 'serial' : 'concurrent'].push(task);
144+
this.emit('stateChange', {
145+
type: 'declared-test',
146+
title,
147+
knownFailing: metadata.failing,
148+
todo: false
149+
});
150+
} else if (!metadata.skipped) {
151+
this.tasks[metadata.type + (metadata.always ? 'Always' : '')].push(task);
152+
}
154153
}
155154
}
156-
}
157-
}, {
158-
serial: false,
159-
exclusive: false,
160-
skipped: false,
161-
todo: false,
162-
failing: false,
163-
callback: false,
164-
inline: false, // Set for attempt metadata created by `t.try()`
165-
always: false
166-
}, meta);
155+
},
156+
defaults: {
157+
always: false,
158+
callback: false,
159+
exclusive: false,
160+
failing: false,
161+
inline: false, // Set for attempt metadata created by `t.try()`
162+
serial: false,
163+
skipped: false,
164+
todo: false
165+
},
166+
meta: Object.freeze({
167+
file: options.file
168+
})
169+
};
170+
171+
this.chain = createChain({
172+
allowCallbacks: true,
173+
...chainOptions
174+
});
175+
176+
if (this.experiments.experimentalTestInterfaces) {
177+
this.experimentalChain = createChain({
178+
allowCallbacks: false,
179+
...chainOptions
180+
});
181+
}
167182
}
168183

169184
compareTestSnapshot(options) {

lib/worker/main.js

+15-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
'use strict';
22
const runner = require('./subprocess').getRunner();
33

4-
const makeCjsExport = () => {
4+
const makeCjsExport = (chain = runner.chain) => {
55
function test(...args) {
6-
return runner.chain(...args);
6+
return chain(...args);
77
}
88

9-
return Object.assign(test, runner.chain);
9+
return Object.assign(test, chain);
1010
};
1111

1212
// Support CommonJS modules by exporting a test function that can be fully
1313
// chained. Also support ES module loaders by exporting __esModule and a
1414
// default.
15-
module.exports = Object.assign(makeCjsExport(), {
15+
exports.ava3 = () => Object.assign(makeCjsExport(), {
1616
__esModule: true,
17-
default: runner.chain
17+
default: makeCjsExport()
1818
});
19+
20+
// Only export a test function that can be fully chained. This will be the
21+
// behavior in AVA 4.
22+
exports.experimental = () => {
23+
if (!runner.experimentalChain) {
24+
throw new Error('You must enable the ’experimentalTestInterfaces’ experiment');
25+
}
26+
27+
return makeCjsExport(runner.experimentalChain);
28+
};

0 commit comments

Comments
 (0)