ava 6.3.0 → 6.4.1

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.
@@ -1,9 +1,9 @@
1
1
  import type {TestFn} from '../types/test-fn.cjs';
2
2
 
3
- export * from '../types/assertions.cjs';
4
- export * from '../types/try-fn.cjs';
5
- export * from '../types/test-fn.cjs';
6
- export * from '../types/subscribable.cjs';
3
+ export type * from '../types/assertions.cjs';
4
+ export type * from '../types/try-fn.cjs';
5
+ export type * from '../types/test-fn.cjs';
6
+ export type * from '../types/subscribable.cjs';
7
7
 
8
8
  /** Call to declare a test, or chain to declare hooks or test modifiers */
9
9
  declare const test: TestFn;
@@ -1,9 +1,9 @@
1
1
  import type {TestFn} from '../types/test-fn.cjs';
2
2
 
3
- export * from '../types/assertions.cjs';
4
- export * from '../types/try-fn.cjs';
5
- export * from '../types/test-fn.cjs';
6
- export * from '../types/subscribable.cjs';
3
+ export type * from '../types/assertions.cjs';
4
+ export type * from '../types/try-fn.cjs';
5
+ export type * from '../types/test-fn.cjs';
6
+ export type * from '../types/subscribable.cjs';
7
7
 
8
8
  /** Call to declare a test, or chain to declare hooks or test modifiers */
9
9
  declare const test: TestFn;
package/lib/api.js CHANGED
@@ -88,7 +88,7 @@ export default class Api extends Emittery {
88
88
  }
89
89
  }
90
90
 
