From 78a3768fbe3360930351374f854f9cc7a46fda51 Mon Sep 17 00:00:00 2001 From: Michael Mulet Date: Wed, 26 Mar 2025 17:54:14 -0500 Subject: [PATCH 1/9] Implemented Watch mode filter by filename and by test name #1530 --- docs/recipes/watch-mode.md | 29 ++ lib/api.js | 2 + lib/reporters/default.js | 15 +- lib/runner.js | 32 +- lib/watch-mode-skip-tests.js | 56 ++++ lib/watcher.js | 224 +++++++++++++- lib/worker/base.js | 1 + test/watch-mode/basic-functionality.js | 291 ++++++++++++++++++ .../fixtures/filter-files/ava.config.js | 1 + .../fixtures/filter-files/package.json | 3 + .../fixtures/filter-files/test1.test.js | 20 ++ .../fixtures/filter-files/test2.test.js | 20 ++ 12 files changed, 674 insertions(+), 20 deletions(-) create mode 100644 lib/watch-mode-skip-tests.js create mode 100644 test/watch-mode/fixtures/filter-files/ava.config.js create mode 100644 test/watch-mode/fixtures/filter-files/package.json create mode 100644 test/watch-mode/fixtures/filter-files/test1.test.js create mode 100644 test/watch-mode/fixtures/filter-files/test2.test.js diff --git a/docs/recipes/watch-mode.md b/docs/recipes/watch-mode.md index 39d7c4bee..8bab4de52 100644 --- a/docs/recipes/watch-mode.md +++ b/docs/recipes/watch-mode.md @@ -34,6 +34,35 @@ export default { If your tests write to disk they may trigger the watcher to rerun your tests. Configuring additional ignore patterns helps avoid this. +### Filter tests while watching +You may also filter tests while watching by using the cli. For example, after running +```console +$ npx ava --watch +``` +You will see a prompt like this : +```console +Type `p` and press enter to filter by a filename regex pattern + [Current filename filter is $pattern] +Type `t` and press enter to filter by a test name regex pattern + [Current test filter is $pattern] + +[Type `a` and press enter to run *all* tests] +(Type `r` and press enter to rerun tests || + Type \`r\` and press enter to rerun tests that match your filters) +Type `u` and press enter to update snapshots + +command > +``` +So, to run only tests numbered like +- foo23434 +- foo4343 +- foo93823 + +You can type `t` and press enter, then type `foo\d+` and press enter. +This will then run all tests that match that pattern. +Afterwards you can use the `r` command to run the matched tests again, +or `a` command to run **all** tests. + ## Dependency tracking AVA tracks which source files your test files depend on. If you change such a dependency only the test file that depends on it will be rerun. AVA will rerun all tests if it cannot determine which test file depends on the changed source file. diff --git a/lib/api.js b/lib/api.js index ff0f0f74c..e32ad724c 100644 --- a/lib/api.js +++ b/lib/api.js @@ -206,6 +206,7 @@ export default class Api extends Emittery { runOnlyExclusive: runtimeOptions.runOnlyExclusive === true, firstRun: runtimeOptions.firstRun ?? true, status: runStatus, + watchModeSkipTestsOrUndefined: runtimeOptions.watchModeSkipTestsOrUndefined, }); if (setupOrGlobError) { @@ -273,6 +274,7 @@ export default class Api extends Emittery { providerStates, lineNumbers, recordNewSnapshots: !isCi, + watchModeSkipTestsData: runtimeOptions.watchModeSkipTestsOrUndefined, // If we're looking for matches, run every single test process in exclusive-only mode runOnlyExclusive: apiOptions.match.length > 0 || runtimeOptions.runOnlyExclusive === true, }; diff --git a/lib/reporters/default.js b/lib/reporters/default.js index a70f24e4e..3081ac1a0 100644 --- a/lib/reporters/default.js +++ b/lib/reporters/default.js @@ -139,6 +139,7 @@ export default class Reporter { this.previousFailures = plan.previousFailures; this.emptyParallelRun = plan.status.emptyParallelRun; this.selectionInsights = plan.status.selectionInsights; + this.watchModeSkipTestsOrUndefined = plan.watchModeSkipTestsOrUndefined; if (this.watching || plan.files.length > 1) { this.prefixTitle = (testFile, title) => prefixTitle(this.extensions, plan.filePathPrefix, testFile, title); @@ -251,9 +252,19 @@ export default class Reporter { case 'selected-test': { if (event.skip) { - this.lineWriter.writeLine(colors.skip(`- [skip] ${this.prefixTitle(event.testFile, event.title)}`)); + if (!this.watchModeSkipTestsOrUndefined?.shouldSkipEvent(event)) { + this.lineWriter.writeLine( + colors.skip( + `- [skip] ${this.prefixTitle(event.testFile, event.title)}`, + ), + ); + } } else if (event.todo) { - this.lineWriter.writeLine(colors.todo(`- [todo] ${this.prefixTitle(event.testFile, event.title)}`)); + this.lineWriter.writeLine( + colors.todo( + `- [todo] ${this.prefixTitle(event.testFile, event.title)}`, + ), + ); } break; diff --git a/lib/runner.js b/lib/runner.js index fac04e344..e5f3011f1 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -10,6 +10,7 @@ import parseTestArgs from './parse-test-args.js'; import serializeError from './serialize-error.js'; import {load as loadSnapshots, determineSnapshotDir} from './snapshot-manager.js'; import Runnable from './test.js'; +import {WatchModeSkipTests} from './watch-mode-skip-tests.js'; import {waitForReady} from './worker/state.cjs'; const makeFileURL = file => file.startsWith('file://') ? file : pathToFileURL(file).toString(); @@ -29,6 +30,10 @@ export default class Runner extends Emittery { this.serial = options.serial === true; this.snapshotDir = options.snapshotDir; this.updateSnapshots = options.updateSnapshots; + this.watchModeSkipTests + = new WatchModeSkipTests(options.watchModeSkipTestsData); + this.skipAllTestsInThisFile = this.watchModeSkipTests + .shouldSkipFile(this.file); this.activeRunnables = new Set(); this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this); @@ -91,6 +96,11 @@ export default class Runner extends Emittery { metadata.taskIndex = this.nextTaskIndex++; const {args, implementation, title} = parseTestArgs(testArgs); + if ( + this.shouldSkipThisTestBecauseItDoesNotMatchARegexSetAtTheWatchCLI(metadata, title.value) + ) { + metadata.skipped = true; + } if (this.checkSelectedByLineNumbers) { metadata.selected = this.checkSelectedByLineNumbers(); @@ -180,7 +190,7 @@ export default class Runner extends Emittery { }, { serial: false, exclusive: false, - skipped: false, + skipped: this.skipAllTestsInThisFile, todo: false, failing: false, callback: false, @@ -254,8 +264,8 @@ export default class Runner extends Emittery { await runnables.reduce((previous, runnable) => { // eslint-disable-line unicorn/no-array-reduce if (runnable.metadata.serial || this.serial) { waitForSerial = previous.then(() => - // Serial runnables run as long as there was no previous failure, unless - // the runnable should always be run. + // Serial runnables run as long as there was no previous failure, unless + // the runnable should always be run. (allPassed || runnable.metadata.always) && runAndStoreResult(runnable), ); return waitForSerial; @@ -264,10 +274,10 @@ export default class Runner extends Emittery { return Promise.all([ previous, waitForSerial.then(() => - // Concurrent runnables are kicked off after the previous serial - // 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. + // Concurrent runnables are kicked off after the previous serial + // 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. (allPassed || runnable.metadata.always) && runAndStoreResult(runnable), ), ]); @@ -551,4 +561,12 @@ export default class Runner extends Emittery { interrupt() { this.interrupted = true; } + + shouldSkipThisTestBecauseItDoesNotMatchARegexSetAtTheWatchCLI(metadata, testTitle) { + if (metadata.skipped || this.skipAllTestsInThisFile) { + return true; + } + + return this.watchModeSkipTests.shouldSkipTest(testTitle); + } } diff --git a/lib/watch-mode-skip-tests.js b/lib/watch-mode-skip-tests.js new file mode 100644 index 000000000..f8ef8b5b5 --- /dev/null +++ b/lib/watch-mode-skip-tests.js @@ -0,0 +1,56 @@ +/** + * The WatchModeSkipTests class is used to determine + * if a test should be skipped using filters provided + * by the user in watch mode. + */ +export class WatchModeSkipTests { + /** + * Properties are public to allow + * for easy sending to the worker. + */ + fileRegexOrNull = null; + testRegexOrNull = null; + + constructor(watchModeSkipTestsData = undefined) { + if (!watchModeSkipTestsData) { + return; + } + + this.fileRegexOrNull = watchModeSkipTestsData.fileRegexOrNull; + this.testRegexOrNull = watchModeSkipTestsData.testRegexOrNull; + } + + shouldSkipFile(file) { + if (this.fileRegexOrNull === null) { + return false; + } + + return !this.fileRegexOrNull.test(file); + } + + shouldSkipTest(testTitle) { + if (this.testRegexOrNull === null) { + return false; + } + + return !this.testRegexOrNull.test(testTitle); + } + + hasAnyFilters() { + return this.fileRegexOrNull !== null || this.testRegexOrNull !== null; + } + + shouldSkipEvent(event) { + return ( + this.shouldSkipFile(event.testFile) || this.shouldSkipTest(event.title) + ); + } + + replaceFileRegex(fileRegexOrNull) { + this.fileRegexOrNull = fileRegexOrNull; + } + + replaceTestRegex(testRegexOrNull) { + this.testRegexOrNull = testRegexOrNull; + } +} diff --git a/lib/watcher.js b/lib/watcher.js index 2ee7d23b2..c91d296c8 100644 --- a/lib/watcher.js +++ b/lib/watcher.js @@ -1,4 +1,5 @@ import fs from 'node:fs'; +import os from 'node:os'; import nodePath from 'node:path'; import process from 'node:process'; import v8 from 'node:v8'; @@ -11,6 +12,7 @@ import { applyTestFileFilter, classify, buildIgnoreMatcher, findTests, } from './globs.js'; import {levels as providerLevels} from './provider-manager.js'; +import {WatchModeSkipTests} from './watch-mode-skip-tests.js'; const debug = createDebug('ava:watcher'); @@ -18,7 +20,41 @@ const debug = createDebug('ava:watcher'); // to make Node.js write out interim reports in various places. const takeCoverageForSelfTests = process.env.TEST_AVA ? v8.takeCoverage : undefined; -const END_MESSAGE = chalk.gray('Type `r` and press enter to rerun tests\nType `u` and press enter to update snapshots\n'); +const endMessage = skipTests => + chalk.gray( + `Type \`p\` and press enter to filter by a filename regex pattern${os.EOL}` + + (skipTests.fileRegexOrNull === null + ? '' + : ` Current filename filter is ${skipTests.fileRegexOrNull}${os.EOL}`) + + `Type \`t\` and press enter to filter by a test name regex pattern${os.EOL}` + + (skipTests.testRegexOrNull === null + ? '' + : ` Current test filter is ${skipTests.testRegexOrNull}${os.EOL}`) + + os.EOL + + (skipTests.hasAnyFilters() + ? `Type \`a\` and press enter to rerun *all* tests${os.EOL}${os.EOL}` + + `Type \`r\` and press enter to rerun tests that match your filters${os.EOL}` + : `Type \`r\` and press enter to rerun tests${os.EOL}`) + + `Type \`u\` and press enter to update snapshots${os.EOL}` + + os.EOL, + ); + +/** + * Once mainstream adoption + * of Promise.withResolvers + * is achieved, simplify this. + * + */ +const promiseWithResolvers + = () => { + let resolve = null; + let reject = null; + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + return {promise, resolve, reject}; + }; export function available(projectDir) { try { @@ -37,15 +73,36 @@ export function available(projectDir) { export async function start({api, filter, globs, projectDir, providers, reporter, stdin, signal}) { providers = providers.filter(({level}) => level >= providerLevels.ava6); for await (const {files, ...runtimeOptions} of plan({ - api, filter, globs, projectDir, providers, stdin, abortSignal: signal, + api, + filter, + globs, + projectDir, + providers, + stdin, + abortSignal: signal, + reporter, })) { await api.run({files, filter, runtimeOptions}); reporter.endRun(); - reporter.lineWriter.writeLine(END_MESSAGE); + reporter.lineWriter.writeLine( + endMessage(runtimeOptions.watchModeSkipTestsOrUndefined), + ); + reporter.lineWriter.write(chalk.white('command > ')); } } -async function * plan({api, filter, globs, projectDir, providers, stdin, abortSignal}) { +async function * plan({ + api, + filter, + globs, + projectDir, + providers, + stdin, + abortSignal, + reporter, +}) { + const watchModeSkipTests = new WatchModeSkipTests(); + const fileTracer = new FileTracer({base: projectDir}); const isIgnored = buildIgnoreMatcher(globs); const patternFilters = filter.map(({pattern}) => pattern); @@ -141,6 +198,7 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi let firstRun = true; let runAll = true; let updateSnapshots = false; + let runSelected = false; const reset = () => { changed = new Promise(resolve => { @@ -149,18 +207,159 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi firstRun = false; runAll = false; updateSnapshots = false; + runSelected = false; }; // Support interactive commands. stdin.setEncoding('utf8'); - stdin.on('data', data => { - data = data.trim().toLowerCase(); - runAll ||= data === 'r'; - updateSnapshots ||= data === 'u'; - if (runAll || updateSnapshots) { - signalChanged({}); + + /** + * Here is how it works: + * The onDataGenerator function + * is an async generator that listens + * for lines of data from stdin. + * + * We store this generator in the field + * #data which we listen to in both + * the #listenForCommand function, + * and the promptForFilter. By keeping + * one generator instance for both + * we can have nested listeners. + */ + const _class = new (class { + #data; + + constructor() { + this.#data = this.#onDataGenerator(); + this.#listenForCommand(); } - }); + + #onDataGenerator = async function * () { + let p = promiseWithResolvers(); + /** + * Pending data allows the data + * callback to be called multiple + * times before the promise + * wakes up from its await. + */ + const pendingData = []; + stdin.on('data', async data => { + pendingData.push(data); + p.resolve(); + }); + while (true) { + /** + * Loops depend on each other, it makes + * sense to disable the eslint rule + */ + await p.promise; // eslint-disable-line no-await-in-loop + p = promiseWithResolvers(); + while (pendingData.length > 0) { + const lines = pendingData.shift().trim().split('\n'); + for (const line of lines) { + yield line; + } + } + } + }; + #listenForCommand = async () => { + for await (const data of this.#data) { + await this.#onCommand(data); + } + }; + #onCommand = async data => { + data = data.trim().toLowerCase(); + + switch (data) { + case 'r': { + runSelected = true; + break; + } + + case 'u': { + updateSnapshots = true; + break; + } + + case 'a': { + runAll = true; + break; + } + + case 'p': { + watchModeSkipTests.replaceFileRegex( + await this.#promptForFilter( + watchModeSkipTests.fileRegexOrNull, + 'filename', + ), + ); + reporter.lineWriter.write(`${os.EOL}${os.EOL}`); + reporter.lineWriter.writeLine(endMessage(watchModeSkipTests)); + reporter.lineWriter.write(chalk.white('command > ')); + runSelected = true; + break; + } + + case 't': { + watchModeSkipTests.replaceTestRegex( + await this.#promptForFilter( + watchModeSkipTests.testRegexOrNull, + 'test', + ), + ); + reporter.lineWriter.write(`${os.EOL}`); + reporter.lineWriter.writeLine(endMessage(watchModeSkipTests)); + reporter.lineWriter.write(chalk.white('command > ')); + runSelected = true; + break; + } + + default: { + return; + } + } + + if (runAll || runSelected || updateSnapshots) { + signalChanged({}); + } + }; + #promptForFilter = async (currentFilterRegexOrNull, filterName) => { + reporter.lineWriter.writeLine( + chalk.gray( + `${os.EOL}${os.EOL}Current ${filterName} filter is ${ + currentFilterRegexOrNull === null + ? 'empty' + : currentFilterRegexOrNull + }${os.EOL}`, + ), + ); + reporter.lineWriter.writeLine( + chalk.gray( + `Type the ${filterName} regex pattern then press enter:${os.EOL}` + + ` - Leave it blank and press enter to clear the filter${os.EOL}`, + ), + ); + reporter.lineWriter.write('pattern > '); + const generatorResult = (await this.#data.next()); + const patternInput = generatorResult.value.trim(); + let outFilter = null; + if (patternInput !== '') { + try { + const pattern = new RegExp(patternInput); + outFilter = pattern; + reporter.lineWriter.writeLine( + chalk.gray(`Added ${filterName} regex filter: ${pattern}${os.EOL}`), + ); + } catch (error) { + reporter.lineWriter.writeLine( + chalk.gray(`Invalid regex: ${error.message}${os.EOL}`), + ); + } + } + + return outFilter; + }; + })(); stdin.unref(); // Whether tests are currently running. Used to control when the next run @@ -400,6 +599,9 @@ async function * plan({api, filter, globs, projectDir, providers, stdin, abortSi previousFailures, runOnlyExclusive, updateSnapshots, // Value is changed by refresh() so record now. + watchModeSkipTestsOrUndefined: runAll + ? new WatchModeSkipTests() + : watchModeSkipTests, }; reset(); // Make sure the next run can be triggered. testsAreRunning = true; diff --git a/lib/worker/base.js b/lib/worker/base.js index 7e4ff4aa4..6da792c98 100644 --- a/lib/worker/base.js +++ b/lib/worker/base.js @@ -85,6 +85,7 @@ const run = async options => { serial: options.serial, snapshotDir: options.snapshotDir, updateSnapshots: options.updateSnapshots, + watchModeSkipTestsData: options.watchModeSkipTestsData, }); refs.runnerChain = runner.chain; diff --git a/test/watch-mode/basic-functionality.js b/test/watch-mode/basic-functionality.js index 33b28a6a7..98b83ce94 100644 --- a/test/watch-mode/basic-functionality.js +++ b/test/watch-mode/basic-functionality.js @@ -8,6 +8,14 @@ test('prints results and instructions', withFixture('basic'), async (t, fixture) process.send('abort-watcher'); const {stdout} = await process; t.regex(stdout, /\d+ tests? passed/); + t.regex( + stdout, + /Type `p` and press enter to filter by a filename regex pattern/, + ); + t.regex( + stdout, + /Type `t` and press enter to filter by a test name regex pattern/, + ); t.regex(stdout, /Type `r` and press enter to rerun tests/); t.regex(stdout, /Type `u` and press enter to update snapshots/); this.done(); @@ -78,3 +86,286 @@ test('can update snapshots', withFixture('basic'), async (t, fixture) => { }, }); }); + +test( + 'can filter tests by filename pattern', + withFixture('filter-files'), + async (t, fixture) => { + const test1RegexString = 'test1'; + const test1Regex = new RegExp(test1RegexString); + await fixture.watch({ + async 1({process, stats}) { + try { + // First run should run all tests + t.is(stats.selectedTestCount, 8); + t.is(stats.passed.length, 6); + + // Set a file filter to only run test1.js + process.stdin.write('p\n'); + process.stdin.write(`${test1RegexString}\n`); + return stats; + } catch { + this.done(); + } + }, + + async 2({stats}) { + try { + /** + * Only tests from test1 should run + */ + t.is(stats.selectedTestCount, 8); + t.is(stats.skipped.length, 4); + for (const skipped of stats.skipped) { + t.notRegex(skipped.file, test1Regex); + } + + t.is(stats.passed.length, 3); + for (const skipped of stats.passed) { + t.regex(skipped.file, test1Regex); + } + + t.is(stats.failed.length, 1); + for (const skipped of stats.failed) { + t.regex(skipped.file, test1Regex); + } + } catch {} + + this.done(); + }, + }); + }, +); + +test( + 'can filter tests by filename pattern and have no tests run', + withFixture('filter-files'), + async (t, fixture) => { + const test1RegexString = 'kangarookanbankentuckykendoll'; + const test1Regex = new RegExp(test1RegexString); + await fixture.watch({ + async 1({process, stats}) { + try { + // First run should run all tests + t.is(stats.selectedTestCount, 8); + t.is(stats.passed.length, 6); + + // Set a file filter to only run test1.js + process.stdin.write('p\n'); + process.stdin.write(`${test1RegexString}\n`); + return stats; + } catch { + this.done(); + } + }, + + async 2({stats}) { + try { + /** + * Only tests from test1 should run + */ + t.is(stats.selectedTestCount, 8); + t.is(stats.skipped.length, 8); + for (const skipped of stats.skipped) { + t.notRegex(skipped.file, test1Regex); + } + } catch {} + + this.done(); + }, + }); + }, +); + +test( + 'can filter tests by filename pattern, and run all tests with \'a', + withFixture('filter-files'), + async (t, fixture) => { + const test1RegexString = 'kangarookanbankentuckykendoll'; + const test1Regex = new RegExp(test1RegexString); + await fixture.watch({ + async 1({process, stats}) { + try { + // First run should run all tests + t.is(stats.selectedTestCount, 8); + t.is(stats.passed.length, 6); + + // Set a file filter to only run test1.js + process.stdin.write('p\n'); + process.stdin.write(`${test1RegexString}\n`); + return stats; + } catch { + this.done(); + } + }, + + async 2({process, stats}) { + try { + /** + * Only tests from test1 should run + */ + t.is(stats.selectedTestCount, 8); + t.is(stats.skipped.length, 8); + for (const skipped of stats.skipped) { + t.notRegex(skipped.file, test1Regex); + } + + process.stdin.write('a\n'); + } catch { + this.done(); + } + }, + async 3({stats}) { + try { + /** + * All tests should run + */ + t.is(stats.selectedTestCount, 8); + t.is(stats.passed.length, 6); + } catch {} + + this.done(); + }, + }); + }, +); + +test( + 'can filter tests by test pattern', + withFixture('filter-files'), + async (t, fixture) => { + const test1RegexString = 'bob'; + const test1Regex = new RegExp(test1RegexString); + await fixture.watch({ + async 1({process, stats}) { + try { + // First run should run all tests + t.is(stats.selectedTestCount, 8); + t.is(stats.passed.length, 6); + + // Set a file filter to only run test1.js + process.stdin.write('t\n'); + process.stdin.write(`${test1RegexString}\n`); + return stats; + } catch (error) { + console.log('e'); + console.log(error); + this.done(); + } + }, + + async 2({stats}) { + try { + /** + * Only tests from test1 should run + */ + t.is(stats.selectedTestCount, 8); + t.is(stats.skipped.length, 7); + for (const skipped of stats.skipped) { + t.notRegex(skipped.title, test1Regex); + } + + t.is(stats.passed.length, 1); + for (const skipped of stats.passed) { + t.regex(skipped.title, test1Regex); + } + } catch {} + + this.done(); + }, + }); + }, +); + +test( + 'can filter tests by test pattern and have no tests run', + withFixture('filter-files'), + async (t, fixture) => { + const test1RegexString = 'sirnotappearinginthisfilm'; + const test1Regex = new RegExp(test1RegexString); + await fixture.watch({ + async 1({process, stats}) { + try { + // First run should run all tests + t.is(stats.selectedTestCount, 8); + t.is(stats.passed.length, 6); + + // Set a file filter to only run test1.js + process.stdin.write('t\n'); + process.stdin.write(`${test1RegexString}\n`); + return stats; + } catch { + this.done(); + } + }, + + async 2({stats}) { + try { + /** + * Only tests from test1 should run + */ + t.is(stats.selectedTestCount, 8); + t.is(stats.skipped.length, 8); + for (const skipped of stats.skipped) { + t.notRegex(skipped.title, test1Regex); + } + } catch {} + + this.done(); + }, + }); + }, +); + +test( + 'can filter tests by test pattern, and run all tests with \'a', + withFixture('filter-files'), + async (t, fixture) => { + const test1RegexString = 'sirnotappearinginthisfilm'; + const test1Regex = new RegExp(test1RegexString); + await fixture.watch({ + async 1({process, stats}) { + try { + // First run should run all tests + t.is(stats.selectedTestCount, 8); + t.is(stats.passed.length, 6); + + // Set a file filter to only run test1.js + process.stdin.write('t\n'); + process.stdin.write(`${test1RegexString}\n`); + return stats; + } catch { + this.done(); + } + }, + + async 2({process, stats}) { + try { + /** + * Only tests from test1 should run + */ + t.is(stats.selectedTestCount, 8); + t.is(stats.skipped.length, 8); + for (const skipped of stats.skipped) { + t.notRegex(skipped.file, test1Regex); + } + + process.stdin.write('a\n'); + } catch { + this.done(); + } + }, + async 3({stats}) { + try { + /** + * All tests should run + */ + t.is(stats.selectedTestCount, 8); + t.is(stats.passed.length, 6); + } catch {} + + this.done(); + }, + }); + }, +); diff --git a/test/watch-mode/fixtures/filter-files/ava.config.js b/test/watch-mode/fixtures/filter-files/ava.config.js new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/test/watch-mode/fixtures/filter-files/ava.config.js @@ -0,0 +1 @@ +export default {}; diff --git a/test/watch-mode/fixtures/filter-files/package.json b/test/watch-mode/fixtures/filter-files/package.json new file mode 100644 index 000000000..47dc78d39 --- /dev/null +++ b/test/watch-mode/fixtures/filter-files/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/test/watch-mode/fixtures/filter-files/test1.test.js b/test/watch-mode/fixtures/filter-files/test1.test.js new file mode 100644 index 000000000..72df83822 --- /dev/null +++ b/test/watch-mode/fixtures/filter-files/test1.test.js @@ -0,0 +1,20 @@ +const {default: test} = await import(process.env.TEST_AVA_IMPORT_FROM); // This fixture is copied to a temporary directory, so import AVA through its configured path. + +test("alice", (t) => { + t.pass(); +}); + +test("bob", async (t) => { + const bar = Promise.resolve("bar"); + t.is(await bar, "bar"); +}); + +test("catherine", async (t) => { + const { promise, resolve } = Promise.withResolvers(); + setTimeout(resolve, 50); + return promise.then(() => t.pass()); +}); + +test("david", async (t) => { + t.is(1, 2); +}); \ No newline at end of file diff --git a/test/watch-mode/fixtures/filter-files/test2.test.js b/test/watch-mode/fixtures/filter-files/test2.test.js new file mode 100644 index 000000000..168981bb7 --- /dev/null +++ b/test/watch-mode/fixtures/filter-files/test2.test.js @@ -0,0 +1,20 @@ +const {default: test} = await import(process.env.TEST_AVA_IMPORT_FROM); // This fixture is copied to a temporary directory, so import AVA through its configured path. + +test("emma", (t) => { + t.pass(); +}); + +test("frank", async (t) => { + const bar = Promise.resolve("bar"); + t.is(await bar, "bar"); +}); + +test("gina", async (t) => { + const { promise, resolve } = Promise.withResolvers(); + setTimeout(resolve, 50); + return promise.then(() => t.pass()); +}); + +test("harry", async (t) => { + t.is(1, 2); +}); \ No newline at end of file From 8ecd51ded2f581314e5952e6fa9998cf79c58c30 Mon Sep 17 00:00:00 2001 From: Michael Mulet Date: Wed, 26 Mar 2025 20:47:16 -0500 Subject: [PATCH 2/9] fix tests on onlder node versions --- test/watch-mode/fixtures/filter-files/test1.test.js | 4 +--- test/watch-mode/fixtures/filter-files/test2.test.js | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/test/watch-mode/fixtures/filter-files/test1.test.js b/test/watch-mode/fixtures/filter-files/test1.test.js index 72df83822..527e19615 100644 --- a/test/watch-mode/fixtures/filter-files/test1.test.js +++ b/test/watch-mode/fixtures/filter-files/test1.test.js @@ -10,9 +10,7 @@ test("bob", async (t) => { }); test("catherine", async (t) => { - const { promise, resolve } = Promise.withResolvers(); - setTimeout(resolve, 50); - return promise.then(() => t.pass()); + t.is(1, 1); }); test("david", async (t) => { diff --git a/test/watch-mode/fixtures/filter-files/test2.test.js b/test/watch-mode/fixtures/filter-files/test2.test.js index 168981bb7..e82f004d7 100644 --- a/test/watch-mode/fixtures/filter-files/test2.test.js +++ b/test/watch-mode/fixtures/filter-files/test2.test.js @@ -10,9 +10,7 @@ test("frank", async (t) => { }); test("gina", async (t) => { - const { promise, resolve } = Promise.withResolvers(); - setTimeout(resolve, 50); - return promise.then(() => t.pass()); + t.is(1, 1); }); test("harry", async (t) => { From 0d78fd3c3541a1fca30dfba3b8e2d740884be4a5 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Fri, 4 Apr 2025 14:04:55 +0200 Subject: [PATCH 3/9] Format documentation --- docs/recipes/watch-mode.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/recipes/watch-mode.md b/docs/recipes/watch-mode.md index 8bab4de52..d861ef31e 100644 --- a/docs/recipes/watch-mode.md +++ b/docs/recipes/watch-mode.md @@ -35,11 +35,15 @@ export default { If your tests write to disk they may trigger the watcher to rerun your tests. Configuring additional ignore patterns helps avoid this. ### Filter tests while watching -You may also filter tests while watching by using the cli. For example, after running + +You may also filter tests while watching by using the CLI. For example, after running + ```console -$ npx ava --watch +npx ava --watch ``` -You will see a prompt like this : + +You will see a prompt like this: + ```console Type `p` and press enter to filter by a filename regex pattern [Current filename filter is $pattern] @@ -48,20 +52,21 @@ Type `t` and press enter to filter by a test name regex pattern [Type `a` and press enter to run *all* tests] (Type `r` and press enter to rerun tests || - Type \`r\` and press enter to rerun tests that match your filters) + Type `r` and press enter to rerun tests that match your filters) Type `u` and press enter to update snapshots -command > +command > ``` + So, to run only tests numbered like + - foo23434 - foo4343 - foo93823 -You can type `t` and press enter, then type `foo\d+` and press enter. -This will then run all tests that match that pattern. -Afterwards you can use the `r` command to run the matched tests again, -or `a` command to run **all** tests. +You can type `t` and press enter, then type `foo\d+` and press enter. This will then run all tests that match that pattern. + +Afterwards you can use the `r` command to run the matched tests again, or `a` command to run **all** tests. ## Dependency tracking From b72a9dd94a8630d6ac52c837c798b4d7513ec5b9 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Fri, 4 Apr 2025 14:20:02 +0200 Subject: [PATCH 4/9] Add newline at EOF --- test/watch-mode/fixtures/filter-files/package.json | 2 +- test/watch-mode/fixtures/filter-files/test1.test.js | 2 +- test/watch-mode/fixtures/filter-files/test2.test.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/watch-mode/fixtures/filter-files/package.json b/test/watch-mode/fixtures/filter-files/package.json index 47dc78d39..bedb411a9 100644 --- a/test/watch-mode/fixtures/filter-files/package.json +++ b/test/watch-mode/fixtures/filter-files/package.json @@ -1,3 +1,3 @@ { "type": "module" -} \ No newline at end of file +} diff --git a/test/watch-mode/fixtures/filter-files/test1.test.js b/test/watch-mode/fixtures/filter-files/test1.test.js index 527e19615..6e28c015f 100644 --- a/test/watch-mode/fixtures/filter-files/test1.test.js +++ b/test/watch-mode/fixtures/filter-files/test1.test.js @@ -15,4 +15,4 @@ test("catherine", async (t) => { test("david", async (t) => { t.is(1, 2); -}); \ No newline at end of file +}); diff --git a/test/watch-mode/fixtures/filter-files/test2.test.js b/test/watch-mode/fixtures/filter-files/test2.test.js index e82f004d7..0b3304753 100644 --- a/test/watch-mode/fixtures/filter-files/test2.test.js +++ b/test/watch-mode/fixtures/filter-files/test2.test.js @@ -15,4 +15,4 @@ test("gina", async (t) => { test("harry", async (t) => { t.is(1, 2); -}); \ No newline at end of file +}); From a3cce757c160b59e73d144a337b96bc462c1695e Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Fri, 4 Apr 2025 14:39:38 +0200 Subject: [PATCH 5/9] Fix error handling in watcher tests If the previous handler failed, perhaps due to an assertion, it wouldn't trigger the next run and the tests would hang. Fix by including a failure of the previous handler in the await condition for the next run. --- test/watch-mode/helpers/watch.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/test/watch-mode/helpers/watch.js b/test/watch-mode/helpers/watch.js index 32c00ced1..b42ce3d4e 100644 --- a/test/watch-mode/helpers/watch.js +++ b/test/watch-mode/helpers/watch.js @@ -12,6 +12,17 @@ import {cwd, exec} from '../../helpers/exec.js'; export const test = available(fileURLToPath(import.meta.url)) ? ava : ava.skip; export const serial = available(fileURLToPath(import.meta.url)) ? ava.serial : ava.serial.skip; +/** + * Races between `promises` and returns the result, unless `possiblyErroring` rejects first. + * + * `possiblyErroring` may be any value and is ignored, unless it rejects. + */ +const raceUnlessError = async (possiblyErroring, ...promises) => { + const race = Promise.race(promises); + const intermediate = await Promise.race([Promise.resolve(possiblyErroring).then(() => raceUnlessError), race]); + return intermediate === raceUnlessError ? race : intermediate; +}; + export const withFixture = fixture => async (t, task) => { let completedTask = false; await temporaryDirectoryTask(async dir => { @@ -124,7 +135,7 @@ export const withFixture = fixture => async (t, task) => { try { let nextResult = results.next(); while (!isDone) { // eslint-disable-line no-unmodified-loop-condition - const item = await Promise.race([nextResult, idlePromise, donePromise]); // eslint-disable-line no-await-in-loop + const item = await raceUnlessError(pendingState, nextResult, idlePromise, donePromise); // eslint-disable-line no-await-in-loop process ??= item.value?.process; if (item.value) { From b0762b0fc2f453a5d8bc092b71f9c9387da91b85 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Fri, 4 Apr 2025 14:39:54 +0200 Subject: [PATCH 6/9] Remove unnecessary this.done() calls in catch blocks --- test/watch-mode/basic-functionality.js | 292 +++++++++++-------------- 1 file changed, 123 insertions(+), 169 deletions(-) diff --git a/test/watch-mode/basic-functionality.js b/test/watch-mode/basic-functionality.js index 98b83ce94..41010b051 100644 --- a/test/watch-mode/basic-functionality.js +++ b/test/watch-mode/basic-functionality.js @@ -95,41 +95,35 @@ test( const test1Regex = new RegExp(test1RegexString); await fixture.watch({ async 1({process, stats}) { - try { - // First run should run all tests - t.is(stats.selectedTestCount, 8); - t.is(stats.passed.length, 6); - - // Set a file filter to only run test1.js - process.stdin.write('p\n'); - process.stdin.write(`${test1RegexString}\n`); - return stats; - } catch { - this.done(); - } + // First run should run all tests + t.is(stats.selectedTestCount, 8); + t.is(stats.passed.length, 6); + + // Set a file filter to only run test1.js + process.stdin.write('p\n'); + process.stdin.write(`${test1RegexString}\n`); + return stats; }, async 2({stats}) { - try { - /** - * Only tests from test1 should run - */ - t.is(stats.selectedTestCount, 8); - t.is(stats.skipped.length, 4); - for (const skipped of stats.skipped) { - t.notRegex(skipped.file, test1Regex); - } - - t.is(stats.passed.length, 3); - for (const skipped of stats.passed) { - t.regex(skipped.file, test1Regex); - } - - t.is(stats.failed.length, 1); - for (const skipped of stats.failed) { - t.regex(skipped.file, test1Regex); - } - } catch {} + /** + * Only tests from test1 should run + */ + t.is(stats.selectedTestCount, 8); + t.is(stats.skipped.length, 4); + for (const skipped of stats.skipped) { + t.notRegex(skipped.file, test1Regex); + } + + t.is(stats.passed.length, 3); + for (const skipped of stats.passed) { + t.regex(skipped.file, test1Regex); + } + + t.is(stats.failed.length, 1); + for (const skipped of stats.failed) { + t.regex(skipped.file, test1Regex); + } this.done(); }, @@ -145,31 +139,25 @@ test( const test1Regex = new RegExp(test1RegexString); await fixture.watch({ async 1({process, stats}) { - try { - // First run should run all tests - t.is(stats.selectedTestCount, 8); - t.is(stats.passed.length, 6); - - // Set a file filter to only run test1.js - process.stdin.write('p\n'); - process.stdin.write(`${test1RegexString}\n`); - return stats; - } catch { - this.done(); - } + // First run should run all tests + t.is(stats.selectedTestCount, 8); + t.is(stats.passed.length, 6); + + // Set a file filter to only run test1.js + process.stdin.write('p\n'); + process.stdin.write(`${test1RegexString}\n`); + return stats; }, async 2({stats}) { - try { - /** - * Only tests from test1 should run - */ - t.is(stats.selectedTestCount, 8); - t.is(stats.skipped.length, 8); - for (const skipped of stats.skipped) { - t.notRegex(skipped.file, test1Regex); - } - } catch {} + /** + * Only tests from test1 should run + */ + t.is(stats.selectedTestCount, 8); + t.is(stats.skipped.length, 8); + for (const skipped of stats.skipped) { + t.notRegex(skipped.file, test1Regex); + } this.done(); }, @@ -185,44 +173,34 @@ test( const test1Regex = new RegExp(test1RegexString); await fixture.watch({ async 1({process, stats}) { - try { - // First run should run all tests - t.is(stats.selectedTestCount, 8); - t.is(stats.passed.length, 6); - - // Set a file filter to only run test1.js - process.stdin.write('p\n'); - process.stdin.write(`${test1RegexString}\n`); - return stats; - } catch { - this.done(); - } + // First run should run all tests + t.is(stats.selectedTestCount, 8); + t.is(stats.passed.length, 6); + + // Set a file filter to only run test1.js + process.stdin.write('p\n'); + process.stdin.write(`${test1RegexString}\n`); + return stats; }, async 2({process, stats}) { - try { - /** - * Only tests from test1 should run - */ - t.is(stats.selectedTestCount, 8); - t.is(stats.skipped.length, 8); - for (const skipped of stats.skipped) { - t.notRegex(skipped.file, test1Regex); - } - - process.stdin.write('a\n'); - } catch { - this.done(); + /** + * Only tests from test1 should run + */ + t.is(stats.selectedTestCount, 8); + t.is(stats.skipped.length, 8); + for (const skipped of stats.skipped) { + t.notRegex(skipped.file, test1Regex); } + + process.stdin.write('a\n'); }, async 3({stats}) { - try { - /** - * All tests should run - */ - t.is(stats.selectedTestCount, 8); - t.is(stats.passed.length, 6); - } catch {} + /** + * All tests should run + */ + t.is(stats.selectedTestCount, 8); + t.is(stats.passed.length, 6); this.done(); }, @@ -238,38 +216,30 @@ test( const test1Regex = new RegExp(test1RegexString); await fixture.watch({ async 1({process, stats}) { - try { - // First run should run all tests - t.is(stats.selectedTestCount, 8); - t.is(stats.passed.length, 6); - - // Set a file filter to only run test1.js - process.stdin.write('t\n'); - process.stdin.write(`${test1RegexString}\n`); - return stats; - } catch (error) { - console.log('e'); - console.log(error); - this.done(); - } + // First run should run all tests + t.is(stats.selectedTestCount, 8); + t.is(stats.passed.length, 6); + + // Set a file filter to only run test1.js + process.stdin.write('t\n'); + process.stdin.write(`${test1RegexString}\n`); + return stats; }, async 2({stats}) { - try { - /** - * Only tests from test1 should run - */ - t.is(stats.selectedTestCount, 8); - t.is(stats.skipped.length, 7); - for (const skipped of stats.skipped) { - t.notRegex(skipped.title, test1Regex); - } - - t.is(stats.passed.length, 1); - for (const skipped of stats.passed) { - t.regex(skipped.title, test1Regex); - } - } catch {} + /** + * Only tests from test1 should run + */ + t.is(stats.selectedTestCount, 8); + t.is(stats.skipped.length, 7); + for (const skipped of stats.skipped) { + t.notRegex(skipped.title, test1Regex); + } + + t.is(stats.passed.length, 1); + for (const skipped of stats.passed) { + t.regex(skipped.title, test1Regex); + } this.done(); }, @@ -285,31 +255,25 @@ test( const test1Regex = new RegExp(test1RegexString); await fixture.watch({ async 1({process, stats}) { - try { - // First run should run all tests - t.is(stats.selectedTestCount, 8); - t.is(stats.passed.length, 6); - - // Set a file filter to only run test1.js - process.stdin.write('t\n'); - process.stdin.write(`${test1RegexString}\n`); - return stats; - } catch { - this.done(); - } + // First run should run all tests + t.is(stats.selectedTestCount, 8); + t.is(stats.passed.length, 6); + + // Set a file filter to only run test1.js + process.stdin.write('t\n'); + process.stdin.write(`${test1RegexString}\n`); + return stats; }, async 2({stats}) { - try { - /** - * Only tests from test1 should run - */ - t.is(stats.selectedTestCount, 8); - t.is(stats.skipped.length, 8); - for (const skipped of stats.skipped) { - t.notRegex(skipped.title, test1Regex); - } - } catch {} + /** + * Only tests from test1 should run + */ + t.is(stats.selectedTestCount, 8); + t.is(stats.skipped.length, 8); + for (const skipped of stats.skipped) { + t.notRegex(skipped.title, test1Regex); + } this.done(); }, @@ -325,44 +289,34 @@ test( const test1Regex = new RegExp(test1RegexString); await fixture.watch({ async 1({process, stats}) { - try { - // First run should run all tests - t.is(stats.selectedTestCount, 8); - t.is(stats.passed.length, 6); - - // Set a file filter to only run test1.js - process.stdin.write('t\n'); - process.stdin.write(`${test1RegexString}\n`); - return stats; - } catch { - this.done(); - } + // First run should run all tests + t.is(stats.selectedTestCount, 8); + t.is(stats.passed.length, 6); + + // Set a file filter to only run test1.js + process.stdin.write('t\n'); + process.stdin.write(`${test1RegexString}\n`); + return stats; }, async 2({process, stats}) { - try { - /** - * Only tests from test1 should run - */ - t.is(stats.selectedTestCount, 8); - t.is(stats.skipped.length, 8); - for (const skipped of stats.skipped) { - t.notRegex(skipped.file, test1Regex); - } - - process.stdin.write('a\n'); - } catch { - this.done(); + /** + * Only tests from test1 should run + */ + t.is(stats.selectedTestCount, 8); + t.is(stats.skipped.length, 8); + for (const skipped of stats.skipped) { + t.notRegex(skipped.file, test1Regex); } + + process.stdin.write('a\n'); }, async 3({stats}) { - try { - /** - * All tests should run - */ - t.is(stats.selectedTestCount, 8); - t.is(stats.passed.length, 6); - } catch {} + /** + * All tests should run + */ + t.is(stats.selectedTestCount, 8); + t.is(stats.passed.length, 6); this.done(); }, From 5195cbe4ce23e222c18850f2ab25e3fc851519ed Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Fri, 4 Apr 2025 14:41:01 +0200 Subject: [PATCH 7/9] Remove unnecessary multiline comments --- test/watch-mode/basic-functionality.js | 32 +++++++------------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/test/watch-mode/basic-functionality.js b/test/watch-mode/basic-functionality.js index 41010b051..a22fc42e9 100644 --- a/test/watch-mode/basic-functionality.js +++ b/test/watch-mode/basic-functionality.js @@ -106,9 +106,7 @@ test( }, async 2({stats}) { - /** - * Only tests from test1 should run - */ + // Only tests from test1 should run t.is(stats.selectedTestCount, 8); t.is(stats.skipped.length, 4); for (const skipped of stats.skipped) { @@ -150,9 +148,7 @@ test( }, async 2({stats}) { - /** - * Only tests from test1 should run - */ + // Only tests from test1 should run t.is(stats.selectedTestCount, 8); t.is(stats.skipped.length, 8); for (const skipped of stats.skipped) { @@ -184,9 +180,7 @@ test( }, async 2({process, stats}) { - /** - * Only tests from test1 should run - */ + // Only tests from test1 should run t.is(stats.selectedTestCount, 8); t.is(stats.skipped.length, 8); for (const skipped of stats.skipped) { @@ -196,9 +190,7 @@ test( process.stdin.write('a\n'); }, async 3({stats}) { - /** - * All tests should run - */ + // All tests should run t.is(stats.selectedTestCount, 8); t.is(stats.passed.length, 6); @@ -227,9 +219,7 @@ test( }, async 2({stats}) { - /** - * Only tests from test1 should run - */ + // Only tests from test1 should run t.is(stats.selectedTestCount, 8); t.is(stats.skipped.length, 7); for (const skipped of stats.skipped) { @@ -266,9 +256,7 @@ test( }, async 2({stats}) { - /** - * Only tests from test1 should run - */ + // Only tests from test1 should run t.is(stats.selectedTestCount, 8); t.is(stats.skipped.length, 8); for (const skipped of stats.skipped) { @@ -300,9 +288,7 @@ test( }, async 2({process, stats}) { - /** - * Only tests from test1 should run - */ + // Only tests from test1 should run t.is(stats.selectedTestCount, 8); t.is(stats.skipped.length, 8); for (const skipped of stats.skipped) { @@ -312,9 +298,7 @@ test( process.stdin.write('a\n'); }, async 3({stats}) { - /** - * All tests should run - */ + // All tests should run t.is(stats.selectedTestCount, 8); t.is(stats.passed.length, 6); From 16630d4438515c92263c06cc51466631251dda24 Mon Sep 17 00:00:00 2001 From: Michael Mulet Date: Mon, 7 Apr 2025 16:09:30 -0500 Subject: [PATCH 8/9] implemented fixes --- lib/api.js | 5 +- lib/interactive-filter.js | 78 +++++ lib/reporters/default.js | 13 +- lib/runner.js | 35 +- lib/watch-mode-skip-tests.js | 56 ---- lib/watcher.js | 304 ++++++++---------- lib/worker/base.js | 2 +- package-lock.json | 10 + package.json | 1 + test/watch-mode/basic-functionality.js | 69 ++-- .../fixtures/filter-files/test1.test.js | 7 +- .../fixtures/filter-files/test2.test.js | 7 +- 12 files changed, 264 insertions(+), 323 deletions(-) create mode 100644 lib/interactive-filter.js delete mode 100644 lib/watch-mode-skip-tests.js diff --git a/lib/api.js b/lib/api.js index e32ad724c..7184132f6 100644 --- a/lib/api.js +++ b/lib/api.js @@ -162,6 +162,8 @@ export default class Api extends Emittery { setupOrGlobError = error; } + selectedFiles = selectedFiles.filter(file => runtimeOptions.interactiveFilter?.canSelectTestsInThisFile(file) ?? true); + const selectionInsights = { filter, ignoredFilterPatternFiles: selectedFiles.ignoredFilterPatternFiles ?? [], @@ -206,7 +208,6 @@ export default class Api extends Emittery { runOnlyExclusive: runtimeOptions.runOnlyExclusive === true, firstRun: runtimeOptions.firstRun ?? true, status: runStatus, - watchModeSkipTestsOrUndefined: runtimeOptions.watchModeSkipTestsOrUndefined, }); if (setupOrGlobError) { @@ -274,7 +275,7 @@ export default class Api extends Emittery { providerStates, lineNumbers, recordNewSnapshots: !isCi, - watchModeSkipTestsData: runtimeOptions.watchModeSkipTestsOrUndefined, + interactiveFilterData: runtimeOptions.interactiveFilter?.getData(), // If we're looking for matches, run every single test process in exclusive-only mode runOnlyExclusive: apiOptions.match.length > 0 || runtimeOptions.runOnlyExclusive === true, }; diff --git a/lib/interactive-filter.js b/lib/interactive-filter.js new file mode 100644 index 000000000..5759f5329 --- /dev/null +++ b/lib/interactive-filter.js @@ -0,0 +1,78 @@ +/** + * The InteractiveFilter class is used to determine + * if a test should be skipped using filters provided + * by the user in watch mode. + */ +export class InteractiveFilter { + #filepathRegex = null; + + replaceFilepathRegex(filepathRegex) { + const filterHasChanged = !this.#regexesAreEqual(this.#filepathRegex, filepathRegex); + this.#filepathRegex = filepathRegex; + return filterHasChanged; + } + + #testTitleRegex = null; + + replaceTestTitleRegex(testTitleRegex) { + const filterHasChanged = !this.#regexesAreEqual(this.#testTitleRegex, testTitleRegex); + this.#testTitleRegex = testTitleRegex; + return filterHasChanged; + } + + #regexesAreEqual(a, b) { + return a?.source === b?.source && a?.flags === b?.flags; + } + + constructor(interactiveFilterData = undefined) { + if (!interactiveFilterData) { + return; + } + + this.#filepathRegex = interactiveFilterData.filepathRegex; + this.#testTitleRegex = interactiveFilterData.testTitleRegex; + } + + getData() { + return { + filepathRegex: this.#filepathRegex, + testTitleRegex: this.#testTitleRegex, + }; + } + + printFilePathRegex() { + if (!this.#filepathRegex) { + return ''; + } + + return `Current filename filter is ${this.#filepathRegex}`; + } + + printTestTitleRegex() { + if (!this.#testTitleRegex) { + return ''; + } + + return `Current test title filter is ${this.#testTitleRegex}`; + } + + shouldSkipThisFile(file) { + if (this.#filepathRegex === null) { + return false; + } + + return !this.#filepathRegex.test(file); + } + + canSelectTestsInThisFile(file) { + return this.#filepathRegex?.test(file) ?? true; + } + + shouldSelectTest(testTitle) { + return this.#testTitleRegex?.test(testTitle) ?? true; + } + + hasAnyFilters() { + return this.#filepathRegex !== null || this.#testTitleRegex !== null; + } +} diff --git a/lib/reporters/default.js b/lib/reporters/default.js index 3081ac1a0..5402cbd71 100644 --- a/lib/reporters/default.js +++ b/lib/reporters/default.js @@ -139,7 +139,6 @@ export default class Reporter { this.previousFailures = plan.previousFailures; this.emptyParallelRun = plan.status.emptyParallelRun; this.selectionInsights = plan.status.selectionInsights; - this.watchModeSkipTestsOrUndefined = plan.watchModeSkipTestsOrUndefined; if (this.watching || plan.files.length > 1) { this.prefixTitle = (testFile, title) => prefixTitle(this.extensions, plan.filePathPrefix, testFile, title); @@ -252,13 +251,11 @@ export default class Reporter { case 'selected-test': { if (event.skip) { - if (!this.watchModeSkipTestsOrUndefined?.shouldSkipEvent(event)) { - this.lineWriter.writeLine( - colors.skip( - `- [skip] ${this.prefixTitle(event.testFile, event.title)}`, - ), - ); - } + this.lineWriter.writeLine( + colors.skip( + `- [skip] ${this.prefixTitle(event.testFile, event.title)}`, + ), + ); } else if (event.todo) { this.lineWriter.writeLine( colors.todo( diff --git a/lib/runner.js b/lib/runner.js index e5f3011f1..0215e8d34 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -6,11 +6,11 @@ import {matcher} from 'matcher'; import ContextRef from './context-ref.js'; import createChain from './create-chain.js'; +import {InteractiveFilter} from './interactive-filter.js'; import parseTestArgs from './parse-test-args.js'; import serializeError from './serialize-error.js'; import {load as loadSnapshots, determineSnapshotDir} from './snapshot-manager.js'; import Runnable from './test.js'; -import {WatchModeSkipTests} from './watch-mode-skip-tests.js'; import {waitForReady} from './worker/state.cjs'; const makeFileURL = file => file.startsWith('file://') ? file : pathToFileURL(file).toString(); @@ -30,10 +30,8 @@ export default class Runner extends Emittery { this.serial = options.serial === true; this.snapshotDir = options.snapshotDir; this.updateSnapshots = options.updateSnapshots; - this.watchModeSkipTests - = new WatchModeSkipTests(options.watchModeSkipTestsData); - this.skipAllTestsInThisFile = this.watchModeSkipTests - .shouldSkipFile(this.file); + this.interactiveFilter + = new InteractiveFilter(options.interactiveFilterData); this.activeRunnables = new Set(); this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this); @@ -97,13 +95,11 @@ export default class Runner extends Emittery { const {args, implementation, title} = parseTestArgs(testArgs); if ( - this.shouldSkipThisTestBecauseItDoesNotMatchARegexSetAtTheWatchCLI(metadata, title.value) + this.interactiveFilter.shouldSelectTest(title.value) ) { - metadata.skipped = true; - } - - if (this.checkSelectedByLineNumbers) { - metadata.selected = this.checkSelectedByLineNumbers(); + metadata.selected = this.checkSelectedByLineNumbers ? this.checkSelectedByLineNumbers() : true; + } else { + metadata.selected = false; } if (metadata.todo) { @@ -190,7 +186,8 @@ export default class Runner extends Emittery { }, { serial: false, exclusive: false, - skipped: this.skipAllTestsInThisFile, + skipped: false, + selected: false, todo: false, failing: false, callback: false, @@ -421,7 +418,7 @@ export default class Runner extends Emittery { continue; } - if (this.checkSelectedByLineNumbers && !task.metadata.selected) { + if (!task.metadata.selected) { this.snapshots.skipBlock(task.title, task.metadata.taskIndex); continue; } @@ -447,7 +444,7 @@ export default class Runner extends Emittery { continue; } - if (this.checkSelectedByLineNumbers && !task.metadata.selected) { + if (!task.metadata.selected) { this.snapshots.skipBlock(task.title, task.metadata.taskIndex); continue; } @@ -474,7 +471,7 @@ export default class Runner extends Emittery { continue; } - if (this.checkSelectedByLineNumbers && !task.metadata.selected) { + if (!task.metadata.selected) { continue; } @@ -561,12 +558,4 @@ export default class Runner extends Emittery { interrupt() { this.interrupted = true; } - - shouldSkipThisTestBecauseItDoesNotMatchARegexSetAtTheWatchCLI(metadata, testTitle) { - if (metadata.skipped || this.skipAllTestsInThisFile) { - return true; - } - - return this.watchModeSkipTests.shouldSkipTest(testTitle); - } } diff --git a/lib/watch-mode-skip-tests.js b/lib/watch-mode-skip-tests.js deleted file mode 100644 index f8ef8b5b5..000000000 --- a/lib/watch-mode-skip-tests.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * The WatchModeSkipTests class is used to determine - * if a test should be skipped using filters provided - * by the user in watch mode. - */ -export class WatchModeSkipTests { - /** - * Properties are public to allow - * for easy sending to the worker. - */ - fileRegexOrNull = null; - testRegexOrNull = null; - - constructor(watchModeSkipTestsData = undefined) { - if (!watchModeSkipTestsData) { - return; - } - - this.fileRegexOrNull = watchModeSkipTestsData.fileRegexOrNull; - this.testRegexOrNull = watchModeSkipTestsData.testRegexOrNull; - } - - shouldSkipFile(file) { - if (this.fileRegexOrNull === null) { - return false; - } - - return !this.fileRegexOrNull.test(file); - } - - shouldSkipTest(testTitle) { - if (this.testRegexOrNull === null) { - return false; - } - - return !this.testRegexOrNull.test(testTitle); - } - - hasAnyFilters() { - return this.fileRegexOrNull !== null || this.testRegexOrNull !== null; - } - - shouldSkipEvent(event) { - return ( - this.shouldSkipFile(event.testFile) || this.shouldSkipTest(event.title) - ); - } - - replaceFileRegex(fileRegexOrNull) { - this.fileRegexOrNull = fileRegexOrNull; - } - - replaceTestRegex(testRegexOrNull) { - this.testRegexOrNull = testRegexOrNull; - } -} diff --git a/lib/watcher.js b/lib/watcher.js index c91d296c8..aad804745 100644 --- a/lib/watcher.js +++ b/lib/watcher.js @@ -6,13 +6,14 @@ import v8 from 'node:v8'; import {nodeFileTrace} from '@vercel/nft'; import createDebug from 'debug'; +import split2 from 'split2'; import {chalk} from './chalk.js'; import { applyTestFileFilter, classify, buildIgnoreMatcher, findTests, } from './globs.js'; +import {InteractiveFilter} from './interactive-filter.js'; import {levels as providerLevels} from './provider-manager.js'; -import {WatchModeSkipTests} from './watch-mode-skip-tests.js'; const debug = createDebug('ava:watcher'); @@ -20,42 +21,38 @@ const debug = createDebug('ava:watcher'); // to make Node.js write out interim reports in various places. const takeCoverageForSelfTests = process.env.TEST_AVA ? v8.takeCoverage : undefined; -const endMessage = skipTests => - chalk.gray( - `Type \`p\` and press enter to filter by a filename regex pattern${os.EOL}` - + (skipTests.fileRegexOrNull === null - ? '' - : ` Current filename filter is ${skipTests.fileRegexOrNull}${os.EOL}`) - + `Type \`t\` and press enter to filter by a test name regex pattern${os.EOL}` - + (skipTests.testRegexOrNull === null - ? '' - : ` Current test filter is ${skipTests.testRegexOrNull}${os.EOL}`) - + os.EOL - + (skipTests.hasAnyFilters() - ? `Type \`a\` and press enter to rerun *all* tests${os.EOL}${os.EOL}` - + `Type \`r\` and press enter to rerun tests that match your filters${os.EOL}` - : `Type \`r\` and press enter to rerun tests${os.EOL}`) - + `Type \`u\` and press enter to update snapshots${os.EOL}` - + os.EOL, - ); - -/** - * Once mainstream adoption - * of Promise.withResolvers - * is achieved, simplify this. - * - */ -const promiseWithResolvers - = () => { - let resolve = null; - let reject = null; - const promise = new Promise((_resolve, _reject) => { - resolve = _resolve; - reject = _reject; - }); - return {promise, resolve, reject}; +const reportEndMessage = (reporter, interactiveFilter) => { + const writeLine = message => { + reporter.lineWriter.writeLine(chalk.gray(message)); }; + writeLine('Type `p` and press enter to filter by a filepath regex pattern'); + + const filePathRegex = interactiveFilter.printFilePathRegex(); + if (filePathRegex) { + writeLine(` ${filePathRegex}`); + } + + writeLine('Type `t` and press enter to filter by a test title regex pattern'); + + const testTitleRegex = interactiveFilter.printTestTitleRegex(); + if (testTitleRegex) { + writeLine(` ${testTitleRegex}`); + } + + writeLine(os.EOL); + if (interactiveFilter.hasAnyFilters()) { + writeLine('Type `a` and press enter to rerun *all* tests'); + writeLine('Type `r` and press enter to rerun tests that match your filters'); + } else { + writeLine('Type `r` and press enter to rerun tests'); + } + + writeLine('Type `u` and press enter to update snapshots'); + writeLine(os.EOL); + reporter.lineWriter.write(chalk.white('command > ')); +}; + export function available(projectDir) { try { fs.watch(projectDir, {persistent: false, recursive: true, signal: AbortSignal.abort()}); @@ -84,10 +81,6 @@ export async function start({api, filter, globs, projectDir, providers, reporter })) { await api.run({files, filter, runtimeOptions}); reporter.endRun(); - reporter.lineWriter.writeLine( - endMessage(runtimeOptions.watchModeSkipTestsOrUndefined), - ); - reporter.lineWriter.write(chalk.white('command > ')); } } @@ -101,7 +94,7 @@ async function * plan({ abortSignal, reporter, }) { - const watchModeSkipTests = new WatchModeSkipTests(); + const interactiveFilter = new InteractiveFilter(); const fileTracer = new FileTracer({base: projectDir}); const isIgnored = buildIgnoreMatcher(globs); @@ -213,153 +206,111 @@ async function * plan({ // Support interactive commands. stdin.setEncoding('utf8'); - /** - * Here is how it works: - * The onDataGenerator function - * is an async generator that listens - * for lines of data from stdin. - * - * We store this generator in the field - * #data which we listen to in both - * the #listenForCommand function, - * and the promptForFilter. By keeping - * one generator instance for both - * we can have nested listeners. - */ - const _class = new (class { - #data; - - constructor() { - this.#data = this.#onDataGenerator(); - this.#listenForCommand(); + const data = (async function * () { + for await (const line of stdin.pipe(split2())) { + yield line.trim(); } + })(); - #onDataGenerator = async function * () { - let p = promiseWithResolvers(); - /** - * Pending data allows the data - * callback to be called multiple - * times before the promise - * wakes up from its await. - */ - const pendingData = []; - stdin.on('data', async data => { - pendingData.push(data); - p.resolve(); - }); - while (true) { - /** - * Loops depend on each other, it makes - * sense to disable the eslint rule - */ - await p.promise; // eslint-disable-line no-await-in-loop - p = promiseWithResolvers(); - while (pendingData.length > 0) { - const lines = pendingData.shift().trim().split('\n'); - for (const line of lines) { - yield line; - } - } - } - }; - #listenForCommand = async () => { - for await (const data of this.#data) { - await this.#onCommand(data); + const promptForFilter = async (currentFilterRegexOrNull, filterName) => { + reporter.lineWriter.writeLine( + chalk.gray( + `${os.EOL}${os.EOL}Current ${filterName} filter is ${ + currentFilterRegexOrNull === null + ? 'empty' + : currentFilterRegexOrNull + }${os.EOL}`, + ), + ); + reporter.lineWriter.writeLine( + chalk.gray( + `Type the ${filterName} regex pattern then press enter:${os.EOL}` + + ` - Leave it blank and press enter to clear the filter${os.EOL}`, + ), + ); + reporter.lineWriter.write('pattern > '); + const generatorResult = (await data.next()); + const patternInput = generatorResult.value.trim(); + let outFilter = null; + if (patternInput !== '') { + try { + const pattern = new RegExp(patternInput); + outFilter = pattern; + reporter.lineWriter.writeLine( + chalk.gray(`Added ${filterName} regex filter: ${pattern}${os.EOL}`), + ); + } catch (error) { + reporter.lineWriter.writeLine( + chalk.gray(`Invalid regex: ${error.message}${os.EOL}`), + ); } - }; - #onCommand = async data => { - data = data.trim().toLowerCase(); + } - switch (data) { - case 'r': { - runSelected = true; - break; - } + return outFilter; + }; - case 'u': { - updateSnapshots = true; - break; - } + const onCommand = async data => { + data = data.trim().toLowerCase(); - case 'a': { - runAll = true; - break; - } + switch (data) { + case 'r': { + runSelected = true; + break; + } - case 'p': { - watchModeSkipTests.replaceFileRegex( - await this.#promptForFilter( - watchModeSkipTests.fileRegexOrNull, - 'filename', - ), - ); - reporter.lineWriter.write(`${os.EOL}${os.EOL}`); - reporter.lineWriter.writeLine(endMessage(watchModeSkipTests)); - reporter.lineWriter.write(chalk.white('command > ')); - runSelected = true; - break; - } + case 'u': { + updateSnapshots = true; + break; + } - case 't': { - watchModeSkipTests.replaceTestRegex( - await this.#promptForFilter( - watchModeSkipTests.testRegexOrNull, - 'test', - ), - ); - reporter.lineWriter.write(`${os.EOL}`); - reporter.lineWriter.writeLine(endMessage(watchModeSkipTests)); - reporter.lineWriter.write(chalk.white('command > ')); - runSelected = true; - break; - } + case 'a': { + runAll = true; + break; + } - default: { - return; - } + case 'p': { + const filterHasChanged = interactiveFilter.replaceFilepathRegex( + await promptForFilter( + interactiveFilter.fileRegexOrNull, + 'filepath', + ), + ); + reporter.lineWriter.write(`${os.EOL}${os.EOL}`); + reportEndMessage(reporter, interactiveFilter); + runSelected = filterHasChanged; + break; } - if (runAll || runSelected || updateSnapshots) { - signalChanged({}); + case 't': { + const filterHasChanged = interactiveFilter.replaceTestTitleRegex( + await promptForFilter( + interactiveFilter.testTitleRegexOrNull, + 'test title', + ), + ); + reporter.lineWriter.write(`${os.EOL}`); + reportEndMessage(reporter, interactiveFilter); + runSelected = filterHasChanged; + break; } - }; - #promptForFilter = async (currentFilterRegexOrNull, filterName) => { - reporter.lineWriter.writeLine( - chalk.gray( - `${os.EOL}${os.EOL}Current ${filterName} filter is ${ - currentFilterRegexOrNull === null - ? 'empty' - : currentFilterRegexOrNull - }${os.EOL}`, - ), - ); - reporter.lineWriter.writeLine( - chalk.gray( - `Type the ${filterName} regex pattern then press enter:${os.EOL}` - + ` - Leave it blank and press enter to clear the filter${os.EOL}`, - ), - ); - reporter.lineWriter.write('pattern > '); - const generatorResult = (await this.#data.next()); - const patternInput = generatorResult.value.trim(); - let outFilter = null; - if (patternInput !== '') { - try { - const pattern = new RegExp(patternInput); - outFilter = pattern; - reporter.lineWriter.writeLine( - chalk.gray(`Added ${filterName} regex filter: ${pattern}${os.EOL}`), - ); - } catch (error) { - reporter.lineWriter.writeLine( - chalk.gray(`Invalid regex: ${error.message}${os.EOL}`), - ); - } + + default: { + return; } + } + + if (runAll || runSelected || updateSnapshots) { + signalChanged({}); + } + }; + + (async () => { + for await (const d of data) { + await onCommand(d); + } + } + )(); - return outFilter; - }; - })(); stdin.unref(); // Whether tests are currently running. Used to control when the next run @@ -599,13 +550,14 @@ async function * plan({ previousFailures, runOnlyExclusive, updateSnapshots, // Value is changed by refresh() so record now. - watchModeSkipTestsOrUndefined: runAll - ? new WatchModeSkipTests() - : watchModeSkipTests, + interactiveFilter: runAll + ? new InteractiveFilter() + : interactiveFilter, }; reset(); // Make sure the next run can be triggered. testsAreRunning = true; yield instructions; // Let the tests run. + reportEndMessage(reporter, interactiveFilter); testsAreRunning = false; debounce.refresh(); // Trigger the callback, which if there were changes will run the tests again. } diff --git a/lib/worker/base.js b/lib/worker/base.js index 6da792c98..71090b1be 100644 --- a/lib/worker/base.js +++ b/lib/worker/base.js @@ -85,7 +85,7 @@ const run = async options => { serial: options.serial, snapshotDir: options.snapshotDir, updateSnapshots: options.updateSnapshots, - watchModeSkipTestsData: options.watchModeSkipTestsData, + interactiveFilterData: options.interactiveFilterData, }); refs.runnerChain = runner.chain; diff --git a/package-lock.json b/package-lock.json index 7fb86d068..14bbc91be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "plur": "^5.1.0", "pretty-ms": "^9.1.0", "resolve-cwd": "^3.0.0", + "split2": "^4.2.0", "stack-utils": "^2.0.6", "strip-ansi": "^7.1.0", "supertap": "^3.0.1", @@ -10943,6 +10944,15 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", diff --git a/package.json b/package.json index 426edad69..c236e7cf6 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "plur": "^5.1.0", "pretty-ms": "^9.1.0", "resolve-cwd": "^3.0.0", + "split2": "^4.2.0", "stack-utils": "^2.0.6", "strip-ansi": "^7.1.0", "supertap": "^3.0.1", diff --git a/test/watch-mode/basic-functionality.js b/test/watch-mode/basic-functionality.js index a22fc42e9..c6601b667 100644 --- a/test/watch-mode/basic-functionality.js +++ b/test/watch-mode/basic-functionality.js @@ -10,11 +10,11 @@ test('prints results and instructions', withFixture('basic'), async (t, fixture) t.regex(stdout, /\d+ tests? passed/); t.regex( stdout, - /Type `p` and press enter to filter by a filename regex pattern/, + /Type `p` and press enter to filter by a filepath regex pattern/, ); t.regex( stdout, - /Type `t` and press enter to filter by a test name regex pattern/, + /Type `t` and press enter to filter by a test title regex pattern/, ); t.regex(stdout, /Type `r` and press enter to rerun tests/); t.regex(stdout, /Type `u` and press enter to update snapshots/); @@ -88,7 +88,7 @@ test('can update snapshots', withFixture('basic'), async (t, fixture) => { }); test( - 'can filter tests by filename pattern', + 'can filter tests by filepath pattern', withFixture('filter-files'), async (t, fixture) => { const test1RegexString = 'test1'; @@ -102,16 +102,13 @@ test( // Set a file filter to only run test1.js process.stdin.write('p\n'); process.stdin.write(`${test1RegexString}\n`); + return stats; }, async 2({stats}) { // Only tests from test1 should run - t.is(stats.selectedTestCount, 8); - t.is(stats.skipped.length, 4); - for (const skipped of stats.skipped) { - t.notRegex(skipped.file, test1Regex); - } + t.is(stats.selectedTestCount, 4); t.is(stats.passed.length, 3); for (const skipped of stats.passed) { @@ -130,11 +127,10 @@ test( ); test( - 'can filter tests by filename pattern and have no tests run', + 'can filter tests by filepath pattern and have no tests run', withFixture('filter-files'), async (t, fixture) => { const test1RegexString = 'kangarookanbankentuckykendoll'; - const test1Regex = new RegExp(test1RegexString); await fixture.watch({ async 1({process, stats}) { // First run should run all tests @@ -144,29 +140,23 @@ test( // Set a file filter to only run test1.js process.stdin.write('p\n'); process.stdin.write(`${test1RegexString}\n`); - return stats; - }, - - async 2({stats}) { - // Only tests from test1 should run - t.is(stats.selectedTestCount, 8); - t.is(stats.skipped.length, 8); - for (const skipped of stats.skipped) { - t.notRegex(skipped.file, test1Regex); - } + process.send('abort-watcher'); + const {stdout} = await process; + t.regex(stdout, /2 test files were found, but did not match the CLI arguments/); this.done(); + + return stats; }, }); }, ); test( - 'can filter tests by filename pattern, and run all tests with \'a', + 'can filter tests by filepath pattern, and run all tests with \'a', withFixture('filter-files'), async (t, fixture) => { - const test1RegexString = 'kangarookanbankentuckykendoll'; - const test1Regex = new RegExp(test1RegexString); + const test1RegexString = 'test1'; await fixture.watch({ async 1({process, stats}) { // First run should run all tests @@ -180,17 +170,11 @@ test( }, async 2({process, stats}) { - // Only tests from test1 should run - t.is(stats.selectedTestCount, 8); - t.is(stats.skipped.length, 8); - for (const skipped of stats.skipped) { - t.notRegex(skipped.file, test1Regex); - } + t.is(stats.selectedTestCount, 4); process.stdin.write('a\n'); }, async 3({stats}) { - // All tests should run t.is(stats.selectedTestCount, 8); t.is(stats.passed.length, 6); @@ -201,7 +185,7 @@ test( ); test( - 'can filter tests by test pattern', + 'can filter tests by test title pattern', withFixture('filter-files'), async (t, fixture) => { const test1RegexString = 'bob'; @@ -219,13 +203,8 @@ test( }, async 2({stats}) { - // Only tests from test1 should run - t.is(stats.selectedTestCount, 8); - t.is(stats.skipped.length, 7); - for (const skipped of stats.skipped) { - t.notRegex(skipped.title, test1Regex); - } - + // Only tests that match the test title should run + t.is(stats.selectedTestCount, 1); t.is(stats.passed.length, 1); for (const skipped of stats.passed) { t.regex(skipped.title, test1Regex); @@ -242,7 +221,6 @@ test( withFixture('filter-files'), async (t, fixture) => { const test1RegexString = 'sirnotappearinginthisfilm'; - const test1Regex = new RegExp(test1RegexString); await fixture.watch({ async 1({process, stats}) { // First run should run all tests @@ -256,13 +234,8 @@ test( }, async 2({stats}) { - // Only tests from test1 should run - t.is(stats.selectedTestCount, 8); - t.is(stats.skipped.length, 8); - for (const skipped of stats.skipped) { - t.notRegex(skipped.title, test1Regex); - } - + // No tests should run + t.is(stats.selectedTestCount, 0); this.done(); }, }); @@ -288,9 +261,7 @@ test( }, async 2({process, stats}) { - // Only tests from test1 should run - t.is(stats.selectedTestCount, 8); - t.is(stats.skipped.length, 8); + t.is(stats.selectedTestCount, 0); for (const skipped of stats.skipped) { t.notRegex(skipped.file, test1Regex); } diff --git a/test/watch-mode/fixtures/filter-files/test1.test.js b/test/watch-mode/fixtures/filter-files/test1.test.js index 6e28c015f..a5c70aedc 100644 --- a/test/watch-mode/fixtures/filter-files/test1.test.js +++ b/test/watch-mode/fixtures/filter-files/test1.test.js @@ -5,14 +5,13 @@ test("alice", (t) => { }); test("bob", async (t) => { - const bar = Promise.resolve("bar"); - t.is(await bar, "bar"); + t.pass(); }); test("catherine", async (t) => { - t.is(1, 1); + t.pass(); }); test("david", async (t) => { - t.is(1, 2); + t.fail(); }); diff --git a/test/watch-mode/fixtures/filter-files/test2.test.js b/test/watch-mode/fixtures/filter-files/test2.test.js index 0b3304753..52927aeb5 100644 --- a/test/watch-mode/fixtures/filter-files/test2.test.js +++ b/test/watch-mode/fixtures/filter-files/test2.test.js @@ -5,14 +5,13 @@ test("emma", (t) => { }); test("frank", async (t) => { - const bar = Promise.resolve("bar"); - t.is(await bar, "bar"); + t.pass(); }); test("gina", async (t) => { - t.is(1, 1); + t.pass(); }); test("harry", async (t) => { - t.is(1, 2); + t.fail(); }); From 0b88520559c35b9e8852c77c090bd8a9b5dc8f6f Mon Sep 17 00:00:00 2001 From: Michael Mulet Date: Mon, 7 Apr 2025 16:12:04 -0500 Subject: [PATCH 9/9] lint fix --- lib/runner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/runner.js b/lib/runner.js index 0215e8d34..365a50e59 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -409,7 +409,7 @@ export default class Runner extends Emittery { return alwaysOk && hooksOk && testOk; } - async start() { // eslint-disable-line complexity + async start() { const concurrentTests = []; const serialTests = []; for (const task of this.tasks.serial) {