Skip to content

Implemented Watch mode filter by filename and by test name #1530 #3372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
78a3768
Implemented Watch mode filter by filename and by test name #1530
Mar 26, 2025
8ecd51d
fix tests on onlder node versions
Mar 27, 2025
0d78fd3
Format documentation
novemberborn Apr 4, 2025
b72a9dd
Add newline at EOF
novemberborn Apr 4, 2025
a3cce75
Fix error handling in watcher tests
novemberborn Apr 4, 2025
b0762b0
Remove unnecessary this.done() calls in catch blocks
novemberborn Apr 4, 2025
5195cbe
Remove unnecessary multiline comments
novemberborn Apr 4, 2025
16630d4
implemented fixes
Apr 7, 2025
0b88520
lint fix
Apr 7, 2025
e958e4b
Merge branch 'main' into Watch_mode_filter
novemberborn Apr 16, 2025
63ff4fc
Revert whitespace changes in reporter
novemberborn Apr 17, 2025
0948247
Revert test worker changes
novemberborn Apr 25, 2025
747ab06
Move interactive filters tests to their own file
novemberborn Apr 30, 2025
8c14e1a
Improve empty line tracking in reporter
novemberborn Apr 29, 2025
1ade579
Add option in reporter line writer to not indent the line
novemberborn Apr 29, 2025
68dd12f
Remove dead chokidar link reference definition
novemberborn Apr 29, 2025
9c1e8c1
Remove special .only() behavior in watch mode
novemberborn Apr 29, 2025
65acae4
Implement --match using the selected flag
novemberborn Apr 29, 2025
6f2adff
Place available() function before reportEndMessage
novemberborn Apr 29, 2025
74d4077
Use helpers to read lines and process commands
novemberborn Apr 29, 2025
8561644
Refactor command instructions and filter prompts for improved clarity…
novemberborn Apr 29, 2025
7b98b2d
Simplify change signaling, don't require an argument
novemberborn Apr 29, 2025
fe34e34
Don't respond to changes when prompting for filters
novemberborn Apr 29, 2025
9575970
Implement interactive file filters using globs
novemberborn Apr 29, 2025
a05369a
Implement interactive test file filter akin to --match
novemberborn Apr 29, 2025
795e9a5
Change run-all to not reset filters
novemberborn May 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/recipes/watch-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
};
Expand Down
15 changes: 13 additions & 2 deletions lib/reporters/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
32 changes: 25 additions & 7 deletions lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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),
),
]);
Expand Down Expand Up @@ -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);
}
}
56 changes: 56 additions & 0 deletions lib/watch-mode-skip-tests.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading