Skip to content

feat: introduce watchTriggerPatterns option #7778

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

Merged
Merged
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
30 changes: 30 additions & 0 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,36 @@ In interactive environments, this is the default, unless `--run` is specified ex

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

### watchTriggerPatterns <Version>3.2.0</Version><NonProjectOption /> {#watchtriggerpatterns}

- **Type:** `WatcherTriggerPattern[]`

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.

To correctly rerun those tests, you can define a regex pattern and a function that retuns a list of test files to run.

```ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
watchTriggerPatterns: [
{
pattern: /^src\/(mailers|templates)\/(.*)\.(ts|html|txt)$/,
testToRun: (match) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be (id, match) ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Should be fixed on main now

// relative to the root value
return `./api/tests/mailers/${match[2]}.test.ts`
},
},
],
},
})
```

::: warning
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.
:::

### root

- **Type:** `string`
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/node/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,7 @@ export const cliOptionsConfig: VitestCLIOptions = {
json: null,
provide: null,
filesOnly: null,
watchTriggerPatterns: null,
}

export const benchCliOptionsConfig: Pick<
Expand Down
8 changes: 8 additions & 0 deletions packages/vitest/src/node/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
BuiltinReporters,
} from '../reporters'
import type { TestSequencerConstructor } from '../sequencers/types'
import type { WatcherTriggerPattern } from '../watcher'
import type { BenchmarkUserOptions } from './benchmark'
import type { BrowserConfigOptions, ResolvedBrowserOptions } from './browser'
import type { CoverageOptions, ResolvedCoverageOptions } from './coverage'
Expand Down Expand Up @@ -491,6 +492,12 @@ export interface InlineConfig {
*/
forceRerunTriggers?: string[]

/**
* Pattern configuration to rerun only the tests that are affected
* by the changes of specific files in the repository.
*/
watchTriggerPatterns?: WatcherTriggerPattern[]

/**
* Coverage options
*/
Expand Down Expand Up @@ -1094,6 +1101,7 @@ type NonProjectOptions =
| 'minWorkers'
| 'fileParallelism'
| 'workspace'
| 'watchTriggerPatterns'

export type ProjectConfig = Omit<
InlineConfig,
Expand Down
48 changes: 46 additions & 2 deletions packages/vitest/src/node/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { TestProject } from './project'
import { readFileSync } from 'node:fs'
import { noop, slash } from '@vitest/utils'
import mm from 'micromatch'
import { resolve } from 'pathe'

export class VitestWatcher {
/**
Expand Down Expand Up @@ -54,14 +55,42 @@ export class VitestWatcher {
this._onRerun.forEach(cb => cb(file))
}

private getTestFilesFromWatcherTrigger(id: string): boolean {
if (!this.vitest.config.watchTriggerPatterns) {
return false
}
let triggered = false
this.vitest.config.watchTriggerPatterns.forEach((definition) => {
const exec = definition.pattern.exec(id)
if (exec) {
const files = definition.testsToRun(id, exec)
if (Array.isArray(files)) {
triggered = true
files.forEach(file => this.changedTests.add(resolve(this.vitest.config.root, file)))
}
else if (typeof files === 'string') {
triggered = true
this.changedTests.add(resolve(this.vitest.config.root, files))
}
}
})
return triggered
}

private onChange = (id: string): void => {
id = slash(id)
this.vitest.logger.clearHighlightCache(id)
this.vitest.invalidateFile(id)
const needsRerun = this.handleFileChanged(id)
if (needsRerun) {
const testFiles = this.getTestFilesFromWatcherTrigger(id)
if (testFiles) {
this.scheduleRerun(id)
}
else {
const needsRerun = this.handleFileChanged(id)
if (needsRerun) {
this.scheduleRerun(id)
}
}
}

private onUnlink = (id: string): void => {
Expand All @@ -82,6 +111,13 @@ export class VitestWatcher {
private onAdd = (id: string): void => {
id = slash(id)
this.vitest.invalidateFile(id)

const testFiles = this.getTestFilesFromWatcherTrigger(id)
if (testFiles) {
this.scheduleRerun(id)
return
}

let fileContent: string | undefined

const matchingProjects: TestProject[] = []
Expand Down Expand Up @@ -171,3 +207,11 @@ export class VitestWatcher {
return !!files.length
}
}

export interface WatcherTriggerPattern {
pattern: RegExp
testsToRun: (
file: string,
match: RegExpMatchArray
) => string[] | string | null | undefined | void
}
1 change: 1 addition & 0 deletions packages/vitest/src/public/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export {
defaultExclude,
defaultInclude,
} from '../defaults'
export type { WatcherTriggerPattern } from '../node/watcher'
export { mergeConfig } from 'vite'
export type { Plugin } from 'vite'

Expand Down
6 changes: 3 additions & 3 deletions packages/vitest/src/public/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,15 @@ export type { TestRunResult } from '../node/types/tests'
export const TestFile: typeof _TestFile = _TestFile
export type { WorkerContext } from '../node/types/worker'
export { createViteLogger } from '../node/viteLogger'
export { distDir, rootDir } from '../paths'
export type { WatcherTriggerPattern } from '../node/watcher'

/**
* @deprecated Use `ModuleDiagnostic` instead
*/
export type FileDiagnostic = _FileDiagnostic

export { distDir, rootDir } from '../paths'

export type {
CollectLineNumbers as TypeCheckCollectLineNumbers,
CollectLines as TypeCheckCollectLines,
Expand All @@ -147,9 +149,7 @@ export type {
} from '../typecheck/types'

export type { TestExecutionMethod as TestExecutionType } from '../types/worker'

export { createDebugger } from '../utils/debugger'

export type {
RunnerTask,
RunnerTaskResult,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { expect, test } from 'vitest';

const filepath = resolve(import.meta.dirname, './text.txt');

test('basic', () => {
expect(readFileSync(filepath, 'utf-8')).toBe('hello world\n');
})
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello world
14 changes: 14 additions & 0 deletions test/config/fixtures/watch-trigger-pattern/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
watchTriggerPatterns: [
{
pattern: /folder\/(\w+)\/.*\.txt$/,
testsToRun: (id, match) => {
return `./folder/${match[1]}/basic.test.ts`;
},
}
]
}
})
25 changes: 25 additions & 0 deletions test/config/test/watch-trigger-pattern.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { resolve } from 'node:path'
import { expect, test } from 'vitest'
import { editFile, runVitest } from '../../test-utils'

const root = resolve(import.meta.dirname, '../fixtures/watch-trigger-pattern')

test('watch trigger pattern picks up the file', async () => {
const { stderr, vitest } = await runVitest({
root,
watch: true,
})

expect(stderr).toBe('')

await vitest.waitForStdout('Waiting for file changes')

editFile(
resolve(root, 'folder/fs/text.txt'),
content => content.replace('world', 'vitest'),
)

await vitest.waitForStderr('basic.test.ts')

expect(vitest.stderr).toContain(`expected 'hello vitest\\n' to be 'hello world\\n'`)
})
Loading