91
- async run({files: selectedFiles = [], filter = [], runtimeOptions = {}} = {}) { // eslint-disable-line complexity
91
+ async run({files: selectedFiles = [], filter = [], runtimeOptions = {}, testFileSelector} = {}) { // eslint-disable-line complexity
92
92
  let setupOrGlobError;
93
93
 
94
94
  const apiOptions = this.options;
@@ -149,13 +149,17 @@ export default class Api extends Emittery {
149
149
  let testFiles;
150
150
  try {
151
151
  testFiles = await globs.findTests({cwd: this.options.projectDir, ...apiOptions.globs});
152
- if (selectedFiles.length === 0) {
153
- selectedFiles = filter.length === 0 ? testFiles : globs.applyTestFileFilter({
154
- cwd: this.options.projectDir,
155
- filter: filter.map(({pattern}) => pattern),
156
- providers,
157
- testFiles,
158
- });
152
+ if (typeof testFileSelector === 'function') {
153
+ selectedFiles = testFileSelector(testFiles, selectedFiles);
154
+ } else if (selectedFiles.length === 0) {
155
+ selectedFiles = filter.length === 0
156
+ ? testFiles
157
+ : globs.applyTestFileFilter({
158
+ cwd: this.options.projectDir,
159
+ filter: filter.map(({pattern}) => pattern),
160
+ providers,
161
+ testFiles,
162
+ });
159
163
  }
160
164
  } catch (error) {
161
165
  selectedFiles = [];
@@ -163,7 +167,7 @@ export default class Api extends Emittery {
163
167
  }
164
168
 
165
169
  const selectionInsights = {
166
- filter,
170
+ filter: selectedFiles.appliedFilters ?? filter,
167
171
  ignoredFilterPatternFiles: selectedFiles.ignoredFilterPatternFiles ?? [],
168
172
  testFileCount: testFiles.length,
169
173
  selectionCount: selectedFiles.length,
@@ -201,9 +205,8 @@ export default class Api extends Emittery {
201
205
  failFastEnabled: failFast,
202
206
  filePathPrefix: getFilePathPrefix(selectedFiles),
203
207
  files: selectedFiles,
204
- matching: apiOptions.match.length > 0,
205
- previousFailures: runtimeOptions.previousFailures ?? 0,
206
- runOnlyExclusive: runtimeOptions.runOnlyExclusive === true,
208
+ matching: apiOptions.match.length > 0 || runtimeOptions.interactiveMatchPattern !== undefined,
209
+ previousFailures: runtimeOptions.countPreviousFailures?.() ?? 0,
207
210
  firstRun: runtimeOptions.firstRun ?? true,
208
211
  status: runStatus,
209
212
  });
@@ -266,14 +269,13 @@ export default class Api extends Emittery {
266
269
 
267
270
  const lineNumbers = getApplicableLineNumbers(globs.normalizeFileForMatching(apiOptions.projectDir, file), filter);
268
271
  // Removing `providers` and `sortTestFiles` fields because they cannot be transferred to the worker threads.
269
- const {providers, sortTestFiles, ...forkOptions} = apiOptions;
272
+ const {providers, sortTestFiles, match, ...forkOptions} = apiOptions;
270
273
  const options = {
271
274
  ...forkOptions,
272
275
  providerStates,
273
276
  lineNumbers,
274
277
  recordNewSnapshots: !isCi,
275
- // If we're looking for matches, run every single test process in exclusive-only mode
276
- runOnlyExclusive: apiOptions.match.length > 0 || runtimeOptions.runOnlyExclusive === true,
278
+ match: runtimeOptions.interactiveMatchPattern === undefined ? match : [...match, runtimeOptions.interactiveMatchPattern],
277
279
  };
278
280
 
279
281
  if (runtimeOptions.updateSnapshots) {
@@ -291,7 +293,7 @@ export default class Api extends Emittery {
291
293
  deregisteredSharedWorkers.push(observeWorkerProcess(worker, runStatus));
292
294
 
293
295
  pendingWorkers.add(worker);
294
- worker.promise.then(() => {
296
+ worker.promise.then(() => { // eslint-disable-line promise/prefer-await-to-then
295
297
  pendingWorkers.delete(worker);
296
298
  });
297
299
  timeoutTrigger.debounce();
package/lib/assert.js CHANGED
@@ -490,7 +490,7 @@ export class Assertions {
490
490
  // Record the stack before it gets lost in the promise chain.
491
491
  const assertionStack = getAssertionStack();
492
492
  // Handle "promise like" objects by casting to a real Promise.
493
- const intermediate = Promise.resolve(promise).then(value => { // eslint-disable-line promise/prefer-await-to-then
493
+ const intermediate = Promise.resolve(promise).then(value => { // eslint-disable-line promise/prefer-catch, promise/prefer-await-to-then
494
494
  throw failPending(new AssertionError(message, {
495
495
  assertion: 't.throwsAsync()',
496
496
  assertionStack,
@@ -592,7 +592,7 @@ export class Assertions {
592
592
  // Create an error object to record the stack before it gets lost in the promise chain.
593
593
  const assertionStack = getAssertionStack();
594
594
  // Handle "promise like" objects by casting to a real Promise.
595
- const intermediate = Promise.resolve(promise).then(noop, error => { // eslint-disable-line promise/prefer-await-to-then
595
+ const intermediate = Promise.resolve(promise).then(noop, error => { // eslint-disable-line promise/prefer-catch, promise/prefer-await-to-then
596
596
  throw failPending(new AssertionError(message, {
597
597
  assertion: 't.notThrowsAsync()',
598
598
  assertionStack,
package/lib/chalk.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {Chalk} from 'chalk'; // eslint-disable-line unicorn/import-style
2
2
 
3
- let chalk = new Chalk(); // eslint-disable-line import/no-mutable-exports
3
+ let chalk = new Chalk(); // eslint-disable-line import-x/no-mutable-exports
4
4
 
5
5
  export {chalk};
6
6
 
package/lib/cli.js CHANGED
@@ -100,9 +100,7 @@ export default async function loadCli() { // eslint-disable-line complexity
100
100
  const {argv: {config: configFile}} = yargs(hideBin(process.argv)).help(false).version(false);
101
101
  const loaded = await loadConfig({configFile});
102
102
  if (loaded.unsupportedFiles.length > 0) {
103
- console.log(chalk.magenta(
104
- ` ${figures.warning} AVA does not support JSON config, ignoring:\n\n ${loaded.unsupportedFiles.join('\n ')}`,
105
- ));
103
+ console.log(chalk.magenta(` ${figures.warning} AVA does not support JSON config, ignoring:\n\n ${loaded.unsupportedFiles.join('\n ')}`));
106
104
  }
107
105
 
108
106
  conf = loaded.config;
@@ -130,7 +128,8 @@ export default async function loadCli() { // eslint-disable-line complexity
130
128
  files: [],
131
129
  host: undefined,
132
130
  port: undefined,
133
- } : null;
131
+ }
132
+ : null;
134
133
 
135
134
  let resetCache = false;
136
135
  const {argv} = yargs(hideBin(process.argv))
@@ -165,6 +164,7 @@ export default async function loadCli() { // eslint-disable-line complexity
165
164
  })
166
165
  .command('* [<pattern>...]', 'Run tests', yargs => yargs.options(FLAGS).positional('pattern', {
167
166
  array: true,
167
+ // eslint-disable-next-line @stylistic/max-len
168
168
  describe: 'Select which test files to run. Leave empty if you want AVA to run all test files as per your configuration. Accepts glob patterns, directories that (recursively) contain test files, and file paths optionally suffixed with a colon and comma-separated numbers and/or ranges identifying the 1-based line(s) of specific tests to run',
169
169
  type: 'string',
170
170
  }), argv => {
@@ -192,6 +192,7 @@ export default async function loadCli() { // eslint-disable-line complexity
192
192
  },
193
193
  }).positional('pattern', {
194
194
  demand: true,
195
+ // eslint-disable-next-line @stylistic/max-len
195
196
  describe: 'Glob pattern to select a single test file to debug, optionally suffixed with a colon and comma-separated numbers and/or ranges identifying the 1-based line(s) of specific tests to run',
196
197
  type: 'string',
197
198
  }),
@@ -203,14 +204,16 @@ export default async function loadCli() { // eslint-disable-line complexity
203
204
  host: argv.host,
204
205
  port: argv.port,
205
206
  };
206
- })
207
+ },
208
+ )
207
209
  .command(
208
210
  'reset-cache',
209
211
  'Delete any temporary files and state kept by AVA, then exit',
210
212
  yargs => yargs,
211
213
  () => {
212
214
  resetCache = true;
213
- })
215
+ },
216
+ )
214
217
  .example('$0')
215
218
  .example('$0 test.js')
216
219
  .example('$0 test.js:4,7-9')
package/lib/fork.js CHANGED
@@ -104,6 +104,11 @@ export default function loadFork(file, options, execArgv = process.execArgv) {
104
104
  }
105
105
 
106
106
  switch (message.ava.type) {
107
+ case 'worker-finished': {
108
+ send({type: 'free-worker'});
109
+ break;
110
+ }
111
+
107
112
  case 'ready-for-options': {
108
113
  send({type: 'options', options});
109
114
  break;
@@ -16,23 +16,21 @@ const parseNumber = string => Number.parseInt(string, 10);
16
16
  const removeAllWhitespace = string => string.replaceAll(/\s/g, '');
17
17
  const range = (start, end) => Array.from({length: end - start + 1}).fill(start).map((element, index) => element + index);
18
18
 
19
- const parseLineNumbers = suffix => sortNumbersAscending(distinctArray(
20
- suffix.split(',').flatMap(part => {
21
- if (NUMBER_REGEX.test(part)) {
22
- return parseNumber(part);
23
- }
19
+ const parseLineNumbers = suffix => sortNumbersAscending(distinctArray(suffix.split(',').flatMap(part => {
20
+ if (NUMBER_REGEX.test(part)) {
21
+ return parseNumber(part);
22
+ }
24
23
 
25
- const {groups: {startGroup, endGroup}} = RANGE_REGEX.exec(part);
26
- const start = parseNumber(startGroup);
27
- const end = parseNumber(endGroup);
24
+ const {groups: {startGroup, endGroup}} = RANGE_REGEX.exec(part);
25
+ const start = parseNumber(startGroup);
26
+ const end = parseNumber(endGroup);
28
27
 
29
- if (start > end) {
30
- return range(end, start);
31
- }
28
+ if (start > end) {
29
+ return range(end, start);
30
+ }
32
31
 
33
- return range(start, end);
34
- }),
35
- ));
32
+ return range(start, end);
33
+ })));
36
34
 
37
35
  export function splitPatternAndLineNumbers(pattern) {
38
36
  const parts = pattern.split(DELIMITER);
@@ -49,9 +47,7 @@ export function splitPatternAndLineNumbers(pattern) {
49
47
  }
50
48
 
51
49
  export function getApplicableLineNumbers(normalizedFilePath, filter) {
52
- return sortNumbersAscending(distinctArray(
53
- filter
54
- .filter(({pattern, lineNumbers}) => lineNumbers && picomatch.isMatch(normalizedFilePath, pattern))
55
- .flatMap(({lineNumbers}) => lineNumbers),
56
- ));
50
+ return sortNumbersAscending(distinctArray(filter
51
+ .filter(({pattern, lineNumbers}) => lineNumbers && picomatch.isMatch(normalizedFilePath, pattern))
52
+ .flatMap(({lineNumbers}) => lineNumbers)));
57
53
  }
@@ -26,7 +26,7 @@ class LineWriter extends stream.Writable {
26
26
 
27
27
  this.dest = dest;
28
28
  this.columns = dest.columns ?? 80;
29
- this.lastLineIsEmpty = false;
29
+ this.lastLineIsEmpty = true;
30
30
  }
31
31
 
32
32
  _write(chunk, _, callback) {
@@ -34,9 +34,9 @@ class LineWriter extends stream.Writable {
34
34
  callback();
35
35
  }
36
36
 
37
- writeLine(string) {
37
+ writeLine(string, indent = true) {
38
38
  if (string) {
39
- this.write(indentString(string, 2) + os.EOL);
39
+ this.write((indent ? indentString(string, 2) : string) + os.EOL);
40
40
  this.lastLineIsEmpty = false;
41
41
  } else {
42
42
  this.write(os.EOL);
@@ -44,6 +44,11 @@ class LineWriter extends stream.Writable {
44
44
  }
45
45
  }
46
46
 
47
+ write(string) {
48
+ this.lastLineIsEmpty = false;
49
+ super.write(string);
50
+ }
51
+
47
52
  ensureEmptyLine() {
48
53
  if (!this.lastLineIsEmpty) {
49
54
  this.writeLine();
@@ -120,7 +125,6 @@ export default class Reporter {
120
125
  this.previousFailures = 0;
121
126
 
122
127
  this.failFastEnabled = false;
123
- this.lastLineIsEmpty = false;
124
128
  this.matching = false;
125
129
 
126
130
  this.removePreviousListener = null;
@@ -323,6 +327,7 @@ export default class Reporter {
323
327
 
324
328
  this.lineWriter.writeLine(colors.error(`${figures.cross} Line numbers for ${this.relativeFile(event.testFile)} did not match any tests`));
325
329
  } else if (!this.failFastEnabled && fileStats.remainingTests > 0) {
330
+ // eslint-disable-next-line @stylistic/max-len
326
331
  this.lineWriter.writeLine(colors.error(`${figures.cross} ${fileStats.remainingTests} ${plur('test', fileStats.remainingTests)} remaining in ${this.relativeFile(event.testFile)}`));
327
332
  }
328
333
  }
@@ -628,7 +633,8 @@ export default class Reporter {
628
633
  this.lineWriter.writeLine(colors.error(`${figures.cross} Couldn’t find any files to test` + firstLinePostfix));
629
634
  } else {
630
635
  const {testFileCount: count} = this.selectionInsights;
631
- this.lineWriter.writeLine(colors.error(`${figures.cross} Based on your configuration, ${count} test ${plur('file was', 'files were', count)} found, but did not match the CLI arguments:` + firstLinePostfix));
636
+ // eslint-disable-next-line @stylistic/max-len
637
+ this.lineWriter.writeLine(colors.error(`${figures.cross} Based on your configuration, ${count} test ${plur('file was', 'files were', count)} found, but did not match the filters:` + firstLinePostfix));
632
638
  this.lineWriter.writeLine();
633
639
  for (const {pattern} of this.selectionInsights.filter) {
634
640
  this.lineWriter.writeLine(colors.error(`* ${pattern}`));
@@ -708,8 +714,7 @@ export default class Reporter {
708
714
  && this.stats.failedTests === 0
709
715
  && this.stats.passedTests > 0
710
716
  ) {
711
- this.lineWriter.writeLine(colors.pass(`${this.stats.passedTests} ${plur('test', this.stats.passedTests)} passed`) + firstLinePostfix,
712
- );
717
+ this.lineWriter.writeLine(colors.pass(`${this.stats.passedTests} ${plur('test', this.stats.passedTests)} passed`) + firstLinePostfix);
713
718
  firstLinePostfix = '';
714
719
  }
715
720
 
package/lib/runner.js CHANGED
@@ -2,7 +2,7 @@ import process from 'node:process';
2
2
  import {pathToFileURL} from 'node:url';
3
3
 
4
4
  import Emittery from 'emittery';
5
- import {matcher} from 'matcher';
5
+ import * as matcher from 'matcher';
6
6
 
7
7
  import ContextRef from './context-ref.js';
8
8
  import createChain from './create-chain.js';
@@ -13,6 +13,15 @@ import Runnable from './test.js';
13
13
  import {waitForReady} from './worker/state.cjs';
14
14
 
15
15
  const makeFileURL = file => file.startsWith('file://') ? file : pathToFileURL(file).toString();
16
+
17
+ const isTitleMatch = (title, patterns) => {
18
+ if (patterns.length === 0) {
19
+ return true;
20
+ }
21
+
22
+ return matcher.isMatch(title, patterns);
23
+ };
24
+
16
25
  export default class Runner extends Emittery {
17
26
  constructor(options = {}) {
18
27
  super();
@@ -22,10 +31,9 @@ export default class Runner extends Emittery {
22
31
  this.failWithoutAssertions = options.failWithoutAssertions !== false;
23
32
  this.file = options.file;
24
33
  this.checkSelectedByLineNumbers = options.checkSelectedByLineNumbers;
25
- this.match = options.match ?? [];
34
+ this.matchPatterns = options.match ?? [];
26
35
  this.projectDir = options.projectDir;
27
36
  this.recordNewSnapshots = options.recordNewSnapshots === true;
28
- this.runOnlyExclusive = options.runOnlyExclusive === true;
29
37
  this.serial = options.serial === true;
30
38
  this.snapshotDir = options.snapshotDir;
31
39
  this.updateSnapshots = options.updateSnapshots;
@@ -34,6 +42,7 @@ export default class Runner extends Emittery {
34
42
  this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this);
35
43
  this.boundSkipSnapshot = this.skipSnapshot.bind(this);
36
44
  this.interrupted = false;
45
+ this.runOnlyExclusive = false;
37
46
 
38
47
  this.nextTaskIndex = 0;
39
48
  this.tasks = {
@@ -92,9 +101,7 @@ export default class Runner extends Emittery {
92
101
 
93
102
  const {args, implementation, title} = parseTestArgs(testArgs);
94
103
 
95
- if (this.checkSelectedByLineNumbers) {
96
- metadata.selected = this.checkSelectedByLineNumbers();
97
- }
104
+ metadata.selected &&= this.checkSelectedByLineNumbers?.() ?? true;
98
105
 
99
106
  if (metadata.todo) {
100
107
  if (implementation) {
@@ -110,10 +117,7 @@ export default class Runner extends Emittery {
110
117
  }
111
118
 
112
119
  // --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;
116
- }
120
+ metadata.selected &&= isTitleMatch(title.value, this.matchPatterns);
117
121
 
118
122
  this.tasks.todo.push({title: title.value, metadata});
119
123
  this.emit('stateChange', {
@@ -154,14 +158,10 @@ export default class Runner extends Emittery {
154
158
  };
155
159
 
156
160
  if (metadata.type === 'test') {
157
- if (this.match.length > 0) {
158
- // --match overrides .only()
159
- task.metadata.exclusive = matcher(title.value, this.match).length === 1;
160
- }
161
-
162
- if (task.metadata.exclusive) {
163
- this.runOnlyExclusive = true;
164
- }
161
+ task.metadata.selected &&= isTitleMatch(title.value, this.matchPatterns);
162
+ // Unmatched .only() are not selected and won't run. However, runOnlyExclusive can only be true if no titles
163
+ // are being matched.
164
+ this.runOnlyExclusive ||= this.matchPatterns.length === 0 && task.metadata.exclusive && task.metadata.selected;
165
165
 
166
166
  this.tasks[metadata.serial ? 'serial' : 'concurrent'].push(task);
167
167
 
@@ -181,6 +181,7 @@ export default class Runner extends Emittery {
181
181
  serial: false,
182
182
  exclusive: false,
183
183
  skipped: false,
184
+ selected: true,
184
185
  todo: false,
185
186
  failing: false,
186
187
  callback: false,
@@ -253,23 +254,21 @@ export default class Runner extends Emittery {
253
254
  let waitForSerial = Promise.resolve();
254
255
  await runnables.reduce((previous, runnable) => { // eslint-disable-line unicorn/no-array-reduce
255
256
  if (runnable.metadata.serial || this.serial) {
256
- waitForSerial = previous.then(() =>
257
+ waitForSerial = previous.then(() => // eslint-disable-line promise/prefer-await-to-then
257
258
  // Serial runnables run as long as there was no previous failure, unless
258
259
  // the runnable should always be run.
259
- (allPassed || runnable.metadata.always) && runAndStoreResult(runnable),
260
- );
260
+ (allPassed || runnable.metadata.always) && runAndStoreResult(runnable));
261
261
  return waitForSerial;
262
262
  }
263
263
 
264
264
  return Promise.all([
265
265
  previous,
266
- waitForSerial.then(() =>
266
+ waitForSerial.then(() => // eslint-disable-line promise/prefer-await-to-then
267
267
  // Concurrent runnables are kicked off after the previous serial
268
268
  // runnables have completed, as long as there was no previous failure
269
269
  // (or if the runnable should always be run). One concurrent runnable's
270
270
  // failure does not prevent the next runnable from running.
271
- (allPassed || runnable.metadata.always) && runAndStoreResult(runnable),
272
- ),
271
+ (allPassed || runnable.metadata.always) && runAndStoreResult(runnable)),
273
272
  ]);
274
273
  }, waitForSerial);
275
274
 
@@ -378,7 +377,8 @@ export default class Runner extends Emittery {
378
377
  {
379
378
  titleSuffix: hookSuffix,
380
379
  testPassed: testOk,
381
- });
380
+ },
381
+ );
382
382
  } else {
383
383
  this.emit('stateChange', {
384
384
  type: 'test-failed',
@@ -398,20 +398,16 @@ export default class Runner extends Emittery {
398
398
  {
399
399
  titleSuffix: hookSuffix,
400
400
  testPassed: testOk,
401
- });
401
+ },
402
+ );
402
403
  return alwaysOk && hooksOk && testOk;
403
404
  }
404
405
 
405
- async start() { // eslint-disable-line complexity
406
+ async start() {
406
407
  const concurrentTests = [];
407
408
  const serialTests = [];
408
409
  for (const task of this.tasks.serial) {
409
- if (this.runOnlyExclusive && !task.metadata.exclusive) {
410
- this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
411
- continue;
412
- }
413
-
414
- if (this.checkSelectedByLineNumbers && !task.metadata.selected) {
410
+ if (!task.metadata.selected || (this.runOnlyExclusive && !task.metadata.exclusive)) {
415
411
  this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
416
412
  continue;
417
413
  }
@@ -432,12 +428,7 @@ export default class Runner extends Emittery {
432
428
  }
433
429
 
434
430
  for (const task of this.tasks.concurrent) {
435
- if (this.runOnlyExclusive && !task.metadata.exclusive) {
436
- this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
437
- continue;
438
- }
439
-
440
- if (this.checkSelectedByLineNumbers && !task.metadata.selected) {
431
+ if (!task.metadata.selected || (this.runOnlyExclusive && !task.metadata.exclusive)) {
441
432
  this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
442
433
  continue;
443
434
  }
@@ -460,11 +451,7 @@ export default class Runner extends Emittery {
460
451
  }
461
452
 
462
453
  for (const task of this.tasks.todo) {
463
- if (this.runOnlyExclusive && !task.metadata.exclusive) {
464
- continue;
465
- }
466
-
467
- if (this.checkSelectedByLineNumbers && !task.metadata.selected) {
454
+ if (!task.metadata.selected || (this.runOnlyExclusive && !task.metadata.exclusive)) {
468
455
  continue;
469
456
  }
470
457
 
@@ -160,26 +160,24 @@ class BufferBuilder {
160
160
  }
161
161
 
162
162
  function sortBlocks(blocksByTitle, blockIndices) {
163
- return [...blocksByTitle].sort(
164
- ([aTitle], [bTitle]) => {
165
- const a = blockIndices.get(aTitle);
166
- const b = blockIndices.get(bTitle);
167
-
168
- if (a === undefined) {
169
- if (b === undefined) {
170
- return 0;
171
- }
172
-
173
- return 1;
174
- }
163
+ return [...blocksByTitle].sort(([aTitle], [bTitle]) => {
164
+ const a = blockIndices.get(aTitle);
165
+ const b = blockIndices.get(bTitle);
175
166
 
167
+ if (a === undefined) {
176
168
  if (b === undefined) {
177
- return -1;
169
+ return 0;
178
170
  }
179
171
 
180
- return a - b;
181
- },
182
- );
172
+ return 1;
173
+ }
174
+
175
+ if (b === undefined) {
176
+ return -1;
177
+ }
178
+
179
+ return a - b;
180
+ });
183
181
  }
184
182
 
185
183
  async function encodeSnapshots(snapshotData) {
@@ -368,9 +366,7 @@ class Manager {
368
366
  }
369
367
 
370
368
  const snapshots = {
371
- blocks: sortBlocks(this.newBlocksByTitle, this.blockIndices).map(
372
- ([title, block]) => ({title, ...block}),
373
- ),
369
+ blocks: sortBlocks(this.newBlocksByTitle, this.blockIndices).map(([title, block]) => ({title, ...block})),
374
370
  };
375
371
 
376
372
  const buffer = await encodeSnapshots(snapshots);
package/lib/test.js CHANGED
@@ -630,11 +630,13 @@ export default class Test {
630
630
  if (this.metadata.failing) {
631
631
  passed = !passed;
632
632
 
633
- error = passed ? null : new AssertionError('Test was expected to fail, but succeeded, you should stop marking the test as failing', {
634
- // TODO: Provide an assertion stack that traces to the test declaration,
635
- // rather than AVA internals.
636
- assertionStack: '',
637
- });
633
+ error = passed
634
+ ? null
635
+ : new AssertionError('Test was expected to fail, but succeeded, you should stop marking the test as failing', {
636
+ // TODO: Provide an assertion stack that traces to the test declaration,
637
+ // rather than AVA internals.
638
+ assertionStack: '',
639
+ });
638
640
  }
639
641
 
640
642
  return {
package/lib/watcher.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import nodePath from 'node:path';
3
3
  import process from 'node:process';
4
+ import * as readline from 'node:readline/promises';
4
5
  import v8 from 'node:v8';
5
6
 
6
7
  import {nodeFileTrace} from '@vercel/nft';
@@ -9,6 +10,7 @@ import createDebug from 'debug';
9
10
  import {chalk} from './chalk.js';
10
11
  import {
11
12
  applyTestFileFilter, classify, buildIgnoreMatcher, findTests,
13
+ normalizePattern,
12
14
  } from './globs.js';
13
15
  import {levels as providerLevels} from './provider-manager.js';
14
16
 
@@ -18,8 +20,6 @@ const debug = createDebug('ava:watcher');
18
20
  // to make Node.js write out interim reports in various places.
19
21
  const takeCoverageForSelfTests = process.env.TEST_AVA ? v8.takeCoverage : undefined;
20
22
 
21
- const END_MESSAGE = chalk.gray('Type `r` and press enter to rerun tests\nType `u` and press enter to update snapshots\n');
22
-
23
23
  export function available(projectDir) {
24
24
  try {
25
25
  fs.watch(projectDir, {persistent: false, recursive: true, signal: AbortSignal.abort()});
@@ -34,18 +34,156 @@ export function available(projectDir) {
34
34
  return true;
35
35
  }
36
36
 
37
+ const cancel = Symbol('cancel');
38
+ const close = Symbol('close');
39
+
40
+ const promiseWithResolvers = Promise.withResolvers?.bind(Promise) ?? (() => {
41
+ let resolve;
42
+ let reject;
43
+ const promise = new Promise((_resolve, _reject) => {
44
+ resolve = _resolve;
45
+ reject = _reject;
46
+ });
47
+ return {promise, resolve, reject};
48
+ });
49
+
50
+ async function * readLines(stream) {
51
+ const rl = readline.createInterface({input: stream, output: process.stdout});
52
+ let promise;
53
+ let resolve;
54
+ let values = [];
55
+ rl.addListener('close', () => {
56
+ values.push(close);
57
+ resolve?.();
58
+ });
59
+ rl.addListener('SIGINT', () => {
60
+ values.push(cancel);
61
+ resolve?.();
62
+ });
63
+ rl.addListener('line', line => {
64
+ values.push(line.trim());
65
+ resolve?.();
66
+ });
67
+
68
+ while (true) {
69
+ yield * values;
70
+ values = [];
71
+ await promise; // eslint-disable-line no-await-in-loop
72
+ // Immediately create a new promise to wait for the next line.
73
+ ({promise, resolve} = promiseWithResolvers());
74
+ }
75
+ }
76
+
77
+ const eachLine = async (lineReader, callback) => {
78
+ for await (const line of lineReader) {
79
+ await callback(line);
80
+ }
81
+ };
82
+
83
+ const writeCommandInstructions = (reporter, interactiveGlobPattern, interactiveMatchPattern) => {
84
+ reporter.lineWriter.writeLine(chalk.gray('Type `g` followed by enter to filter test files by a glob pattern'));
85
+ reporter.lineWriter.writeLine(chalk.gray('Type `m` followed by enter to filter tests by their title (similar to --match)'));
86
+ if (interactiveGlobPattern || interactiveMatchPattern) {
87
+ reporter.lineWriter.writeLine(chalk.gray('Type `a` followed by enter to rerun all tests (while preserving filters)'));
88
+ reporter.lineWriter.writeLine(chalk.gray('Type `r` followed by enter to rerun tests that match your filters'));
89
+ } else {
90
+ reporter.lineWriter.writeLine(chalk.gray('Type `r` followed by enter to rerun tests'));
91
+ }
92
+
93
+ reporter.lineWriter.writeLine(chalk.gray('Type `u` followed by enter to update snapshots in selected tests'));
94
+
95
+ if (interactiveGlobPattern || interactiveMatchPattern) {
96
+ reporter.lineWriter.writeLine();
97
+
98
+ if (interactiveGlobPattern) {
99
+ reporter.lineWriter.writeLine(chalk.gray(`Current test file glob pattern: ${chalk.italic(interactiveGlobPattern)}`));
100
+ }
101
+
102
+ if (interactiveMatchPattern) {
103
+ reporter.lineWriter.writeLine(chalk.gray(`Current test title match pattern: ${chalk.italic(interactiveMatchPattern)}`));
104
+ }
105
+ }
106
+
107
+ reporter.lineWriter.writeLine();
108
+ reporter.lineWriter.write('> ');
109
+ };
110
+
111
+ const promptForGlobPattern = async (reporter, lineReader, currentPattern, projectDir) => {
112
+ reporter.lineWriter.ensureEmptyLine();
113
+ reporter.lineWriter.writeLine('Type the glob pattern then press enter. Leave blank to clear.', false);
114
+ if (currentPattern === undefined) {
115
+ reporter.lineWriter.writeLine();
116
+ reporter.lineWriter.writeLine(chalk.italic('Tip: Start with `**/` to select files in any directory.'), false);
117
+ reporter.lineWriter.writeLine(chalk.italic('Tip: Start with `!` to exclude files.'), false);
118
+ } else {
119
+ reporter.lineWriter.writeLine();
120
+ reporter.lineWriter.writeLine(`Current glob pattern is: ${chalk.italic(currentPattern)}`, false);
121
+ reporter.lineWriter.writeLine();
122
+ reporter.lineWriter.writeLine(chalk.italic('Tip: Ctrl+C to exit without any changes.'), false);
123
+ }
124
+
125
+ reporter.lineWriter.write('> ');
126
+
127
+ const {value} = await lineReader.next();
128
+ if (value === close || value === cancel) {
129
+ return value;
130
+ }
131
+
132
+ if (value === '') {
133
+ return undefined;
134
+ }
135
+
136
+ return normalizePattern(nodePath.relative(projectDir, nodePath.resolve(process.cwd(), value)));
137
+ };
138
+
139
+ const promptForMatchPattern = async (reporter, lineReader, currentPattern) => {
140
+ reporter.lineWriter.writeLine();
141
+ reporter.lineWriter.writeLine('Type the match pattern then press enter. Leave blank to clear.', false);
142
+ if (currentPattern === undefined) {
143
+ reporter.lineWriter.writeLine();
144
+ reporter.lineWriter.writeLine(chalk.italic('Tip: Start with `*` to match suffixes'), false);
145
+ reporter.lineWriter.writeLine(chalk.italic('Tip: End with `*` to match prefixes.'), false);
146
+ reporter.lineWriter.writeLine(chalk.italic('Tip: Start with `!` to exclude titles.'), false);
147
+ } else {
148
+ reporter.lineWriter.writeLine();
149
+ reporter.lineWriter.writeLine(`Current match pattern is: ${chalk.italic(currentPattern)}`, false);
150
+ reporter.lineWriter.writeLine();
151
+ reporter.lineWriter.writeLine(chalk.italic('Tip: Ctrl+C to exit without any changes.'), false);
152
+ }
153
+
154
+ reporter.lineWriter.write('> ');
155
+
156
+ const {value} = await lineReader.next();
157
+ return value === '' ? undefined : value;
158
+ };
159
+
37
160
  export async function start({api, filter, globs, projectDir, providers, reporter, stdin, signal}) {
38
161
  providers = providers.filter(({level}) => level >= providerLevels.ava6);
39
- for await (const {files, ...runtimeOptions} of plan({
40
- api, filter, globs, projectDir, providers, stdin, abortSignal: signal,
162
+ for await (const {files, testFileSelector, ...runtimeOptions} of plan({
163
+ api,
164
+ filter,
165
+ globs,
166
+ projectDir,
167
+ providers,
168
+ stdin,
169
+ abortSignal: signal,
170
+ reporter,
41
171
  })) {
42
- await api.run({files, filter, runtimeOptions});
172
+ await api.run({files, testFileSelector, runtimeOptions});
43
173
  reporter.endRun();
44
- reporter.lineWriter.writeLine(END_MESSAGE);
45
174
  }
46
175
  }
47
176
 
48
- async function * plan({api, filter, globs, projectDir, providers, stdin, abortSignal}) {
177
+ async function * plan({
178
+ api,
179
+ filter,
180
+ globs,
181
+ projectDir,
182
+ providers,
183
+ stdin,
184
+ abortSignal,
185
+ reporter,
186
+ }) {
49
187
  const fileTracer = new FileTracer({base: projectDir});
50
188
  const isIgnored = buildIgnoreMatcher(globs);
51
189
  const patternFilters = filter.map(({pattern}) => pattern);
@@ -79,11 +217,19 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
79
217
  }))));
80
218
 
81
219
  // State tracked for test runs.
82
- const filesWithExclusiveTests = new Set();
83
220
  const touchedFiles = new Set();
84
221
  const temporaryFiles = new Set();
85
222
  const failureCounts = new Map();
86
223
 
224
+ const countPreviousFailures = () => {
225
+ let previousFailures = 0;
226
+ for (const count of failureCounts.values()) {
227
+ previousFailures += count;
228
+ }
229
+
230
+ return previousFailures;
231
+ };
232
+
87
233
  // Observe all test runs.
88
234
  api.on('run', ({status}) => {
89
235
  status.on('stateChange', evt => {
@@ -117,17 +263,6 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
117
263
  break;
118
264
  }
119
265
 
120
- case 'worker-finished': {
121
- const fileStats = status.stats.byFile.get(evt.testFile);
122
- if (fileStats.selectedTests > 0 && fileStats.declaredTests > fileStats.selectedTests) {
123
- filesWithExclusiveTests.add(nodePath.relative(projectDir, evt.testFile));
124
- } else {
125
- filesWithExclusiveTests.delete(nodePath.relative(projectDir, evt.testFile));
126
- }
127
-
128
- break;
129
- }
130
-
131
266
  default: {
132
267
  break;
133
268
  }
@@ -151,21 +286,124 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
151
286
  updateSnapshots = false;
152
287
  };
153
288
 
154
- // Support interactive commands.
155
- stdin.setEncoding('utf8');
156
- stdin.on('data', data => {
157
- data = data.trim().toLowerCase();
158
- runAll ||= data === 'r';
159
- updateSnapshots ||= data === 'u';
160
- if (runAll || updateSnapshots) {
161
- signalChanged({});
289
+ // Interactive filters.
290
+ let interactiveGlobPattern;
291
+ let interactiveMatchPattern;
292
+ const testFileSelector = (allTestFiles, selectedFiles = [], skipInteractive = runAll) => {
293
+ if (selectedFiles.length === 0) {
294
+ selectedFiles = allTestFiles;
162
295
  }
163
- });
296
+
297
+ if (patternFilters.length > 0) {
298
+ selectedFiles = applyTestFileFilter({
299
+ cwd: projectDir,
300
+ filter: patternFilters,
301
+ testFiles: selectedFiles,
302
+ treatFilterPatternsAsFiles: runAll, // This option is additive, so only select individual files on full runs.
303
+ });
304
+ selectedFiles.appliedFilters = filter; // `filter` is the original input.
305
+ }
306
+
307
+ if (!skipInteractive && interactiveGlobPattern !== undefined) {
308
+ const {appliedFilters = [], ignoredFilterPatternFiles} = selectedFiles;
309
+ selectedFiles = applyTestFileFilter({
310
+ cwd: projectDir,
311
+ filter: [interactiveGlobPattern],
312
+ testFiles: selectedFiles,
313
+ treatFilterPatternsAsFiles: false,
314
+ });
315
+ selectedFiles.appliedFilters = [...appliedFilters, {pattern: interactiveGlobPattern}];
316
+ selectedFiles.ignoredFilterPatternFiles = ignoredFilterPatternFiles;
317
+ }
318
+
319
+ // Remove previous failures for tests that will run again.
320
+ for (const file of selectedFiles) {
321
+ const path = nodePath.relative(projectDir, file);
322
+ failureCounts.delete(path);
323
+ }
324
+
325
+ return selectedFiles;
326
+ };
327
+
328
+ const lineReader = readLines(stdin);
329
+
330
+ // Don't let the reader keep the process alive.
164
331
  stdin.unref();
165
332
 
166
- // Whether tests are currently running. Used to control when the next run
167
- // is prepared.
168
- let testsAreRunning = false;
333
+ // Handle commands.
334
+ eachLine(lineReader, async line => {
335
+ if (line === cancel || line === close) {
336
+ process.exit(); // eslint-disable-line unicorn/no-process-exit
337
+ }
338
+
339
+ switch (line.toLowerCase()) {
340
+ case 'r': {
341
+ signalChanged();
342
+ break;
343
+ }
344
+
345
+ case 'u': {
346
+ updateSnapshots = true;
347
+ signalChanged();
348
+ break;
349
+ }
350
+
351
+ case 'a': {
352
+ runAll = true;
353
+ signalChanged();
354
+ break;
355
+ }
356
+
357
+ case 'g': {
358
+ respondToChanges = false;
359
+ const oldGlobPattern = interactiveGlobPattern;
360
+ const promptValue = await promptForGlobPattern(reporter, lineReader, interactiveGlobPattern, projectDir);
361
+ respondToChanges = true;
362
+ reporter.lineWriter.writeLine();
363
+ if (promptValue === close) {
364
+ process.exit(); // eslint-disable-line unicorn/no-process-exit
365
+ }
366
+
367
+ if (promptValue === cancel || (promptValue === oldGlobPattern)) {
368
+ signalChanged();
369
+ break;
370
+ }
371
+
372
+ interactiveGlobPattern = promptValue;
373
+ signalChanged();
374
+
375
+ break;
376
+ }
377
+
378
+ case 'm': {
379
+ respondToChanges = false;
380
+ const oldMatchPattern = interactiveMatchPattern;
381
+ const promptValue = await promptForMatchPattern(reporter, lineReader, interactiveMatchPattern);
382
+ respondToChanges = true;
383
+ reporter.lineWriter.writeLine();
384
+ if (promptValue === close) {
385
+ process.exit(); // eslint-disable-line unicorn/no-process-exit
386
+ }
387
+
388
+ if (promptValue === cancel || (promptValue === oldMatchPattern)) {
389
+ signalChanged();
390
+ break;
391
+ }
392
+
393
+ interactiveMatchPattern = promptValue;
394
+ signalChanged();
395
+
396
+ break;
397
+ }
398
+
399
+ default: {
400
+ break;
401
+ }
402
+ }
403
+ });
404
+
405
+ // Whether to respond to file system changes. Used to control when the next run is prepared.
406
+ let respondToChanges = true;
169
407
 
170
408
  // Tracks file paths we know have changed since the previous test run.
171
409
  const dirtyPaths = new Set();
@@ -177,9 +415,9 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
177
415
  return;
178
416
  }
179
417
 
180
- // Equally, if tests are currently running, then keep accumulating changes.
181
- // The timer is refreshed after tests finish running.
182
- if (testsAreRunning) {
418
+ // Equally, if tests are currently running, or the user is being prompted, then keep accumulating changes.
419
+ // The timer is refreshed when we're ready to resume.
420
+ if (!respondToChanges) {
183
421
  takeCoverageForSelfTests?.();
184
422
  return;
185
423
  }
@@ -327,42 +565,12 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
327
565
  fileTracer.update(changes);
328
566
  }
329
567
 
330
- // Select the test files to run, and how to run them.
331
- let testFiles = [...uniqueTestFiles];
332
- let runOnlyExclusive = false;
333
-
334
- if (testFiles.length > 0) {
335
- const exclusiveFiles = testFiles.filter(path => filesWithExclusiveTests.has(path));
336
- runOnlyExclusive = exclusiveFiles.length !== filesWithExclusiveTests.size;
337
- if (runOnlyExclusive) {
338
- // The test files that previously contained exclusive tests are always
339
- // run, together with the test files.
340
- debug('Running exclusive tests in %o', [...filesWithExclusiveTests]);
341
- testFiles = [...new Set([...filesWithExclusiveTests, ...testFiles])];
342
- }
343
- }
344
-
345
- if (filter.length > 0) {
346
- testFiles = applyTestFileFilter({
347
- cwd: projectDir,
348
- expandDirectories: false,
349
- filter: patternFilters,
350
- testFiles,
351
- treatFilterPatternsAsFiles: false,
352
- });
353
- }
354
-
355
568
  if (nonTestFiles.length > 0) {
356
569
  debug('Non-test files changed, running all tests');
357
570
  failureCounts.clear(); // All tests are run, so clear previous failures.
358
- signalChanged({runOnlyExclusive});
359
- } else if (testFiles.length > 0) {
360
- // Remove previous failures for tests that will run again.
361
- for (const path of testFiles) {
362
- failureCounts.delete(path);
363
- }
364
-
365
- signalChanged({runOnlyExclusive, testFiles});
571
+ signalChanged();
572
+ } else if (uniqueTestFiles.size > 0) {
573
+ signalChanged({testFiles: [...uniqueTestFiles]});
366
574
  }
367
575
 
368
576
  takeCoverageForSelfTests?.();
@@ -378,34 +586,61 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi
378
586
  });
379
587
 
380
588
  abortSignal?.addEventListener('abort', () => {
381
- signalChanged?.({});
589
+ signalChanged?.();
382
590
  });
383
591
 
384
592
  // And finally, the watch loop.
385
593
  while (abortSignal?.aborted !== true) {
386
- const {testFiles: files = [], runOnlyExclusive = false} = await changed; // eslint-disable-line no-await-in-loop
594
+ const {testFiles = []} = (await changed) ?? {}; // eslint-disable-line no-await-in-loop
387
595
 
388
596
  if (abortSignal?.aborted) {
389
597
  break;
390
598
  }
391
599
 
392
- let previousFailures = 0;
393
- for (const count of failureCounts.values()) {
394
- previousFailures += count;
600
+ // Values are changed by refresh() so copy them now.
601
+ const instructFirstRun = firstRun;
602
+ const skipInteractive = runAll;
603
+ const instructUpdateSnapshots = updateSnapshots;
604
+ reset(); // Make sure the next run can be triggered.
605
+
606
+ let files = testFiles.map(file => nodePath.join(projectDir, file));
607
+ let instructTestFileSelector = testFileSelector;
608
+ if (files.length > 0) {
609
+ files = testFileSelector(files, [], skipInteractive);
610
+ if (files.length === 0) {
611
+ debug('Filters rejected all test files');
612
+ continue;
613
+ }
614
+
615
+ // Make a no-op for the API to avoid filtering `files` again.
616
+ instructTestFileSelector = () => files;
617
+ } else if (skipInteractive) {
618
+ instructTestFileSelector = (allTestFiles, selectedFiles = []) => testFileSelector(allTestFiles, selectedFiles, true);
619
+ }
620
+
621
+ // Clear any prompt.
622
+ if (!reporter.lineWriter.lastLineIsEmpty && reporter.reportStream.isTTY) {
623
+ reporter.reportStream.clearLine(0);
624
+ reporter.lineWriter.writeLine();
395
625
  }
396
626
 
397
- const instructions = {
398
- files: files.map(file => nodePath.join(projectDir, file)),
399
- firstRun, // Value is changed by refresh() so record now.
400
- previousFailures,
401
- runOnlyExclusive,
402
- updateSnapshots, // Value is changed by refresh() so record now.
627
+ // Let the tests run.
628
+ respondToChanges = false;
629
+ yield {
630
+ countPreviousFailures,
631
+ files,
632
+ firstRun: instructFirstRun,
633
+ testFileSelector: instructTestFileSelector,
634
+ updateSnapshots: instructUpdateSnapshots,
635
+ interactiveMatchPattern: skipInteractive ? undefined : interactiveMatchPattern,
403
636
  };
404
- reset(); // Make sure the next run can be triggered.
405
- testsAreRunning = true;
406
- yield instructions; // Let the tests run.
407
- testsAreRunning = false;
408
- debounce.refresh(); // Trigger the callback, which if there were changes will run the tests again.
637
+ respondToChanges = true;
638
+
639
+ // Write command instructions after the tests have run and been reported.
640
+ writeCommandInstructions(reporter, interactiveGlobPattern, interactiveMatchPattern);
641
+
642
+ // Trigger the callback, which if there were changes will run the tests again.
643
+ debounce.refresh();
409
644
  }
410
645
  }
411
646
 
@@ -81,7 +81,6 @@ const run = async options => {
81
81
  match: options.match,
82
82
  projectDir: options.projectDir,
83
83
  recordNewSnapshots: options.recordNewSnapshots,
84
- runOnlyExclusive: options.runOnlyExclusive,
85
84
  serial: options.serial,
86
85
  snapshotDir: options.snapshotDir,
87
86
  updateSnapshots: options.updateSnapshots,
@@ -121,6 +120,15 @@ const run = async options => {
121
120
  return;
122
121
  }
123
122
 
123
+ channel.send({type: 'worker-finished'});
124
+
125
+ // Reference the channel until the worker is freed. This should prevent Node.js from terminating the child process
126
+ // prematurely, which has been witnessed on Windows. See discussion at
127
+ // <https://github.com/avajs/ava/issues/3390#issuecomment-3056119361>.
128
+ channel.ref();
129
+ await channel.workerFreed;
130
+ channel.unref();
131
+
124
132
  nowAndTimers.setImmediate(() => {
125
133
  const unhandled = currentlyUnhandled();
126
134
  if (unhandled.length === 0) {
@@ -107,7 +107,9 @@ handle.ref();
107
107
 
108
108
  exports.options = selectAvaMessage(handle.channel, 'options').then(message => message.ava.options);
109
109
  exports.peerFailed = selectAvaMessage(handle.channel, 'peer-failed');
110
+ exports.workerFreed = selectAvaMessage(handle.channel, 'free-worker');
110
111
  exports.send = handle.send.bind(handle);
112
+ exports.ref = handle.ref.bind(handle);
111
113
  exports.unref = handle.unref.bind(handle);
112
114
 
113
115
  let channelCounter = 0;
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
- require('./guard-environment.cjs'); // eslint-disable-line import/no-unassigned-import
2
+ require('./guard-environment.cjs'); // eslint-disable-line import-x/no-unassigned-import
3
3
 
4
4
  const assert = require('node:assert');
5
5
 
@@ -4,7 +4,7 @@ const {registerSharedWorker: register} = require('./channel.cjs');
4
4
  const options = require('./options.cjs');
5
5
  const {sharedWorkerTeardowns, waitForReady} = require('./state.cjs');
6
6
 
7
- require('./guard-environment.cjs'); // eslint-disable-line import/no-unassigned-import
7
+ require('./guard-environment.cjs'); // eslint-disable-line import-x/no-unassigned-import
8
8
 
9
9
  const workers = new Map();
10
10
  const workerTeardownFns = new WeakMap();
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "ava",
3
- "version": "6.3.0",
3
+ "version": "6.4.1",
4
4
  "description": "Node.js test runner that lets you develop with confidence.",
5
5
  "license": "MIT",
6
- "repository": "avajs/ava",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/avajs/ava.git"
9
+ },
7
10
  "homepage": "https://avajs.dev",
8
11
  "bin": {
9
12
  "ava": "entrypoints/cli.mjs"
@@ -36,7 +39,7 @@
36
39
  },
37
40
  "type": "module",
38
41
  "engines": {
39
- "node": "^18.18 || ^20.8 || ^22 || >=23"
42
+ "node": "^18.18 || ^20.8 || ^22 || ^23 || >=24"
40
43
  },
41
44
  "scripts": {
42
45
  "test": "./scripts/test.sh"
@@ -83,25 +86,25 @@
83
86
  "typescript"
84
87
  ],
85
88
  "dependencies": {
86
- "@vercel/nft": "^0.29.2",
87
- "acorn": "^8.14.1",
89
+ "@vercel/nft": "^0.29.4",
90
+ "acorn": "^8.15.0",
88
91
  "acorn-walk": "^8.3.4",
89
92
  "ansi-styles": "^6.2.1",
90
93
  "arrgv": "^1.0.2",
91
94
  "arrify": "^3.0.0",
92
95
  "callsites": "^4.2.0",
93
- "cbor": "^10.0.3",
96
+ "cbor": "^10.0.9",
94
97
  "chalk": "^5.4.1",
95
98
  "chunkd": "^2.0.1",
96
- "ci-info": "^4.2.0",
99
+ "ci-info": "^4.3.0",
97
100
  "ci-parallel-vars": "^1.0.1",
98
101
  "cli-truncate": "^4.0.0",
99
102
  "code-excerpt": "^4.0.0",
100
103
  "common-path-prefix": "^3.0.0",
101
104
  "concordance": "^5.0.4",
102
105
  "currently-unhandled": "^0.4.1",
103
- "debug": "^4.4.0",
104
- "emittery": "^1.1.0",
106
+ "debug": "^4.4.1",
107
+ "emittery": "^1.2.0",
105
108
  "figures": "^6.1.0",
106
109
  "globby": "^14.1.0",
107
110
  "ignore-by-default": "^2.1.0",
@@ -126,19 +129,19 @@
126
129
  },
127
130
  "devDependencies": {
128
131
  "@ava/test": "github:avajs/test",
129
- "@ava/typescript": "^5.0.0",
132
+ "@ava/typescript": "^6.0.0",
130
133
  "@sindresorhus/tsconfig": "^5.1.1",
131
- "@types/node": "^22.14.1",
134
+ "@types/node": "^22.16.3",
132
135
  "ansi-escapes": "^7.0.0",
133
136
  "c8": "^10.1.3",
134
- "execa": "^9.5.2",
135
- "expect": "^29.7.0",
136
- "sinon": "^20.0.0",
137
+ "execa": "^9.6.0",
138
+ "expect": "^30.0.4",
139
+ "sinon": "^21.0.0",
137
140
  "tap": "^21.1.0",
138
141
  "tempy": "^3.1.0",
139
142
  "tsd": "^0.32.0",
140
143
  "typescript": "~5.8.3",
141
- "xo": "^0.60.0",
144
+ "xo": "^1.1.1",
142
145
  "zen-observable": "^0.10.0"
143
146
  },
144
147
  "peerDependencies": {
@@ -150,7 +153,7 @@
150
153
  }
151
154
  },
152
155
  "volta": {
153
- "node": "22.14.0",
154
- "npm": "11.3.0"
156
+ "node": "22.16.0",
157
+ "npm": "11.4.1"
155
158
  }
156
159
  }
@@ -143,7 +143,7 @@ export type Assertions = {
143
143
  truthy: TruthyAssertion;
144
144
  };
145
145
 
146
- type FalsyValue = false | 0 | 0n | '' | null | undefined; // eslint-disable-line @typescript-eslint/ban-types
146
+ type FalsyValue = false | 0 | 0n | '' | undefined;
147
147
  type Falsy<T> = T extends Exclude<T, FalsyValue> ? (T extends number | string | bigint ? T & FalsyValue : never) : T;
148
148
 
149
149
  export type AssertAssertion = {
@@ -2,7 +2,7 @@ export namespace SharedWorker {
2
2
  export type ProtocolIdentifier = 'ava-4';
3
3
 
4
4
  export type FactoryOptions = {
5
- negotiateProtocol <Data = unknown>(supported: readonly ['ava-4']): Protocol<Data>;
5
+ negotiateProtocol<Data = unknown>(supported: readonly ['ava-4']): Protocol<Data>;
6
6
  // Add overloads for additional protocols.
7
7
  };
8
8