[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

test: run watcher tests on private inline roots instead of a shared fixture

Vladimir Sheremet (Jul 27, 2026, 4:11 PM +0200) dbbcc05e d817d243

+201 -180
+5 -9
test/e2e/vitest.config.ts
··· 2 2 import { defineConfig } from 'vite' 3 3 import { defaultExclude } from 'vitest/config' 4 4 5 - // Tests that mutate shared fixtures (editing files in `fixtures/watch`, driving 6 - // git `--changed`) and cannot tolerate other tests mutating the working tree 7 - // concurrently: an instance watching `fixtures/watch` restarts when a parallel 8 - // test rewrites the fixture config and goes deaf to further file edits. Run in 9 - // a serial project. 5 + // Tests that drive git `--changed` against committed fixtures and cannot 6 + // tolerate other tests mutating the working tree concurrently. Run in a serial 7 + // project. Tests that only need a watched directory use `runInlineTests` 8 + // instead: a private tmp root needs no serialization, but it cannot back the 9 + // git-driven tests, which require tracked files. 10 10 const serialTests = [ 11 11 'test/git-changed.test.ts', 12 12 'test/list-changed.test.ts', 13 13 'test/setup-files.test.ts', 14 - 'test/watch/file-watching.test.ts', 15 - 'test/watch/global-setup-rerun.test.ts', 16 14 'test/watch/related.test.ts', 17 - 'test/watch/restart-coalescing.test.ts', 18 - 'test/watch/stdout.test.ts', 19 15 ] 20 16 21 17 export default defineConfig({
+7
test/e2e/fixtures/related/math.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + 3 + import { sum } from './math' 4 + 5 + test('sum', () => { 6 + expect(sum(1, 2)).toBe(3) 7 + })
+3
test/e2e/fixtures/related/math.ts
··· 1 + export function sum(a: number, b: number) { 2 + return a + b 3 + }
+1
test/e2e/fixtures/related/vitest.config.ts
··· 1 + export default {}
-4
test/e2e/fixtures/watch/42.txt
··· 1 - 42 2 - 3 - 4 -
-14
test/e2e/fixtures/watch/example.test.ts
··· 1 - import { expect, test } from 'vitest' 2 - 3 - import { getHelloWorld } from './example' 4 - 5 - // @ts-expect-error not typed txt 6 - import answer from './42.txt?raw' 7 - 8 - test('answer is 42', () => { 9 - expect(answer).toContain('42') 10 - }) 11 - 12 - test('getHello', async () => { 13 - expect(getHelloWorld()).toBe('Hello world') 14 - })
-3
test/e2e/fixtures/watch/example.ts
··· 1 - export function getHelloWorld() { 2 - return 'Hello world' 3 - }
-15
test/e2e/fixtures/watch/global-setup.ts
··· 1 - import { TestProject } from 'vitest/node'; 2 - 3 - const calls: string[] = []; 4 - 5 - (globalThis as any).__CALLS = calls 6 - 7 - export default (project: TestProject) => { 8 - calls.push('start') 9 - project.onTestsRerun(() => { 10 - calls.push('rerun') 11 - }) 12 - return () => { 13 - calls.push('end') 14 - } 15 - }
-7
test/e2e/fixtures/watch/math.test.ts
··· 1 - import { expect, test } from 'vitest' 2 - 3 - import { sum } from './math' 4 - 5 - test('sum', () => { 6 - expect(sum(1, 2)).toBe(3) 7 - })
-3
test/e2e/fixtures/watch/math.ts
··· 1 - export function sum(a: number, b: number) { 2 - return a + b 3 - }
-22
test/e2e/fixtures/watch/vitest.config.ts
··· 1 - import { defaultExclude, defineConfig } from 'vitest/config' 2 - 3 - // Patch stdin on the process so that we can fake it to seem like a real interactive terminal and pass the TTY checks 4 - process.stdin.isTTY = true 5 - process.stdin.setRawMode = () => process.stdin 6 - 7 - export default defineConfig({ 8 - test: { 9 - watch: true, 10 - exclude: [ 11 - ...defaultExclude, 12 - '**/single-failed/**', 13 - ], 14 - 15 - // This configuration is edited by tests 16 - reporters: 'verbose', 17 - 18 - forceRerunTriggers: [ 19 - '**/force-watch/**', 20 - ], 21 - }, 22 - })
+110 -72
test/e2e/test/watch/file-watching.test.ts
··· 1 - import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs' 1 + import { existsSync, rmSync } from 'node:fs' 2 2 import * as testUtils from '#test-utils' 3 3 4 4 import { playwright } from '@vitest/browser-playwright' 5 - import { afterEach, describe, expect, onTestFinished, test } from 'vitest' 5 + import { resolve } from 'pathe' 6 + import { describe, expect, test } from 'vitest' 6 7 7 - const sourceFile = 'fixtures/watch/math.ts' 8 - const sourceFileContent = readFileSync(sourceFile, 'utf-8') 9 - const sourceFileStat = statSync(sourceFile) 8 + const mathTs = /* ts */ ` 9 + export function sum(a: number, b: number) { 10 + return a + b 11 + } 12 + ` 10 13 11 - const testFile = 'fixtures/watch/math.test.ts' 12 - const testFileContent = readFileSync(testFile, 'utf-8') 13 - const testFileStat = statSync(testFile) 14 + const mathTestTs = /* ts */ ` 15 + import { expect, test } from 'vitest' 14 16 15 - const configFile = 'fixtures/watch/vitest.config.ts' 16 - const configFileContent = readFileSync(configFile, 'utf-8') 17 - const configFileStat = statSync(configFile) 17 + import { sum } from './math' 18 18 19 - const forceTriggerFile = 'fixtures/watch/force-watch/trigger.js' 20 - const forceTriggerFileContent = readFileSync(forceTriggerFile, 'utf-8') 21 - const forceTriggerFileStat = statSync(forceTriggerFile) 19 + test('sum', () => { 20 + expect(sum(1, 2)).toBe(3) 21 + }) 22 + ` 22 23 23 - const options = { root: 'fixtures/watch', watch: true } 24 + const exampleTs = /* ts */ ` 25 + export function getHelloWorld() { 26 + return 'Hello world' 27 + } 28 + ` 29 + 30 + const exampleTestTs = /* ts */ ` 31 + import { expect, test } from 'vitest' 32 + 33 + import { getHelloWorld } from './example' 34 + 35 + test('getHello', async () => { 36 + expect(getHelloWorld()).toBe('Hello world') 37 + }) 38 + ` 39 + 40 + // two test files, so the initial run reports "2 passed" and a "1 passed" wait 41 + // can only be satisfied by a rerun of a single affected file 42 + const baseFixture = { 43 + 'math.ts': mathTs, 44 + 'math.test.ts': mathTestTs, 45 + 'example.ts': exampleTs, 46 + 'example.test.ts': exampleTestTs, 47 + } 24 48 25 49 function modifyContent(fileContent: string) { 26 50 return `// Modified by file-watching.test.ts ··· 29 53 ` 30 54 } 31 55 32 - // Only restore files the test actually modified, and restore their mtime with 33 - // the content: a restore that changes the stat of a fixture file is 34 - // indistinguishable from a real edit for the next test's watcher, and a stale 35 - // `vitest.config.ts` change event even restarts that instance mid-test, 36 - // leaving it deaf to further file edits. 37 - afterEach(() => { 38 - const files = [ 39 - [sourceFile, sourceFileContent, sourceFileStat], 40 - [testFile, testFileContent, testFileStat], 41 - [configFile, configFileContent, configFileStat], 42 - [forceTriggerFile, forceTriggerFileContent, forceTriggerFileStat], 43 - ] as const 44 - for (const [file, content, stat] of files) { 45 - if (readFileSync(file, 'utf-8') !== content) { 46 - testUtils.restoreFile(file, content, stat) 47 - } 48 - } 49 - }) 50 - 51 56 test('editing source file triggers re-run', async () => { 52 - const { vitest } = await testUtils.runVitest(options) 57 + const { vitest, fs } = await testUtils.runInlineTests(baseFixture, { watch: true }) 53 58 54 - writeFileSync(sourceFile, modifyContent(sourceFileContent), 'utf8') 59 + fs.editFile('math.ts', modifyContent) 55 60 56 61 await vitest.waitForStdout('New code running') 57 - await vitest.waitForStdout('RERUN ../../math.ts') 62 + await vitest.waitForStdout('RERUN ../math.ts') 58 63 await vitest.waitForStdout('1 passed') 59 64 }) 60 65 61 66 test('editing file that was imported with a query reruns suite', async () => { 62 - const { vitest } = await testUtils.runVitest(options) 67 + const { vitest, fs } = await testUtils.runInlineTests({ 68 + ...baseFixture, 69 + '42.txt': '42\n', 70 + 'answer.test.ts': /* ts */ ` 71 + import { expect, test } from 'vitest' 63 72 64 - testUtils.editFile( 65 - testUtils.resolvePath(import.meta.url, '../../fixtures/watch/42.txt'), 66 - file => `${file}\n`, 67 - ) 73 + // @ts-expect-error not typed txt 74 + import answer from './42.txt?raw' 68 75 69 - await vitest.waitForStdout('RERUN ../../42.txt') 76 + test('answer is 42', () => { 77 + expect(answer).toContain('42') 78 + }) 79 + `, 80 + }, { watch: true }) 81 + 82 + fs.editFile('42.txt', file => `${file}\n`) 83 + 84 + await vitest.waitForStdout('RERUN ../42.txt') 70 85 await vitest.waitForStdout('1 passed') 71 86 }) 72 87 73 88 test('editing force rerun trigger reruns all tests', async () => { 74 - const { vitest } = await testUtils.runVitest(options) 75 - 76 - writeFileSync(forceTriggerFile, modifyContent(forceTriggerFileContent), 'utf8') 89 + const { vitest, fs } = await testUtils.runInlineTests({ 90 + ...baseFixture, 91 + 'force-watch/trigger.js': 'export const trigger = false\n', 92 + 'vitest.config.ts': /* ts */ ` 93 + export default { 94 + test: { 95 + forceRerunTriggers: ['**/force-watch/**'], 96 + }, 97 + } 98 + `, 99 + }, { watch: true }) 77 100 78 101 await vitest.waitForStdout('Waiting for file changes...') 79 - await vitest.waitForStdout('RERUN ../../force-watch/trigger.js') 102 + vitest.resetOutput() 103 + 104 + fs.editFile('force-watch/trigger.js', modifyContent) 105 + 106 + await vitest.waitForStdout('RERUN ../force-watch/trigger.js') 80 107 await vitest.waitForStdout('example.test.ts') 81 108 await vitest.waitForStdout('math.test.ts') 82 109 await vitest.waitForStdout('2 passed') 83 110 }) 84 111 85 112 test('editing test file triggers re-run', async () => { 86 - const { vitest } = await testUtils.runVitest(options) 113 + const { vitest, fs } = await testUtils.runInlineTests(baseFixture, { watch: true }) 87 114 88 - writeFileSync(testFile, modifyContent(testFileContent), 'utf8') 115 + fs.editFile('math.test.ts', modifyContent) 89 116 90 117 await vitest.waitForStdout('New code running') 91 - await vitest.waitForStdout('RERUN ../../math.test.ts') 118 + await vitest.waitForStdout('RERUN ../math.test.ts') 92 119 await vitest.waitForStdout('1 passed') 93 120 }) 94 121 95 122 test('editing config file triggers re-run', async () => { 96 - const { vitest } = await testUtils.runVitest(options) 123 + const { vitest, fs } = await testUtils.runInlineTests({ 124 + ...baseFixture, 125 + 'vitest.config.ts': /* ts */ ` 126 + export default { 127 + test: { 128 + reporters: 'verbose', 129 + }, 130 + } 131 + `, 132 + }, { watch: true, reporters: 'none' }) 97 133 98 - writeFileSync(configFile, modifyContent(configFileContent), 'utf8') 134 + await vitest.waitForStdout('Waiting for file changes...') 135 + vitest.resetOutput() 136 + 137 + fs.editFile('vitest.config.ts', modifyContent) 99 138 100 139 await vitest.waitForStdout('Restarting due to config changes') 101 140 await vitest.waitForStdout('2 passed') 102 141 }) 103 142 104 143 test('editing config file reloads new changes', async () => { 105 - const { vitest } = await testUtils.runVitest({ ...options, reporters: 'none' }) 144 + const { vitest, fs } = await testUtils.runInlineTests({ 145 + ...baseFixture, 146 + 'vitest.config.ts': /* ts */ ` 147 + export default { 148 + test: { 149 + reporters: 'verbose', 150 + }, 151 + } 152 + `, 153 + }, { watch: true, reporters: 'none' }) 106 154 107 - writeFileSync(configFile, configFileContent.replace('reporters: \'verbose\'', 'reporters: \'tap\''), 'utf8') 155 + fs.editFile('vitest.config.ts', content => content.replace('reporters: \'verbose\'', 'reporters: \'tap\'')) 108 156 109 157 await vitest.waitForStdout('TAP version') 110 158 await vitest.waitForStdout('ok 2') ··· 177 225 }) 178 226 179 227 test('editing source file generates new test report to file system', async () => { 180 - const report = 'fixtures/watch/test-results/junit.xml' 181 - onTestFinished(() => { 182 - if (existsSync(report)) { 183 - rmSync(report) 184 - } 185 - }) 186 - 187 - // Test report should not be present before test run 188 - expect(existsSync(report)).toBe(false) 189 - 190 - const { vitest } = await testUtils.runVitest({ 191 - ...options, 228 + const { vitest, fs, root } = await testUtils.runInlineTests(baseFixture, { 229 + watch: true, 192 230 reporters: ['verbose', 'junit'], 193 231 outputFile: './test-results/junit.xml', 194 232 }) 233 + 234 + const report = resolve(root, 'test-results/junit.xml') 195 235 196 236 // Test report should be generated on initial test run 197 237 expect(existsSync(report)).toBe(true) ··· 201 241 expect(existsSync(report)).toBe(false) 202 242 203 243 vitest.resetOutput() 204 - writeFileSync(sourceFile, modifyContent(sourceFileContent), 'utf8') 244 + fs.editFile('math.ts', modifyContent) 205 245 206 246 await vitest.waitForStdout('JUNIT report written') 207 - await vitest.waitForStdout(report) 208 247 expect(existsSync(report)).toBe(true) 209 248 }) 210 249 211 250 describe('browser', () => { 212 251 test.runIf((process.platform !== 'win32'))('editing source file triggers re-run', { retry: 3 }, async () => { 213 - const { vitest } = await testUtils.runVitest({ 214 - root: 'fixtures/watch', 252 + const { vitest, fs } = await testUtils.runInlineTests(baseFixture, { 215 253 watch: true, 216 254 browser: { 217 255 instances: [{ browser: 'chromium' }], ··· 221 259 }, 222 260 }) 223 261 224 - writeFileSync(sourceFile, modifyContent(sourceFileContent), 'utf8') 262 + fs.editFile('math.ts', modifyContent) 225 263 226 264 await vitest.waitForStdout('New code running') 227 - await vitest.waitForStdout('RERUN ../../math.ts') 265 + await vitest.waitForStdout('RERUN ../math.ts') 228 266 await vitest.waitForStdout('1 passed') 229 267 230 268 vitest.write('q')
+39 -9
test/e2e/test/watch/global-setup-rerun.test.ts
··· 1 - import { editFile, runVitest } from '#test-utils' 1 + import { runInlineTests } from '#test-utils' 2 2 import { expect, test } from 'vitest' 3 3 4 - const testFile = 'fixtures/watch/math.test.ts' 4 + const fixture = { 5 + 'math.ts': /* ts */ ` 6 + export function sum(a: number, b: number) { 7 + return a + b 8 + } 9 + `, 10 + 'math.test.ts': /* ts */ ` 11 + import { expect, test } from 'vitest' 12 + 13 + import { sum } from './math' 14 + 15 + test('sum', () => { 16 + expect(sum(1, 2)).toBe(3) 17 + }) 18 + `, 19 + 'globalSetup.ts': /* ts */ ` 20 + import { TestProject } from 'vitest/node'; 21 + 22 + const calls: string[] = []; 23 + 24 + (globalThis as any).__CALLS = calls 25 + 26 + export default (project: TestProject) => { 27 + calls.push('start') 28 + project.onTestsRerun(() => { 29 + calls.push('rerun') 30 + }) 31 + return () => { 32 + calls.push('end') 33 + } 34 + } 35 + `, 36 + } 5 37 6 38 test('global setup calls hooks correctly when file changes', async () => { 7 - const { vitest, ctx } = await runVitest({ 8 - root: 'fixtures/watch', 39 + const { vitest, ctx, fs } = await runInlineTests(fixture, { 9 40 watch: true, 10 41 include: ['math.test.ts'], 11 - globalSetup: ['./global-setup.ts'], 42 + globalSetup: ['./globalSetup.ts'], 12 43 }) 13 44 14 45 await vitest.waitForStdout('Waiting for file changes') ··· 16 47 const calls = (globalThis as any).__CALLS as string[] 17 48 expect(calls).toEqual(['start']) 18 49 19 - editFile(testFile, testFileContent => `${testFileContent}\n\n`) 50 + fs.editFile('math.test.ts', testFileContent => `${testFileContent}\n\n`) 20 51 21 52 await vitest.waitForStdout('RERUN') 22 53 expect(calls).toEqual(['start', 'rerun']) ··· 27 58 }) 28 59 29 60 test('global setup calls hooks correctly with a manual rerun', async () => { 30 - const { vitest, ctx } = await runVitest({ 31 - root: 'fixtures/watch', 61 + const { vitest, ctx } = await runInlineTests(fixture, { 32 62 watch: true, 33 63 include: ['math.test.ts'], 34 - globalSetup: ['./global-setup.ts'], 64 + globalSetup: ['./globalSetup.ts'], 35 65 }) 36 66 37 67 await vitest.waitForStdout('Waiting for file changes')
+7 -3
test/e2e/test/watch/related.test.ts
··· 2 2 import { resolve } from 'pathe' 3 3 import { test } from 'vitest' 4 4 5 + // `changed: true` is git-driven, so unlike the other watch tests this one has 6 + // to run against a committed fixture: an inline tmp directory is either 7 + // untracked (git reports every file as changed) or gitignored (git never 8 + // reports its files as changed, even after edits). 5 9 test('when nothing is changed, run nothing but keep watching', async () => { 6 10 const { vitest } = await runVitest({ 7 - root: 'fixtures/watch', 11 + root: 'fixtures/related', 8 12 watch: true, 9 13 changed: true, 10 14 }) ··· 12 16 await vitest.waitForStdout('No affected test files found') 13 17 await vitest.waitForStdout('Waiting for file changes...') 14 18 15 - editFile(resolve(import.meta.dirname, '../../fixtures/watch/math.ts'), content => `${content}\n\n`) 19 + editFile(resolve(import.meta.dirname, '../../fixtures/related/math.ts'), content => `${content}\n\n`) 16 20 17 21 await vitest.waitForStdout('RERUN ../../math.ts') 18 22 await vitest.waitForStdout('1 passed') 19 23 20 - editFile(resolve(import.meta.dirname, '../../fixtures/watch/math.test.ts'), content => `${content}\n\n`) 24 + editFile(resolve(import.meta.dirname, '../../fixtures/related/math.test.ts'), content => `${content}\n\n`) 21 25 22 26 await vitest.waitForStdout('RERUN ../../math.test.ts') 23 27 await vitest.waitForStdout('1 passed')
+10 -5
test/e2e/test/watch/restart-coalescing.test.ts
··· 1 - import { runVitest } from '#test-utils' 1 + import { runInlineTests } from '#test-utils' 2 2 import { expect, test } from 'vitest' 3 3 4 4 // chokidar regularly delivers several change events for one config edit, each ··· 7 7 // were re-instantiated but not yet initialized, crashing the run with 8 8 // "Cannot read properties of undefined (reading 'logger')". 9 9 test('concurrent restarts are coalesced instead of overlapping', async () => { 10 - const { ctx, vitest } = await runVitest({ 11 - root: 'fixtures/watch', 12 - watch: true, 13 - }) 10 + const { ctx, vitest } = await runInlineTests({ 11 + 'basic.test.ts': /* ts */ ` 12 + import { expect, test } from 'vitest' 13 + 14 + test('basic', () => { 15 + expect(1).toBe(1) 16 + }) 17 + `, 18 + }, { watch: true }) 14 19 15 20 const restart = (ctx as any)._restart.bind(ctx) 16 21 await Promise.all([restart('config'), restart('config'), restart('config')])
+19 -13
test/e2e/test/watch/stdout.test.ts
··· 1 - import { readFileSync, statSync, writeFileSync } from 'node:fs' 2 - import { restoreFile, runVitest } from '#test-utils' 3 - import { afterEach, test } from 'vitest' 4 - 5 - const testFile = 'fixtures/watch/math.test.ts' 6 - const testFileContent = readFileSync(testFile, 'utf-8') 7 - const testFileStat = statSync(testFile) 8 - 9 - afterEach(() => { 10 - restoreFile(testFile, testFileContent, testFileStat) 11 - }) 1 + import { runInlineTests } from '#test-utils' 2 + import { test } from 'vitest' 12 3 13 4 test('console.log is visible on test re-run', async () => { 14 - const { vitest } = await runVitest({ root: 'fixtures/watch', watch: true }) 5 + const { vitest, fs } = await runInlineTests({ 6 + 'math.ts': /* ts */ ` 7 + export function sum(a: number, b: number) { 8 + return a + b 9 + } 10 + `, 11 + 'math.test.ts': /* ts */ ` 12 + import { expect, test } from 'vitest' 13 + 14 + import { sum } from './math' 15 + 16 + test('sum', () => { 17 + expect(sum(1, 2)).toBe(3) 18 + }) 19 + `, 20 + }, { watch: true }) 15 21 16 22 const testCase = ` 17 23 test('test with logging', () => { ··· 22 28 }) 23 29 ` 24 30 25 - writeFileSync(testFile, `${testFileContent}${testCase}`, 'utf8') 31 + fs.editFile('math.test.ts', content => `${content}${testCase}`) 26 32 27 33 await vitest.waitForStdout('stdout | math.test.ts > test with logging') 28 34 await vitest.waitForStdout('First')
-1
test/e2e/fixtures/watch/force-watch/trigger.js
··· 1 - export const trigger = false