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 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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);
}

Check warning on line 65 in lib/interactive-filter.js

View check run for this annotation

Codecov / codecov/patch

lib/interactive-filter.js#L60-L65

Added lines #L60 - L65 were not covered by tests

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
33 changes: 20 additions & 13 deletions lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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';
Expand All @@ -29,6 +30,8 @@ export default class Runner extends Emittery {
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 @@ export default class Runner extends Emittery {
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 @@ export default class Runner extends Emittery {
serial: false,
exclusive: false,
skipped: false,
selected: false,
todo: false,
failing: false,
callback: false,
Expand Down Expand Up @@ -254,8 +261,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 +271,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 @@ -402,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) {
Expand All @@ -411,7 +418,7 @@ export default class Runner extends Emittery {
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 @@ 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;
}
Expand All @@ -464,7 +471,7 @@ export default class Runner extends Emittery {
continue;
}

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

Expand Down
Loading