Skip to content

Commit a067531

Browse files
authored
feat: introduce watchTriggerPatterns option (#7778)
1 parent 029c078 commit a067531

File tree

10 files changed

+138
-5
lines changed

10 files changed

+138
-5
lines changed

docs/config/index.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,36 @@ In interactive environments, this is the default, unless `--run` is specified ex
697697

698698
In CI, or when run from a non-interactive shell, "watch" mode is not the default, but can be enabled explicitly with this flag.
699699

700+
### watchTriggerPatterns <Version>3.2.0</Version><NonProjectOption /> {#watchtriggerpatterns}
701+
702+
- **Type:** `WatcherTriggerPattern[]`
703+
704+
Vitest reruns tests based on the module graph which is populated by static and dynamic `import` statements. However, if you are reading from the file system or fetching from a proxy, then Vitest cannot detect those dependencies.
705+
706+
To correctly rerun those tests, you can define a regex pattern and a function that retuns a list of test files to run.
707+
708+
```ts
709+
import { defineConfig } from 'vitest/config'
710+
711+
export default defineConfig({
712+
test: {
713+
watchTriggerPatterns: [
714+
{
715+
pattern: /^src\/(mailers|templates)\/(.*)\.(ts|html|txt)$/,
716+
testToRun: (match) => {
717+
// relative to the root value
718+
return `./api/tests/mailers/${match[2]}.test.ts`
719+
},
720+
},
721+
],
722+
},
723+
})
724+
```
725+
726+
::: warning
727+
Returned files should be either absolute or relative to the root. Note that this is a global option, and it cannot be used inside of [project](/guide/workspace) configs.
728+
:::
729+
700730
### root
701731

702732
- **Type:** `string`

packages/vitest/src/node/cli/cli-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,7 @@ export const cliOptionsConfig: VitestCLIOptions = {
863863
json: null,
864864
provide: null,
865865
filesOnly: null,
866+
watchTriggerPatterns: null,
866867
}
867868

868869
export const benchCliOptionsConfig: Pick<

packages/vitest/src/node/types/config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
BuiltinReporters,
1717
} from '../reporters'
1818
import type { TestSequencerConstructor } from '../sequencers/types'
19+
import type { WatcherTriggerPattern } from '../watcher'
1920
import type { BenchmarkUserOptions } from './benchmark'
2021
import type { BrowserConfigOptions, ResolvedBrowserOptions } from './browser'
2122
import type { CoverageOptions, ResolvedCoverageOptions } from './coverage'
@@ -491,6 +492,12 @@ export interface InlineConfig {
491492
*/
492493
forceRerunTriggers?: string[]
493494

495+
/**
496+
* Pattern configuration to rerun only the tests that are affected
497+
* by the changes of specific files in the repository.
498+
*/
499+
watchTriggerPatterns?: WatcherTriggerPattern[]
500+
494501
/**
495502
* Coverage options
496503
*/
@@ -1094,6 +1101,7 @@ type NonProjectOptions =
10941101
| 'minWorkers'
10951102
| 'fileParallelism'
10961103
| 'workspace'
1104+
| 'watchTriggerPatterns'
10971105

10981106
export type ProjectConfig = Omit<
10991107
InlineConfig,

packages/vitest/src/node/watcher.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { TestProject } from './project'
33
import { readFileSync } from 'node:fs'
44
import { noop, slash } from '@vitest/utils'
55
import mm from 'micromatch'
6+
import { resolve } from 'pathe'
67

78
export class VitestWatcher {
89
/**
@@ -54,14 +55,42 @@ export class VitestWatcher {
5455
this._onRerun.forEach(cb => cb(file))
5556
}
5657

58+
private getTestFilesFromWatcherTrigger(id: string): boolean {
59+
if (!this.vitest.config.watchTriggerPatterns) {
60+
return false
61+
}
62+
let triggered = false
63+
this.vitest.config.watchTriggerPatterns.forEach((definition) => {
64+
const exec = definition.pattern.exec(id)
65+
if (exec) {
66+
const files = definition.testsToRun(id, exec)
67+
if (Array.isArray(files)) {
68+
triggered = true
69+
files.forEach(file => this.changedTests.add(resolve(this.vitest.config.root, file)))
70+
}
71+
else if (typeof files === 'string') {
72+
triggered = true
73+
this.changedTests.add(resolve(this.vitest.config.root, files))
74+
}
75+
}
76+
})
77+
return triggered
78+
}
79+
5780
private onChange = (id: string): void => {
5881
id = slash(id)
5982
this.vitest.logger.clearHighlightCache(id)
6083
this.vitest.invalidateFile(id)
61-
const needsRerun = this.handleFileChanged(id)
62-
if (needsRerun) {
84+
const testFiles = this.getTestFilesFromWatcherTrigger(id)
85+
if (testFiles) {
6386
this.scheduleRerun(id)
6487
}
88+
else {
89+
const needsRerun = this.handleFileChanged(id)
90+
if (needsRerun) {
91+
this.scheduleRerun(id)
92+
}
93+
}
6594
}
6695

6796
private onUnlink = (id: string): void => {
@@ -82,6 +111,13 @@ export class VitestWatcher {
82111
private onAdd = (id: string): void => {
83112
id = slash(id)
84113
this.vitest.invalidateFile(id)
114+
115+
const testFiles = this.getTestFilesFromWatcherTrigger(id)
116+
if (testFiles) {
117+
this.scheduleRerun(id)
118+
return
119+
}
120+
85121
let fileContent: string | undefined
86122

87123
const matchingProjects: TestProject[] = []
@@ -171,3 +207,11 @@ export class VitestWatcher {
171207
return !!files.length
172208
}
173209
}
210+
211+
export interface WatcherTriggerPattern {
212+
pattern: RegExp
213+
testsToRun: (
214+
file: string,
215+
match: RegExpMatchArray
216+
) => string[] | string | null | undefined | void
217+
}

packages/vitest/src/public/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export {
1919
defaultExclude,
2020
defaultInclude,
2121
} from '../defaults'
22+
export type { WatcherTriggerPattern } from '../node/watcher'
2223
export { mergeConfig } from 'vite'
2324
export type { Plugin } from 'vite'
2425

packages/vitest/src/public/node.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,15 @@ export type { TestRunResult } from '../node/types/tests'
130130
export const TestFile: typeof _TestFile = _TestFile
131131
export type { WorkerContext } from '../node/types/worker'
132132
export { createViteLogger } from '../node/viteLogger'
133-
export { distDir, rootDir } from '../paths'
133+
export type { WatcherTriggerPattern } from '../node/watcher'
134134

135135
/**
136136
* @deprecated Use `ModuleDiagnostic` instead
137137
*/
138138
export type FileDiagnostic = _FileDiagnostic
139139

140+
export { distDir, rootDir } from '../paths'
141+
140142
export type {
141143
CollectLineNumbers as TypeCheckCollectLineNumbers,
142144
CollectLines as TypeCheckCollectLines,
@@ -147,9 +149,7 @@ export type {
147149
} from '../typecheck/types'
148150

149151
export type { TestExecutionMethod as TestExecutionType } from '../types/worker'
150-
151152
export { createDebugger } from '../utils/debugger'
152-
153153
export type {
154154
RunnerTask,
155155
RunnerTaskResult,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { readFileSync } from 'node:fs';
2+
import { resolve } from 'node:path';
3+
import { expect, test } from 'vitest';
4+
5+
const filepath = resolve(import.meta.dirname, './text.txt');
6+
7+
test('basic', () => {
8+
expect(readFileSync(filepath, 'utf-8')).toBe('hello world\n');
9+
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello world
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({
4+
test: {
5+
watchTriggerPatterns: [
6+
{
7+
pattern: /folder\/(\w+)\/.*\.txt$/,
8+
testsToRun: (id, match) => {
9+
return `./folder/${match[1]}/basic.test.ts`;
10+
},
11+
}
12+
]
13+
}
14+
})
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { resolve } from 'node:path'
2+
import { expect, test } from 'vitest'
3+
import { editFile, runVitest } from '../../test-utils'
4+
5+
const root = resolve(import.meta.dirname, '../fixtures/watch-trigger-pattern')
6+
7+
test('watch trigger pattern picks up the file', async () => {
8+
const { stderr, vitest } = await runVitest({
9+
root,
10+
watch: true,
11+
})
12+
13+
expect(stderr).toBe('')
14+
15+
await vitest.waitForStdout('Waiting for file changes')
16+
17+
editFile(
18+
resolve(root, 'folder/fs/text.txt'),
19+
content => content.replace('world', 'vitest'),
20+
)
21+
22+
await vitest.waitForStderr('basic.test.ts')
23+
24+
expect(vitest.stderr).toContain(`expected 'hello vitest\\n' to be 'hello world\\n'`)
25+
})

0 commit comments

Comments
 (0)