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 8 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
34 changes: 34 additions & 0 deletions docs/recipes/watch-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,40 @@ 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
3 changes: 3 additions & 0 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ export default class Api extends Emittery {
setupOrGlobError = error;
}

selectedFiles = selectedFiles.filter(file => runtimeOptions.interactiveFilter?.canSelectTestsInThisFile(file) ?? true);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seemed like the best place to skip loading entire file if it was filtered out by the interactive filter, but would somewhere else be better?


const selectionInsights = {
filter,
ignoredFilterPatternFiles: selectedFiles.ignoredFilterPatternFiles ?? [],
Expand Down Expand Up @@ -273,6 +275,7 @@ export default class Api extends Emittery {
providerStates,
lineNumbers,
recordNewSnapshots: !isCi,
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,
};
Expand Down
78 changes: 78 additions & 0 deletions lib/interactive-filter.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
12 changes: 10 additions & 2 deletions lib/reporters/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,17 @@ export default class Reporter {

case 'selected-test': {
if (event.skip) {
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(`- [todo] ${this.prefixTitle(event.testFile, event.title)}`));
this.lineWriter.writeLine(
colors.todo(
`- [todo] ${this.prefixTitle(event.testFile, event.title)}`,
),
);
}

break;
Expand Down
31 changes: 19 additions & 12 deletions lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

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';
Expand All @@ -29,6 +30,8 @@
this.serial = options.serial === true;
this.snapshotDir = options.snapshotDir;
this.updateSnapshots = options.updateSnapshots;
this.interactiveFilter
= new InteractiveFilter(options.interactiveFilterData);

this.activeRunnables = new Set();
this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this);
Expand Down Expand Up @@ -91,9 +94,12 @@
metadata.taskIndex = this.nextTaskIndex++;

const {args, implementation, title} = parseTestArgs(testArgs);

if (this.checkSelectedByLineNumbers) {
metadata.selected = this.checkSelectedByLineNumbers();
if (
this.interactiveFilter.shouldSelectTest(title.value)
) {
metadata.selected = this.checkSelectedByLineNumbers ? this.checkSelectedByLineNumbers() : true;
} else {
metadata.selected = false;
}

if (metadata.todo) {
Expand Down Expand Up @@ -181,6 +187,7 @@
serial: false,
exclusive: false,
skipped: false,
selected: false,
todo: false,
failing: false,
callback: false,
Expand Down Expand Up @@ -254,8 +261,8 @@
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 +271,10 @@
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 @@ -402,7 +409,7 @@
return alwaysOk && hooksOk && testOk;
}

async start() { // eslint-disable-line complexity

Check failure on line 412 in lib/runner.js

View workflow job for this annotation

GitHub Actions / Lint source files

'complexity' rule is disabled but never reported.
const concurrentTests = [];
const serialTests = [];
for (const task of this.tasks.serial) {
Expand All @@ -411,7 +418,7 @@
continue;
}

if (this.checkSelectedByLineNumbers && !task.metadata.selected) {
if (!task.metadata.selected) {
Copy link
Author

@mmulet mmulet Apr 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I moved to metadata.selected rather than metadata.skipped, I had to delete the checks for this.checkSelectedByLineNumbers whenever it also checks metadata.selected.

All tests still pass, and it works fine. So, I am assuming that these checks are not needed.

this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
continue;
}
Expand All @@ -437,7 +444,7 @@
continue;
}

if (this.checkSelectedByLineNumbers && !task.metadata.selected) {
if (!task.metadata.selected) {
this.snapshots.skipBlock(task.title, task.metadata.taskIndex);
continue;
}
Expand All @@ -464,7 +471,7 @@
continue;
}

if (this.checkSelectedByLineNumbers && !task.metadata.selected) {
if (!task.metadata.selected) {
continue;
}

Expand Down
Loading
Loading