ava 3.15.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/entrypoints/cli.mjs +4 -0
  2. package/entrypoints/eslint-plugin-helper.cjs +109 -0
  3. package/entrypoints/main.cjs +2 -0
  4. package/entrypoints/main.mjs +1 -0
  5. package/entrypoints/plugin.cjs +2 -0
  6. package/entrypoints/plugin.mjs +4 -0
  7. package/index.d.ts +6 -816
  8. package/lib/api.js +108 -49
  9. package/lib/assert.js +255 -270
  10. package/lib/chalk.js +9 -14
  11. package/lib/cli.js +118 -112
  12. package/lib/code-excerpt.js +12 -17
  13. package/lib/concordance-options.js +29 -65
  14. package/lib/context-ref.js +3 -6
  15. package/lib/create-chain.js +32 -20
  16. package/lib/environment-variables.js +1 -4
  17. package/lib/eslint-plugin-helper-worker.js +73 -0
  18. package/lib/extensions.js +2 -2
  19. package/lib/fork.js +81 -84
  20. package/lib/glob-helpers.cjs +140 -0
  21. package/lib/globs.js +136 -163
  22. package/lib/{ipc-flow-control.js → ipc-flow-control.cjs} +1 -0
  23. package/lib/is-ci.js +4 -2
  24. package/lib/like-selector.js +7 -13
  25. package/lib/line-numbers.js +11 -18
  26. package/lib/load-config.js +56 -180
  27. package/lib/module-types.js +3 -7
  28. package/lib/node-arguments.js +4 -5
  29. package/lib/{now-and-timers.js → now-and-timers.cjs} +0 -0
  30. package/lib/parse-test-args.js +22 -11
  31. package/lib/pkg.cjs +2 -0
  32. package/lib/plugin-support/shared-worker-loader.js +45 -48
  33. package/lib/plugin-support/shared-workers.js +24 -46
  34. package/lib/provider-manager.js +20 -14
  35. package/lib/reporters/beautify-stack.js +6 -12
  36. package/lib/reporters/colors.js +40 -15
  37. package/lib/reporters/default.js +114 -364
  38. package/lib/reporters/format-serialized-error.js +7 -18
  39. package/lib/reporters/improper-usage-messages.js +8 -9
  40. package/lib/reporters/prefix-title.js +17 -15
  41. package/lib/reporters/tap.js +18 -25
  42. package/lib/run-status.js +29 -23
  43. package/lib/runner.js +157 -172
  44. package/lib/scheduler.js +53 -0
  45. package/lib/serialize-error.js +61 -64
  46. package/lib/snapshot-manager.js +271 -289
  47. package/lib/test.js +135 -291
  48. package/lib/watcher.js +69 -44
  49. package/lib/worker/base.js +208 -0
  50. package/lib/worker/channel.cjs +290 -0
  51. package/lib/worker/dependency-tracker.js +24 -23
  52. package/lib/worker/{ensure-forked.js → guard-environment.cjs} +5 -4
  53. package/lib/worker/line-numbers.js +58 -20
  54. package/lib/worker/main.cjs +12 -0
  55. package/lib/worker/{options.js → options.cjs} +0 -0
  56. package/lib/worker/{plugin.js → plugin.cjs} +30 -21
  57. package/lib/worker/state.cjs +5 -0
  58. package/lib/worker/utils.cjs +6 -0
  59. package/package.json +71 -68
  60. package/plugin.d.ts +51 -53
  61. package/readme.md +5 -13
  62. package/types/assertions.d.ts +327 -0
  63. package/types/subscribable.ts +6 -0
  64. package/types/test-fn.d.ts +231 -0
  65. package/types/try-fn.d.ts +58 -0
  66. package/cli.js +0 -11
  67. package/eslint-plugin-helper.js +0 -201
  68. package/index.js +0 -8
  69. package/lib/worker/ipc.js +0 -201
  70. package/lib/worker/main.js +0 -21
  71. package/lib/worker/subprocess.js +0 -266
  72. package/plugin.js +0 -9
