diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c420599a419..1b51cf5ab930 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1276,6 +1276,18 @@ importers: vite-node: link:../../packages/vite-node vitest: link:../../packages/vitest + test/watch: + specifiers: + execa: ^7.0.0 + strip-ansi: ^7.0.1 + vite: ^4.0.0 + vitest: workspace:* + devDependencies: + execa: 7.0.0 + strip-ansi: 7.0.1 + vite: 4.0.0 + vitest: link:../../packages/vitest + test/web-worker: specifiers: '@vitest/web-worker': workspace:* diff --git a/test/watch/fixtures/example.test.ts b/test/watch/fixtures/example.test.ts new file mode 100644 index 000000000000..4fd8bd10912c --- /dev/null +++ b/test/watch/fixtures/example.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from 'vitest' + +import { getHelloWorld } from './example' + +test('getHello', async () => { + expect(getHelloWorld()).toBe('Hello world') +}) diff --git a/test/watch/fixtures/example.ts b/test/watch/fixtures/example.ts new file mode 100644 index 000000000000..1c18cd94437c --- /dev/null +++ b/test/watch/fixtures/example.ts @@ -0,0 +1,3 @@ +export function getHelloWorld() { + return 'Hello world' +} diff --git a/test/watch/fixtures/math.test.ts b/test/watch/fixtures/math.test.ts new file mode 100644 index 000000000000..e1994a02ed3d --- /dev/null +++ b/test/watch/fixtures/math.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from 'vitest' + +import { sum } from './math' + +test('sum', () => { + expect(sum(1, 2)).toBe(3) +}) diff --git a/test/watch/fixtures/math.ts b/test/watch/fixtures/math.ts new file mode 100644 index 000000000000..5d8550bb9d4d --- /dev/null +++ b/test/watch/fixtures/math.ts @@ -0,0 +1,3 @@ +export function sum(a: number, b: number) { + return a + b +} diff --git a/test/watch/fixtures/vitest.config.ts b/test/watch/fixtures/vitest.config.ts new file mode 100644 index 000000000000..83ca9cdd40cb --- /dev/null +++ b/test/watch/fixtures/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config' + +// Patch stdin on the process so that we can fake it to seem like a real interactive terminal and pass the TTY checks +process.stdin.isTTY = true +process.stdin.setRawMode = () => process.stdin + +export default defineConfig({ + test: { + watch: true, + outputTruncateLength: 999, + + // This configuration is edited by tests + reporters: 'verbose', + }, +}) diff --git a/test/watch/package.json b/test/watch/package.json new file mode 100644 index 000000000000..7a005dc398f8 --- /dev/null +++ b/test/watch/package.json @@ -0,0 +1,13 @@ +{ + "name": "@vitest/test-watch", + "private": true, + "scripts": { + "test": "vitest" + }, + "devDependencies": { + "execa": "^7.0.0", + "strip-ansi": "^7.0.1", + "vite": "latest", + "vitest": "workspace:*" + } +} diff --git a/test/watch/test/file-watching.test.ts b/test/watch/test/file-watching.test.ts new file mode 100644 index 000000000000..7e3c82c5e7f0 --- /dev/null +++ b/test/watch/test/file-watching.test.ts @@ -0,0 +1,65 @@ +import { readFileSync, writeFileSync } from 'fs' +import { afterEach, expect, test } from 'vitest' + +import { startWatchMode, waitFor } from './utils' + +const EDIT_COMMENT = '// Modified by file-watching.test.ts\n\n' + +const sourceFile = 'fixtures/math.ts' +const sourceFileContent = readFileSync(sourceFile, 'utf-8') + +const testFile = 'fixtures/math.test.ts' +const testFileContent = readFileSync(testFile, 'utf-8') + +const configFile = 'fixtures/vitest.config.ts' +const configFileContent = readFileSync(configFile, 'utf-8') + +afterEach(() => { + writeFileSync(sourceFile, sourceFileContent, 'utf8') + writeFileSync(testFile, testFileContent, 'utf8') + writeFileSync(configFile, configFileContent, 'utf8') +}) + +test('editing source file triggers re-run', async () => { + const vitest = await startWatchMode() + + writeFileSync(sourceFile, `${EDIT_COMMENT}${sourceFileContent}`, 'utf8') + + await waitFor(() => { + expect(vitest.getOutput()).toContain('RERUN math.ts') + expect(vitest.getOutput()).toContain('1 passed') + }) +}) + +test('editing test file triggers re-run', async () => { + const vitest = await startWatchMode() + + writeFileSync(testFile, `${EDIT_COMMENT}${testFileContent}`, 'utf8') + + await waitFor(() => { + expect(vitest.getOutput()).toMatch('RERUN math.test.ts') + expect(vitest.getOutput()).toMatch('1 passed') + }) +}) + +test('editing config file triggers re-run', async () => { + const vitest = await startWatchMode() + + writeFileSync(configFile, `${EDIT_COMMENT}${configFileContent}`, 'utf8') + + await waitFor(() => { + expect(vitest.getOutput()).toMatch('Restarting due to config changes') + expect(vitest.getOutput()).toMatch('2 passed') + }) +}) + +test('editing config file reloads new changes', async () => { + const vitest = await startWatchMode() + + writeFileSync(configFile, configFileContent.replace('reporters: \'verbose\'', 'reporters: \'tap\''), 'utf8') + + await waitFor(() => { + expect(vitest.getOutput()).toMatch('TAP version') + expect(vitest.getOutput()).toMatch('ok 2') + }) +}) diff --git a/test/watch/test/stdin.test.ts b/test/watch/test/stdin.test.ts new file mode 100644 index 000000000000..3ec282c2c1c0 --- /dev/null +++ b/test/watch/test/stdin.test.ts @@ -0,0 +1,45 @@ +import { expect, test } from 'vitest' + +import { startWatchMode, waitFor } from './utils' + +test('quit watch mode', async () => { + const vitest = await startWatchMode() + + vitest.write('q') + + await vitest.isDone +}) + +test('filter by filename', async () => { + const vitest = await startWatchMode() + + vitest.write('p') + + await waitFor(() => { + expect(vitest.getOutput()).toMatch('Input filename pattern') + }) + + vitest.write('math\n') + + await waitFor(() => { + expect(vitest.getOutput()).toMatch('Filename pattern: math') + expect(vitest.getOutput()).toMatch('1 passed') + }) +}) + +test('filter by test name', async () => { + const vitest = await startWatchMode() + + vitest.write('t') + + await waitFor(() => { + expect(vitest.getOutput()).toMatch('Input test name pattern') + }) + + vitest.write('sum\n') + + await waitFor(() => { + expect(vitest.getOutput()).toMatch('Test name pattern: /sum/') + expect(vitest.getOutput()).toMatch('1 passed') + }) +}) diff --git a/test/watch/test/utils.ts b/test/watch/test/utils.ts new file mode 100644 index 000000000000..4d89702769be --- /dev/null +++ b/test/watch/test/utils.ts @@ -0,0 +1,62 @@ +import { afterEach, expect } from 'vitest' +import { execa } from 'execa' +import stripAnsi from 'strip-ansi' + +export async function startWatchMode() { + const subprocess = execa('vitest', ['--root', 'fixtures']) + + let setDone: (value?: unknown) => void + const isDone = new Promise(resolve => (setDone = resolve)) + + const vitest = { + output: '', + isDone, + write(text: string) { + this.resetOutput() + subprocess.stdin!.write(text) + }, + getOutput() { + return this.output + }, + resetOutput() { + this.output = '' + }, + } + + subprocess.stdout!.on('data', (data) => { + vitest.output += stripAnsi(data.toString()) + }) + + subprocess.on('exit', () => setDone()) + + // Manually stop the processes so that each test don't have to do this themselves + afterEach(async () => { + if (subprocess.exitCode === null) + subprocess.kill() + + await vitest.isDone + }) + + // Wait for initial test run to complete + await waitFor(() => { + expect(vitest.getOutput()).toMatch('Waiting for file changes') + }) + vitest.resetOutput() + + return vitest +} + +export async function waitFor(method: () => unknown, retries = 100): Promise { + try { + method() + } + catch (error) { + if (retries === 0) { + console.error(error) + throw error + } + + await new Promise(resolve => setTimeout(resolve, 250)) + return waitFor(method, retries - 1) + } +} diff --git a/test/watch/vitest.config.ts b/test/watch/vitest.config.ts new file mode 100644 index 000000000000..9ef05a07f993 --- /dev/null +++ b/test/watch/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + reporters: 'verbose', + include: ['test/**/*.test.*'], + + // For Windows CI mostly + testTimeout: 30_000, + + // Test cases may have side effects, e.g. files under fixtures/ are modified on the fly to trigger file watchers + singleThread: true, + }, +})