package/lib/runner.js CHANGED
@@ -1,14 +1,19 @@
1
- 'use strict';
2
- const Emittery = require('emittery');
3
- const matcher = require('matcher');
4
- const ContextRef = require('./context-ref');
5
- const createChain = require('./create-chain');
6
- const parseTestArgs = require('./parse-test-args');
7
- const snapshotManager = require('./snapshot-manager');
8
- const serializeError = require('./serialize-error');
9
- const Runnable = require('./test');
10
-
11
- class Runner extends Emittery {
1
+ import process from 'node:process';
2
+ import {pathToFileURL} from 'node:url';
3
+
4
+ import Emittery from 'emittery';
5
+ import {matcher} from 'matcher';
6
+
7
+ import ContextRef from './context-ref.js';
8
+ import createChain from './create-chain.js';
9
+ import parseTestArgs from './parse-test-args.js';
10
+ import serializeError from './serialize-error.js';
11
+ import {load as loadSnapshots, determineSnapshotDir} from './snapshot-manager.js';
12
+ import Runnable from './test.js';
13
+ import {waitForReady} from './worker/state.cjs';
14
+
15
+ const makeFileURL = file => file.startsWith('file://') ? file : pathToFileURL(file).toString();
16
+ export default class Runner extends Emittery {
12
17
  constructor(options = {}) {
13
18
  super();
14
19
 
@@ -18,21 +23,18 @@ class Runner extends Emittery {
18
23
  this.file = options.file;
19
24
  this.checkSelectedByLineNumbers = options.checkSelectedByLineNumbers;
20
25
  this.match = options.match || [];
21
- this.powerAssert = undefined; // Assigned later.
22
26
  this.projectDir = options.projectDir;
23
27
  this.recordNewSnapshots = options.recordNewSnapshots === true;
24
28
  this.runOnlyExclusive = options.runOnlyExclusive === true;
25
29
  this.serial = options.serial === true;
26
- this.skippingTests = false;
27
30
  this.snapshotDir = options.snapshotDir;
28
31
  this.updateSnapshots = options.updateSnapshots;
29
32
 
30
33
  this.activeRunnables = new Set();
31
34
  this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this);
32
- this.skippedSnapshots = false;
33
35
  this.boundSkipSnapshot = this.skipSnapshot.bind(this);
34
36
  this.interrupted = false;
35
- this.snapshots = null;
37
+
36
38
  this.nextTaskIndex = 0;
37
39
  this.tasks = {
38
40
  after: [],
@@ -43,9 +45,9 @@ class Runner extends Emittery {
43
45
  beforeEach: [],
44
46
  concurrent: [],
45
47
  serial: [],
46
- todo: []
48
+ todo: [],
47
49
  };
48
- this.waitForReady = [];
50
+ this.waitForReady = waitForReady;
49
51
 
50
52
  const uniqueTestTitles = new Set();
51
53
  this.registerUniqueTitle = title => {
@@ -57,14 +59,21 @@ class Runner extends Emittery {
57
59
  return true;
58
60
  };
59
61
 
62
+ this.notifyTimeoutUpdate = timeoutMs => {
63
+ this.emit('stateChange', {
64
+ type: 'test-timeout-configured',
65
+ period: timeoutMs,
66
+ });
67
+ };
68
+
60
69
  let hasStarted = false;
61
70
  let scheduledStart = false;
62
71
  const meta = Object.freeze({
63
- file: options.file,
72
+ file: makeFileURL(options.file),
64
73
  get snapshotDirectory() {
65
74
  const {file, snapshotDir: fixedLocation, projectDir} = options;
66
- return snapshotManager.determineSnapshotDir({file, fixedLocation, projectDir});
67
- }
75
+ return makeFileURL(determineSnapshotDir({file, fixedLocation, projectDir}));
76
+ },
68
77
  });
69
78
  this.chain = createChain((metadata, testArgs) => { // eslint-disable-line complexity
70
79
  if (hasStarted) {
@@ -81,97 +90,95 @@ class Runner extends Emittery {
81
90
 
82
91
  metadata.taskIndex = this.nextTaskIndex++;
83
92
 
84
- const {args, buildTitle, implementations, rawTitle} = parseTestArgs(testArgs);
93
+ const {args, implementation, title} = parseTestArgs(testArgs);
85
94
 
86
95
  if (this.checkSelectedByLineNumbers) {
87
96
  metadata.selected = this.checkSelectedByLineNumbers();
88
97
  }
89
98
 
90
99
  if (metadata.todo) {
91
- if (implementations.length > 0) {
100
+ if (implementation) {
92
101
  throw new TypeError('`todo` tests are not allowed to have an implementation. Use `test.skip()` for tests with an implementation.');
93
102
  }
94
103
 
95
- if (!rawTitle) { // Either undefined or a string.
104
+ if (!title.raw) { // Either undefined or a string.
96
105
  throw new TypeError('`todo` tests require a title');
97
106
  }
98
107
 
99
- if (!this.registerUniqueTitle(rawTitle)) {
100
- throw new Error(`Duplicate test title: ${rawTitle}`);
108
+ if (!this.registerUniqueTitle(title.value)) {
109
+ throw new Error(`Duplicate test title: ${title.value}`);
101
110
  }
102
111
 
103
- if (this.match.length > 0) {
104
- // --match selects TODO tests.
105
- if (matcher([rawTitle], this.match).length === 1) {
106
- metadata.exclusive = true;
107
- this.runOnlyExclusive = true;
108
- }
112
+ // --match selects TODO tests.
113
+ if (this.match.length > 0 && matcher(title.value, this.match).length === 1) {
114
+ metadata.exclusive = true;
115
+ this.runOnlyExclusive = true;
109
116
  }
110
117
 
111
- this.tasks.todo.push({title: rawTitle, metadata});
118
+ this.tasks.todo.push({title: title.value, metadata});
112
119
  this.emit('stateChange', {
113
120
  type: 'declared-test',
114
- title: rawTitle,
121
+ title: title.value,
115
122
  knownFailing: false,
116
- todo: true
123
+ todo: true,
117
124
  });
118
125
  } else {
119
- if (implementations.length === 0) {
126
+ if (!implementation) {
120
127
  throw new TypeError('Expected an implementation. Use `test.todo()` for tests without an implementation.');
121
128
  }
122
129
 
123
- for (const implementation of implementations) {
124
- let {title, isSet, isValid, isEmpty} = buildTitle(implementation);
130
+ if (Array.isArray(implementation)) {
131
+ throw new TypeError('AVA 4 no longer supports multiple implementations.');
132
+ }
133
+
134
+ if (title.isSet && !title.isValid) {
135
+ throw new TypeError('Test & hook titles must be strings');
136
+ }
125
137
 
126
- if (isSet && !isValid) {
127
- throw new TypeError('Test & hook titles must be strings');
138
+ let fallbackTitle = title.value;
139
+ if (title.isEmpty) {
140
+ if (metadata.type === 'test') {
141
+ throw new TypeError('Tests must have a title');
142
+ } else if (metadata.always) {
143
+ fallbackTitle = `${metadata.type}.always hook`;
144
+ } else {
145
+ fallbackTitle = `${metadata.type} hook`;
128
146
  }
147
+ }
129
148
 
130
- if (isEmpty) {
131
- if (metadata.type === 'test') {
132
- throw new TypeError('Tests must have a title');
133
- } else if (metadata.always) {
134
- title = `${metadata.type}.always hook`;
135
- } else {
136
- title = `${metadata.type} hook`;
137
- }
149
+ if (metadata.type === 'test' && !this.registerUniqueTitle(title.value)) {
150
+ throw new Error(`Duplicate test title: ${title.value}`);
151
+ }
152
+
153
+ const task = {
154
+ title: title.value || fallbackTitle,
155
+ implementation,
156
+ args,
157
+ metadata: {...metadata},
158
+ };
159
+
160
+ if (metadata.type === 'test') {
161
+ if (this.match.length > 0) {
162
+ // --match overrides .only()
163
+ task.metadata.exclusive = matcher(title.value, this.match).length === 1;
138
164
  }
139
165
 
140
- if (metadata.type === 'test' && !this.registerUniqueTitle(title)) {
141
- throw new Error(`Duplicate test title: ${title}`);
166
+ if (task.metadata.exclusive) {
167
+ this.runOnlyExclusive = true;
142
168
  }
143
169
 
144
- const task = {
145
- title,
146
- implementation,
147
- args,
148
- metadata: {...metadata}
149
- };
170
+ this.tasks[metadata.serial ? 'serial' : 'concurrent'].push(task);
150
171
 
151
- if (metadata.type === 'test') {
152
- if (this.match.length > 0) {
153
- // --match overrides .only()
154
- task.metadata.exclusive = matcher([title], this.match).length === 1;
155
- }
156
-
157
- if (task.metadata.skipped) {
158
- this.skippingTests = true;
159
- }
160
-
161
- if (task.metadata.exclusive) {
162
- this.runOnlyExclusive = true;
163
- }
164
-
165
- this.tasks[metadata.serial ? 'serial' : 'concurrent'].push(task);
166
- this.emit('stateChange', {
167
- type: 'declared-test',
168
- title,
169
- knownFailing: metadata.failing,
170
- todo: false
171
- });
172
- } else if (!metadata.skipped) {
173
- this.tasks[metadata.type + (metadata.always ? 'Always' : '')].push(task);
174
- }
172
+ this.snapshots.touch(title.value, metadata.taskIndex);
173
+
174
+ this.emit('stateChange', {
175
+ type: 'declared-test',
176
+ title: title.value,
177
+ knownFailing: metadata.failing,
178
+ todo: false,
179
+ });
180
+ } else if (!metadata.skipped) {
181
+ this.tasks[metadata.type + (metadata.always ? 'Always' : '')].push(task);
175
182
  }
176
183
  }
177
184
  }, {
@@ -182,54 +189,43 @@ class Runner extends Emittery {
182
189
  failing: false,
183
190
  callback: false,
184
191
  inline: false, // Set for attempt metadata created by `t.try()`
185
- always: false
192
+ always: false,
186
193
  }, meta);
187
194
  }
188
195
 
189
- compareTestSnapshot(options) {
190
- if (!this.snapshots) {
191
- this.snapshots = snapshotManager.load({
192
- file: this.file,
193
- fixedLocation: this.snapshotDir,
194
- projectDir: this.projectDir,
195
- recordNewSnapshots: this.recordNewSnapshots,
196
- updating: this.updateSnapshots && !this.runOnlyExclusive && !this.skippingTests
197
- });
198
- this.emit('dependency', this.snapshots.snapPath);
196
+ get snapshots() {
197
+ if (this._snapshots) {
198
+ return this._snapshots;
199
+ }
200
+
201
+ // Lazy load not when the runner is instantiated but when snapshots are
202
+ // needed. This should be after the test file has been loaded and source
203
+ // maps are available.
204
+ const snapshots = loadSnapshots({
205
+ file: this.file,
206
+ fixedLocation: this.snapshotDir,
207
+ projectDir: this.projectDir,
208
+ recordNewSnapshots: this.recordNewSnapshots,
209
+ updating: this.updateSnapshots,
210
+ });
211
+ if (snapshots.snapPath !== undefined) {
212
+ this.emit('dependency', snapshots.snapPath);
199
213
  }
200
214
 
215
+ this._snapshots = snapshots;
216
+ return snapshots;
217
+ }
218
+
219
+ compareTestSnapshot(options) {
201
220
  return this.snapshots.compare(options);
202
221
  }
203
222
 
204
- skipSnapshot() {
205
- this.skippedSnapshots = true;
223
+ skipSnapshot(options) {
224
+ return this.snapshots.skipSnapshot(options);
206
225
  }
207
226
 
208
227
  saveSnapshotState() {
209
- if (
210
- this.updateSnapshots &&
211
- (
212
- this.runOnlyExclusive ||
213
- this.skippingTests ||
214
- this.skippedSnapshots
215
- )
216
- ) {
217
- return {cannotSave: true};
218
- }
219
-
220
- if (this.snapshots) {
221
- return {touchedFiles: this.snapshots.save()};
222
- }
223
-
224
- if (this.updateSnapshots) {
225
- return {touchedFiles: snapshotManager.cleanSnapshots({
226
- file: this.file,
227
- fixedLocation: this.snapshotDir,
228
- projectDir: this.projectDir
229
- })};
230
- }
231
-
232
- return {};
228
+ return {touchedFiles: this.snapshots.save()};
233
229
  }
234
230
 
235
231
  onRun(runnable) {
@@ -240,16 +236,6 @@ class Runner extends Emittery {
240
236
  this.activeRunnables.delete(runnable);
241
237
  }
242
238
 
243
- attributeLeakedError(err) {
244
- for (const runnable of this.activeRunnables) {
245
- if (runnable.attributeLeakedError(err)) {
246
- return true;
247
- }
248
- }
249
-
250
- return false;
251
- }
252
-
253
239
  beforeExitHandler() {
254
240
  for (const runnable of this.activeRunnables) {
255
241
  runnable.finishDueToInactivity();
@@ -269,25 +255,25 @@ class Runner extends Emittery {
269
255
  };
270
256
 
271
257
  let waitForSerial = Promise.resolve();
272
- await runnables.reduce((previous, runnable) => { // eslint-disable-line unicorn/no-reduce
258
+ await runnables.reduce((previous, runnable) => { // eslint-disable-line unicorn/no-array-reduce
273
259
  if (runnable.metadata.serial || this.serial) {
274
- waitForSerial = previous.then(() => {
260
+ waitForSerial = previous.then(() =>
275
261
  // Serial runnables run as long as there was no previous failure, unless
276
262
  // the runnable should always be run.
277
- return (allPassed || runnable.metadata.always) && runAndStoreResult(runnable);
278
- });
263
+ (allPassed || runnable.metadata.always) && runAndStoreResult(runnable),
264
+ );
279
265
  return waitForSerial;
280
266
  }
281
267
 
282
268
  return Promise.all([
283
269
  previous,
284
- waitForSerial.then(() => {
270
+ waitForSerial.then(() =>
285
271
  // Concurrent runnables are kicked off after the previous serial
286
272
  // runnables have completed, as long as there was no previous failure
287
273
  // (or if the runnable should always be run). One concurrent runnable's
288
274
  // failure does not prevent the next runnable from running.
289
- return (allPassed || runnable.metadata.always) && runAndStoreResult(runnable);
290
- })
275
+ (allPassed || runnable.metadata.always) && runAndStoreResult(runnable),
276
+ ),
291
277
  ]);
292
278
  }, waitForSerial);
293
279
 
@@ -303,22 +289,22 @@ class Runner extends Emittery {
303
289
  return result;
304
290
  }
305
291
 
306
- async runHooks(tasks, contextRef, {titleSuffix, testPassed, associatedTaskIndex} = {}) {
292
+ async runHooks(tasks, contextRef, {titleSuffix, testPassed} = {}) {
307
293
  const hooks = tasks.map(task => new Runnable({
308
294
  contextRef,
309
295
  experiments: this.experiments,
310
296
  failWithoutAssertions: false,
311
- fn: task.args.length === 0 ?
312
- task.implementation :
313
- t => task.implementation.apply(null, [t].concat(task.args)),
297
+ fn: task.args.length === 0
298
+ ? task.implementation
299
+ : t => Reflect.apply(task.implementation, null, [t, ...task.args]),
314
300
  compareTestSnapshot: this.boundCompareTestSnapshot,
315
301
  skipSnapshot: this.boundSkipSnapshot,
316
302
  updateSnapshots: this.updateSnapshots,
317
- metadata: {...task.metadata, associatedTaskIndex},
318
- powerAssert: this.powerAssert,
303
+ metadata: task.metadata,
319
304
  title: `${task.title}${titleSuffix || ''}`,
320
305
  isHook: true,
321
- testPassed
306
+ testPassed,
307
+ notifyTimeoutUpdate: this.notifyTimeoutUpdate,
322
308
  }));
323
309
  const outcome = await this.runMultiple(hooks, this.serial);
324
310
  for (const result of outcome.storedResults) {
@@ -327,7 +313,7 @@ class Runner extends Emittery {
327
313
  type: 'hook-finished',
328
314
  title: result.title,
329
315
  duration: result.duration,
330
- logs: result.logs
316
+ logs: result.logs,
331
317
  });
332
318
  } else {
333
319
  this.emit('stateChange', {
@@ -335,7 +321,7 @@ class Runner extends Emittery {
335
321
  title: result.title,
336
322
  err: serializeError('Hook failure', true, result.error),
337
323
  duration: result.duration,
338
- logs: result.logs
324
+ logs: result.logs,
339
325
  });
340
326
  }
341
327
  }
@@ -350,8 +336,7 @@ class Runner extends Emittery {
350
336
  contextRef,
351
337
  {
352
338
  titleSuffix: hookSuffix,
353
- associatedTaskIndex: task.metadata.taskIndex
354
- }
339
+ },
355
340
  );
356
341
 
357
342
  let testOk = false;
@@ -361,16 +346,16 @@ class Runner extends Emittery {
361
346
  contextRef,
362
347
  experiments: this.experiments,
363
348
  failWithoutAssertions: this.failWithoutAssertions,
364
- fn: task.args.length === 0 ?
365
- task.implementation :
366
- t => task.implementation.apply(null, [t].concat(task.args)),
349
+ fn: task.args.length === 0
350
+ ? task.implementation
351
+ : t => Reflect.apply(task.implementation, null, [t, ...task.args]),
367
352
  compareTestSnapshot: this.boundCompareTestSnapshot,
368
353
  skipSnapshot: this.boundSkipSnapshot,
369
354
  updateSnapshots: this.updateSnapshots,
370
355
  metadata: task.metadata,
371
- powerAssert: this.powerAssert,
372
356
  title: task.title,
373
- registerUniqueTitle: this.registerUniqueTitle
357
+ registerUniqueTitle: this.registerUniqueTitle,
358
+ notifyTimeoutUpdate: this.notifyTimeoutUpdate,
374
359
  });
375
360
 
376
361
  const result = await this.runSingle(test);
@@ -382,7 +367,7 @@ class Runner extends Emittery {
382
367
  title: result.title,
383
368
  duration: result.duration,
384
369
  knownFailing: result.metadata.failing,
385
- logs: result.logs
370
+ logs: result.logs,
386
371
  });
387
372
 
388
373
  hooksOk = await this.runHooks(
@@ -391,7 +376,6 @@ class Runner extends Emittery {
391
376
  {
392
377
  titleSuffix: hookSuffix,
393
378
  testPassed: testOk,
394
- associatedTaskIndex: task.metadata.taskIndex
395
379
  });
396
380
  } else {
397
381
  this.emit('stateChange', {
@@ -400,7 +384,7 @@ class Runner extends Emittery {
400
384
  err: serializeError('Test failure', true, result.error, this.file),
401
385
  duration: result.duration,
402
386
  knownFailing: result.metadata.failing,
403
- logs: result.logs
387
+ logs: result.logs,
404
388
  });
405
389
  // Don't run `afterEach` hooks if the test failed.
406
390
  }
@@ -412,20 +396,21 @@ class Runner extends Emittery {
412
396
  {
413
397
  titleSuffix: hookSuffix,
414
398
  testPassed: testOk,
415
- associatedTaskIndex: task.metadata.taskIndex
416
399
  });
417
400
  return alwaysOk && hooksOk && testOk;
418
401
  }
419
402
 
420
- async start() {
403
+ async start() { // eslint-disable-line complexity
421
404
  const concurrentTests = [];
422
405
  const serialTests = [];
423
406
  for (const task of this.tasks.serial) {
424
407
  if (this.runOnlyExclusive && !task.metadata.exclusive) {
408
+ this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
425
409
  continue;
426
410
  }
427
411
 
428
412
  if (this.checkSelectedByLineNumbers && !task.metadata.selected) {
413
+ this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
429
414
  continue;
430
415
  }
431
416
 
@@ -434,20 +419,24 @@ class Runner extends Emittery {
434
419
  title: task.title,
435
420
  knownFailing: task.metadata.failing,
436
421
  skip: task.metadata.skipped,
437
- todo: false
422
+ todo: false,
438
423
  });
439
424
 
440
- if (!task.metadata.skipped) {
425
+ if (task.metadata.skipped) {
426
+ this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
427
+ } else {
441
428
  serialTests.push(task);
442
429
  }
443
430
  }
444
431
 
445
432
  for (const task of this.tasks.concurrent) {
446
433
  if (this.runOnlyExclusive && !task.metadata.exclusive) {
434
+ this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
447
435
  continue;
448
436
  }
449
437
 
450
438
  if (this.checkSelectedByLineNumbers && !task.metadata.selected) {
439
+ this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
451
440
  continue;
452
441
  }
453
442
 
@@ -456,15 +445,15 @@ class Runner extends Emittery {
456
445
  title: task.title,
457
446
  knownFailing: task.metadata.failing,
458
447
  skip: task.metadata.skipped,
459
- todo: false
448
+ todo: false,
460
449
  });
461
450
 
462
- if (!task.metadata.skipped) {
463
- if (this.serial) {
464
- serialTests.push(task);
465
- } else {
466
- concurrentTests.push(task);
467
- }
451
+ if (task.metadata.skipped) {
452
+ this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
453
+ } else if (this.serial) {
454
+ serialTests.push(task);
455
+ } else {
456
+ concurrentTests.push(task);
468
457
  }
469
458
  }
470
459
 
@@ -482,7 +471,7 @@ class Runner extends Emittery {
482
471
  title: task.title,
483
472
  knownFailing: false,
484
473
  skip: false,
485
- todo: true
474
+ todo: true,
486
475
  });
487
476
  }
488
477
 
@@ -498,13 +487,13 @@ class Runner extends Emittery {
498
487
 
499
488
  // Note that the hooks and tests always begin running asynchronously.
500
489
  const beforePromise = this.runHooks(this.tasks.before, contextRef);
501
- const serialPromise = beforePromise.then(beforeHooksOk => { // eslint-disable-line promise/prefer-await-to-then
490
+ const serialPromise = beforePromise.then(beforeHooksOk => {
502
491
  // Don't run tests if a `before` hook failed.
503
492
  if (!beforeHooksOk) {
504
493
  return false;
505
494
  }
506
495
 
507
- return serialTests.reduce(async (previous, task) => { // eslint-disable-line unicorn/no-reduce
496
+ return serialTests.reduce(async (previous, task) => { // eslint-disable-line unicorn/no-array-reduce
508
497
  const previousOk = await previous;
509
498
  // Don't start tests after an interrupt.
510
499
  if (this.interrupted) {
@@ -520,7 +509,7 @@ class Runner extends Emittery {
520
509
  return this.runTest(task, contextRef.copy());
521
510
  }, true);
522
511
  });
523
- const concurrentPromise = Promise.all([beforePromise, serialPromise]).then(async ([beforeHooksOk, serialOk]) => { // eslint-disable-line promise/prefer-await-to-then
512
+ const concurrentPromise = Promise.all([beforePromise, serialPromise]).then(async ([beforeHooksOk, serialOk]) => {
524
513
  // Don't run tests if a `before` hook failed, or if `failFast` is enabled
525
514
  // and a previous serial test failed.
526
515
  if (!beforeHooksOk || (!serialOk && this.failFast)) {
@@ -534,9 +523,7 @@ class Runner extends Emittery {
534
523
 
535
524
  // If a concurrent test fails, even if `failFast` is enabled it won't
536
525
  // stop other concurrent tests from running.
537
- const allOkays = await Promise.all(concurrentTests.map(task => {
538
- return this.runTest(task, contextRef.copy());
539
- }));
526
+ const allOkays = await Promise.all(concurrentTests.map(task => this.runTest(task, contextRef.copy())));
540
527
  return allOkays.every(ok => ok);
541
528
  });
542
529
 
@@ -563,5 +550,3 @@ class Runner extends Emittery {
563
550
  this.interrupted = true;
564
551
  }
565
552
  }
566
-
567
- module.exports = Runner;
@@ -0,0 +1,53 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import writeFileAtomic from 'write-file-atomic';
5
+
6
+ import isCi from './is-ci.js';
7
+
8
+ const FILENAME = 'failing-tests.json';
9
+
10
+ const scheduler = {
11
+ storeFailedTestFiles(runStatus, cacheDir) {
12
+ if (isCi || !cacheDir) {
13
+ return;
14
+ }
15
+
16
+ try {
17
+ writeFileAtomic.sync(path.join(cacheDir, FILENAME), JSON.stringify(runStatus.getFailedTestFiles()));
18
+ } catch {}
19
+ },
20
+
21
+ // Order test-files, so that files with failing tests come first
22
+ failingTestsFirst(selectedFiles, cacheDir, cacheEnabled) {
23
+ if (isCi || cacheEnabled === false) {
24
+ return selectedFiles;
25
+ }
26
+
27
+ const filePath = path.join(cacheDir, FILENAME);
28
+ let failedTestFiles;
29
+ try {
30
+ failedTestFiles = JSON.parse(fs.readFileSync(filePath));
31
+ } catch {
32
+ return selectedFiles;
33
+ }
34
+
35
+ return [...selectedFiles].sort((f, s) => {
36
+ if (failedTestFiles.includes(f) && failedTestFiles.includes(s)) {
37
+ return 0;
38
+ }
39
+
40
+ if (failedTestFiles.includes(f)) {
41
+ return -1;
42
+ }
43
+
44
+ if (failedTestFiles.includes(s)) {
45
+ return 1;
46
+ }
47
+
48
+ return 0;
49
+ });
50
+ },
51
+ };
52
+
53
+ export default scheduler;