Skip to content

Commit 0f44d2c

Browse files
sheremet-vaAriPerkkio
andauthoredMar 28, 2023
feat: implement istanbul coverage support for browser testing (#3040)
Co-authored-by: AriPerkkio <ari.perkkio@gmail.com>
1 parent 09fec84 commit 0f44d2c

32 files changed

+458
-151
lines changed
 

‎packages/browser/src/client/main.ts

+15-3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ ws.addEventListener('open', async () => {
5757
const { getSafeTimers } = await importId('vitest/utils') as typeof import('vitest/utils')
5858
const safeRpc = createSafeRpc(client, getSafeTimers)
5959

60+
// @ts-expect-error untyped global for internal use
61+
globalThis.__vitest_browser__ = true
6062
// @ts-expect-error mocking vitest apis
6163
globalThis.__vitest_worker__ = {
6264
config,
@@ -82,11 +84,20 @@ async function runTests(paths: string[], config: any) {
8284
const viteClientPath = '/@vite/client'
8385
await import(viteClientPath)
8486

85-
const { startTests, setupCommonEnv, setupSnapshotEnvironment } = await importId('vitest/browser') as typeof import('vitest/browser')
87+
const {
88+
startTests,
89+
setupCommonEnv,
90+
setupSnapshotEnvironment,
91+
takeCoverageInsideWorker,
92+
} = await importId('vitest/browser') as typeof import('vitest/browser')
93+
94+
const executor = {
95+
executeId: (id: string) => importId(id),
96+
}
8697

8798
if (!runner) {
8899
const { VitestTestRunner } = await importId('vitest/runners') as typeof import('vitest/runners')
89-
const BrowserRunner = createBrowserRunner(VitestTestRunner)
100+
const BrowserRunner = createBrowserRunner(VitestTestRunner, { takeCoverage: () => takeCoverageInsideWorker(config.coverage, executor) })
90101
runner = new BrowserRunner({ config, browserHashMap })
91102
}
92103

@@ -104,7 +115,8 @@ async function runTests(paths: string[], config: any) {
104115
const now = `${new Date().getTime()}`
105116
files.forEach(i => browserHashMap.set(i, now))
106117

107-
await startTests(files, runner)
118+
for (const file of files)
119+
await startTests([file], runner)
108120
}
109121
finally {
110122
await rpcDone()

‎packages/browser/src/client/runner.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ interface BrowserRunnerOptions {
77
browserHashMap: Map<string, string>
88
}
99

10-
export function createBrowserRunner(original: any) {
10+
interface CoverageHandler {
11+
takeCoverage: () => Promise<unknown>
12+
}
13+
14+
export function createBrowserRunner(original: any, coverageModule: CoverageHandler | null) {
1115
return class BrowserTestRunner extends original {
1216
public config: ResolvedConfig
1317
hashMap = new Map<string, string>()
@@ -25,6 +29,12 @@ export function createBrowserRunner(original: any) {
2529
})
2630
}
2731

32+
async onAfterRunSuite() {
33+
await super.onAfterRunSuite?.()
34+
const coverage = await coverageModule?.takeCoverage?.()
35+
await rpc().onAfterSuiteRun({ coverage })
36+
}
37+
2838
onCollected(files: File[]): unknown {
2939
return rpc().onCollected(files)
3040
}

‎packages/browser/src/node/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ export default (base = '/'): Plugin[] => {
1818
{
1919
enforce: 'pre',
2020
name: 'vitest:browser',
21+
async config(viteConfig) {
22+
// Enables using ignore hint for coverage providers with @preserve keyword
23+
viteConfig.esbuild ||= {}
24+
viteConfig.esbuild.legalComments = 'inline'
25+
},
2126
async configureServer(server) {
2227
server.middlewares.use(
2328
base,

‎packages/coverage-c8/rollup.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import pkg from './package.json'
1010

1111
const entries = {
1212
index: 'src/index.ts',
13+
provider: 'src/provider.ts',
1314
}
1415

1516
const external = [

‎packages/coverage-c8/src/index.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
export * from './takeCoverage'
1+
import * as coverage from './takeCoverage'
22

3-
export async function getProvider() {
4-
const { C8CoverageProvider } = await import('./provider')
5-
return new C8CoverageProvider()
3+
export default {
4+
...coverage,
5+
async getProvider() {
6+
// to not bundle the provider
7+
const name = './provider.js'
8+
const { C8CoverageProvider } = await import(name) as typeof import('./provider')
9+
return new C8CoverageProvider()
10+
},
611
}

‎packages/coverage-istanbul/rollup.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import pkg from './package.json'
1010

1111
const entries = {
1212
index: 'src/index.ts',
13+
provider: 'src/provider.ts',
1314
}
1415

1516
const external = [

‎packages/coverage-istanbul/src/index.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { COVERAGE_STORE_KEY } from './constants'
22

33
export async function getProvider() {
4-
const { IstanbulCoverageProvider } = await import('./provider')
4+
// to not bundle the provider
5+
const providerPath = './provider.js'
6+
const { IstanbulCoverageProvider } = await import(providerPath) as typeof import('./provider')
57
return new IstanbulCoverageProvider()
68
}
79

@@ -15,3 +17,8 @@ export function takeCoverage() {
1517

1618
return coverage
1719
}
20+
21+
export default {
22+
getProvider,
23+
takeCoverage,
24+
}

‎packages/vitest/src/api/setup.ts

+3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ export function setup(ctx: Vitest, server?: ViteDevServer) {
4646
ctx.state.updateTasks(packs)
4747
await ctx.report('onTaskUpdate', packs)
4848
},
49+
onAfterSuiteRun(meta) {
50+
ctx.coverageProvider?.onAfterSuiteRun(meta)
51+
},
4952
getFiles() {
5053
return ctx.state.getFiles()
5154
},

‎packages/vitest/src/api/types.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { TransformResult } from 'vite'
2-
import type { File, ModuleGraphData, Reporter, ResolvedConfig, SnapshotResult, TaskResultPack, UserConsoleLog } from '../types'
2+
import type { AfterSuiteRunMeta, File, ModuleGraphData, Reporter, ResolvedConfig, SnapshotResult, TaskResultPack, UserConsoleLog } from '../types'
33

44
export interface TransformResultWithSource extends TransformResult {
55
source?: string
@@ -8,6 +8,7 @@ export interface TransformResultWithSource extends TransformResult {
88
export interface WebSocketHandlers {
99
onCollected(files?: File[]): Promise<void>
1010
onTaskUpdate(packs: TaskResultPack[]): void
11+
onAfterSuiteRun(meta: AfterSuiteRunMeta): void
1112
onDone(name: string): void
1213
sendLog(log: UserConsoleLog): void
1314
getFiles(): File[]

‎packages/vitest/src/browser.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { startTests } from '@vitest/runner'
22
export { setupCommonEnv } from './runtime/setup.common'
33
export { setupSnapshotEnvironment } from './integrations/snapshot/env'
4+
export { takeCoverageInsideWorker, stopCoverageInsideWorker, getCoverageProvider, startCoverageInsideWorker } from './integrations/coverage'

‎packages/vitest/src/integrations/browser/server.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { Vitest } from '../../node'
66
import type { UserConfig } from '../../types/config'
77
import { ensurePackageInstalled } from '../../node/pkg'
88
import { resolveApiServerConfig } from '../../node/config'
9+
import { CoverageTransform } from '../../node/plugins/coverageTransform'
910

1011
export async function createBrowserServer(ctx: Vitest, options: UserConfig) {
1112
const root = ctx.config.root
@@ -31,6 +32,7 @@ export async function createBrowserServer(ctx: Vitest, options: UserConfig) {
3132
},
3233
plugins: [
3334
(await import('@vitest/browser')).default('/'),
35+
CoverageTransform(ctx),
3436
{
3537
enforce: 'post',
3638
name: 'vitest:browser:config',
@@ -44,14 +46,19 @@ export async function createBrowserServer(ctx: Vitest, options: UserConfig) {
4446
config.optimizeDeps ??= {}
4547
config.optimizeDeps.entries ??= []
4648

47-
const root = config.root || process.cwd()
4849
const [...entries] = await ctx.globAllTestFiles(ctx.config, ctx.config.dir || root)
4950
entries.push(...ctx.config.setupFiles)
5051

5152
if (typeof config.optimizeDeps.entries === 'string')
5253
config.optimizeDeps.entries = [config.optimizeDeps.entries]
5354

5455
config.optimizeDeps.entries.push(...entries)
56+
57+
return {
58+
resolve: {
59+
alias: config.test?.alias,
60+
},
61+
}
5562
},
5663
},
5764
],

‎packages/vitest/src/integrations/coverage.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { importModule } from 'local-pkg'
21
import type { CoverageOptions, CoverageProvider, CoverageProviderModule } from '../types'
32

43
interface Loader {
@@ -16,8 +15,10 @@ async function resolveCoverageProviderModule(options: CoverageOptions | undefine
1615

1716
const provider = options.provider
1817

19-
if (provider === 'c8' || provider === 'istanbul')
20-
return await importModule<CoverageProviderModule>(CoverageProviderMap[provider])
18+
if (provider === 'c8' || provider === 'istanbul') {
19+
const { default: coverageModule } = await loader.executeId(CoverageProviderMap[provider])
20+
return coverageModule
21+
}
2122

2223
let customProviderModule
2324

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

+10
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ export function resolveConfig(
113113
}
114114
}
115115

116+
if (resolved.coverage.provider === 'c8' && resolved.coverage.enabled && isBrowserEnabled(resolved))
117+
throw new Error('@vitest/coverage-c8 does not work with --browser. Use @vitest/coverage-istanbul instead')
118+
116119
resolved.deps = resolved.deps || {}
117120
// vitenode will try to import such file with native node,
118121
// but then our mocker will not work properly
@@ -263,3 +266,10 @@ export function resolveConfig(
263266

264267
return resolved
265268
}
269+
270+
export function isBrowserEnabled(config: ResolvedConfig) {
271+
if (config.browser.enabled)
272+
return true
273+
274+
return config.poolMatchGlobs?.length && config.poolMatchGlobs.some(([, pool]) => pool === 'browser')
275+
}

‎packages/vitest/src/node/core.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { createPool } from './pool'
1818
import type { ProcessPool } from './pool'
1919
import { createBenchmarkReporters, createReporters } from './reporters/utils'
2020
import { StateManager } from './state'
21-
import { resolveConfig } from './config'
21+
import { isBrowserEnabled, resolveConfig } from './config'
2222
import { Logger } from './logger'
2323
import { VitestCache } from './cache'
2424
import { VitestServer } from './server'
@@ -725,9 +725,7 @@ export class Vitest {
725725
}
726726

727727
isBrowserEnabled() {
728-
if (this.config.browser.enabled)
729-
return true
730-
return this.config.poolMatchGlobs?.length && this.config.poolMatchGlobs.some(([, pool]) => pool === 'browser')
728+
return isBrowserEnabled(this.config)
731729
}
732730

733731
// The server needs to be running for communication
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import type { Plugin as VitePlugin } from 'vite'
2+
import { normalizeRequestId } from 'vite-node/utils'
23

34
import type { Vitest } from '../core'
45

56
export function CoverageTransform(ctx: Vitest): VitePlugin | null {
67
return {
78
name: 'vitest:coverage-transform',
89
transform(srcCode, id) {
9-
return ctx.coverageProvider?.onFileTransform?.(srcCode, id, this)
10+
return ctx.coverageProvider?.onFileTransform?.(srcCode, normalizeRequestId(id), this)
1011
},
1112
}
1213
}

‎pnpm-lock.yaml

+230-42
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/browser/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"test": "pnpm run test:webdriverio && pnpm run test:playwright",
77
"test:webdriverio": "PROVIDER=webdriverio node --test specs/",
88
"test:playwright": "PROVIDER=playwright node --test specs/",
9-
"coverage": "vitest run --coverage"
9+
"coverage": "vitest --coverage.enabled --coverage.provider=istanbul"
1010
},
1111
"devDependencies": {
1212
"@vitest/browser": "workspace:*",

‎test/browser/specs/runner.test.mjs

+6-6
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@ import { readFile } from 'node:fs/promises'
33
import test from 'node:test'
44
import { execa } from 'execa'
55

6-
const browser = process.env.BROWSER || 'chrome'
6+
const browser = process.env.BROWSER || (process.env.PROVIDER === 'playwright' ? 'chromium' : 'chrome')
77

8-
const { stderr, stdout } = await execa('npx', ['vitest', `--browser=${browser}`], {
8+
const { stderr, stdout } = await execa('npx', ['vitest', '--run', `--browser.name=${browser}`, '--browser.headless'], {
99
env: {
1010
...process.env,
1111
CI: 'true',
1212
NO_COLOR: 'true',
1313
},
1414
})
1515

16-
test('tests are actually running', async () => {
16+
await test('tests are actually running', async () => {
1717
const browserResult = await readFile('./browser.json', 'utf-8')
1818
const browserResultJson = JSON.parse(browserResult)
1919

@@ -23,7 +23,7 @@ test('tests are actually running', async () => {
2323
assert.ok(result.status === 'passed', `${result.name} has failed`)
2424
})
2525

26-
test('logs are redirected to stdout', async () => {
26+
await test('logs are redirected to stdout', async () => {
2727
assert.match(stdout, /stdout | test\/logs.test.ts > logging to stdout/)
2828
assert.match(stdout, /hello from console.log/, 'prints console.log')
2929
assert.match(stdout, /hello from console.info/, 'prints console.info')
@@ -43,15 +43,15 @@ test('logs are redirected to stdout', async () => {
4343
assert.match(stdout, /time: [\d.]+ ms/, 'prints custom time')
4444
})
4545

46-
test('logs are redirected to stderr', async () => {
46+
await test('logs are redirected to stderr', async () => {
4747
assert.match(stderr, /stderr | test\/logs.test.ts > logging to stderr/)
4848
assert.match(stderr, /hello from console.error/, 'prints console.log')
4949
assert.match(stderr, /hello from console.warn/, 'prints console.info')
5050
assert.match(stderr, /Timer "invalid timeLog" does not exist/, 'prints errored timeLog')
5151
assert.match(stderr, /Timer "invalid timeEnd" does not exist/, 'prints errored timeEnd')
5252
})
5353

54-
test('popup apis should log a warning', () => {
54+
await test('popup apis should log a warning', () => {
5555
assert.ok(stderr.includes('Vitest encountered a \`alert\("test"\)\`'), 'prints warning for alert')
5656
assert.ok(stderr.includes('Vitest encountered a \`confirm\("test"\)\`'), 'prints warning for confirm')
5757
assert.ok(stderr.includes('Vitest encountered a \`prompt\("test"\)\`'), 'prints warning for prompt')

‎test/browser/src/createNode.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function createNode() {
2+
const div = document.createElement('div')
3+
div.textContent = 'Hello World'
4+
document.body.appendChild(div)
5+
return div
6+
}

‎test/browser/test/dom.test.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { expect, test } from 'vitest'
2+
import { createNode } from '#src/createNode'
23

3-
test('render div', async () => {
4-
const div = document.createElement('div')
5-
div.textContent = 'Hello World'
6-
document.body.appendChild(div)
4+
test('renders div', () => {
5+
const div = createNode()
76
expect(div.textContent).toBe('Hello World')
87
})

‎test/browser/tsconfig.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
{
22
"compilerOptions": {
3-
"esModuleInterop": true
3+
"module": "node16",
4+
"esModuleInterop": true,
5+
"rootDir": ".",
6+
"paths": {
7+
"#src/*": ["./src/*"]
8+
}
49
}
510
}

‎test/browser/vitest.config.ts ‎test/browser/vitest.config.mts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1+
import { dirname, resolve } from 'node:path'
2+
import { fileURLToPath } from 'node:url'
13
import { defineConfig } from 'vitest/config'
24

5+
const dir = dirname(fileURLToPath(import.meta.url))
6+
37
const noop = () => {}
48

59
export default defineConfig({
610
test: {
711
include: ['test/**.test.{ts,js}'],
812
browser: {
913
enabled: true,
10-
headless: true,
1114
name: 'chrome',
15+
headless: false,
1216
provider: process.env.PROVIDER || 'webdriverio',
1317
},
18+
alias: {
19+
'#src': resolve(dir, './src'),
20+
},
1421
open: false,
1522
isolate: false,
1623
outputFile: './browser.json',

‎test/config/test/failures.test.ts

+6
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,9 @@ test('inspect-brk cannot be used with threads', async () => {
3737

3838
expect(error).toMatch('Error: You cannot use --inspect-brk without "threads: false" or "singleThread: true"')
3939
})
40+
41+
test('c8 coverage provider cannot be used with browser', async () => {
42+
const { error } = await runVitest('run', ['--coverage.enabled', '--browser'])
43+
44+
expect(error).toMatch('Error: @vitest/coverage-c8 does not work with --browser. Use @vitest/coverage-istanbul instead')
45+
})

‎test/coverage-test/package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22
"name": "@vitest/test-coverage",
33
"private": true,
44
"scripts": {
5-
"test": "pnpm test:c8 && pnpm test:istanbul && pnpm test:custom && pnpm test:types",
5+
"test": "pnpm test:c8 && pnpm test:istanbul && pnpm test:custom && pnpm test:browser && pnpm test:types",
66
"test:c8": "node ./testing.mjs --provider c8",
77
"test:custom": "node ./testing.mjs --provider custom",
88
"test:istanbul": "node ./testing.mjs --provider istanbul",
9+
"test:browser": "node ./testing.mjs --browser --provider istanbul",
910
"test:types": "vitest typecheck --run --reporter verbose"
1011
},
1112
"devDependencies": {
1213
"@vitejs/plugin-vue": "latest",
14+
"@vitest/browser": "workspace:*",
1315
"@vue/test-utils": "latest",
1416
"happy-dom": "latest",
1517
"vite": "latest",
1618
"vitest": "workspace:*",
17-
"vue": "latest"
19+
"vue": "latest",
20+
"webdriverio": "latest"
1821
}
1922
}

‎test/coverage-test/test/coverage.test.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import { expect, test } from 'vitest'
2-
import { pythagoras } from '../src/index.mjs'
32
import { implicitElse } from '../src/implicitElse'
43
import { useImportEnv } from '../src/importEnv'
54

5+
const { pythagoras } = await (() => {
6+
if ('__vitest_browser__' in globalThis)
7+
// TODO: remove workaround after vite 4.3.2
8+
// @ts-expect-error extension is not specified
9+
return import('../src/index')
10+
const dynamicImport = '../src/index.mjs'
11+
return import(dynamicImport)
12+
})()
13+
614
test('Math.sqrt()', async () => {
715
expect(pythagoras(3, 4)).toBe(5)
816
})

‎test/coverage-test/testing.mjs

+15-3
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@ import { startVitest } from 'vitest/node'
44
const UPDATE_SNAPSHOTS = false
55

66
const provider = process.argv[1 + process.argv.indexOf('--provider')]
7+
const isBrowser = process.argv.includes('--browser')
78

89
const configs = [
910
// Run test cases. Generates coverage report.
1011
['test/', {
1112
include: ['test/*.test.*'],
12-
exclude: ['coverage-report-tests/**/*'],
13+
exclude: [
14+
'coverage-report-tests/**/*',
15+
// TODO: Include once mocking is supported in browser
16+
isBrowser && '**/no-esbuild-transform.test.js',
17+
].filter(Boolean),
1318
coverage: { enabled: true },
19+
browser: { enabled: isBrowser, name: 'chrome', headless: true },
1420
}],
1521

1622
// Run tests for checking coverage report contents.
@@ -23,19 +29,25 @@ const configs = [
2329
}],
2430
]
2531

32+
// Prevent the "vitest/src/node/browser/webdriver.ts" from calling process.exit
33+
const exit = process.exit
34+
process.exit = () => !isBrowser && exit()
35+
2636
for (const threads of [{ threads: true }, { threads: false }, { singleThread: true }]) {
2737
for (const isolate of [true, false]) {
2838
for (const [directory, config] of configs) {
2939
await startVitest('test', [directory], {
30-
name: `With settings: ${JSON.stringify({ ...threads, isolate, directory })}`,
40+
name: `With settings: ${JSON.stringify({ ...threads, isolate, directory, browser: config.browser?.enabled })}`,
3141
...config,
3242
update: UPDATE_SNAPSHOTS,
3343
...threads,
3444
isolate,
3545
})
3646

3747
if (process.exitCode)
38-
process.exit()
48+
exit()
3949
}
4050
}
4151
}
52+
53+
exit()

‎test/watch/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
"test": "vitest"
66
},
77
"devDependencies": {
8+
"@vitest/browser": "workspace:*",
89
"execa": "^7.0.0",
910
"strip-ansi": "^7.0.1",
1011
"vite": "latest",
11-
"vitest": "workspace:*"
12+
"vitest": "workspace:*",
13+
"webdriverio": "latest"
1214
}
1315
}

‎test/watch/test/file-watching.test.ts

+36-22
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { readFileSync, writeFileSync } from 'fs'
2-
import { afterEach, expect, test } from 'vitest'
2+
import { afterEach, describe, test } from 'vitest'
33

4-
import { startWatchMode, waitFor } from './utils'
5-
6-
const EDIT_COMMENT = '// Modified by file-watching.test.ts\n\n'
4+
import { startWatchMode } from './utils'
75

86
const sourceFile = 'fixtures/math.ts'
97
const sourceFileContent = readFileSync(sourceFile, 'utf-8')
@@ -14,6 +12,13 @@ const testFileContent = readFileSync(testFile, 'utf-8')
1412
const configFile = 'fixtures/vitest.config.ts'
1513
const configFileContent = readFileSync(configFile, 'utf-8')
1614

15+
function editFile(fileContent: string) {
16+
return `// Modified by file-watching.test.ts
17+
${fileContent}
18+
console.log("New code running"); // This is used to check that edited changes are actually run, and cached files are not run instead
19+
`
20+
}
21+
1722
afterEach(() => {
1823
writeFileSync(sourceFile, sourceFileContent, 'utf8')
1924
writeFileSync(testFile, testFileContent, 'utf8')
@@ -23,43 +28,52 @@ afterEach(() => {
2328
test('editing source file triggers re-run', async () => {
2429
const vitest = await startWatchMode()
2530

26-
writeFileSync(sourceFile, `${EDIT_COMMENT}${sourceFileContent}`, 'utf8')
31+
writeFileSync(sourceFile, editFile(sourceFileContent), 'utf8')
2732

28-
await waitFor(() => {
29-
expect(vitest.getOutput()).toContain('RERUN math.ts')
30-
expect(vitest.getOutput()).toContain('1 passed')
31-
})
33+
await vitest.waitForOutput('New code running')
34+
await vitest.waitForOutput('RERUN math.ts')
35+
await vitest.waitForOutput('1 passed')
3236
})
3337

3438
test('editing test file triggers re-run', async () => {
3539
const vitest = await startWatchMode()
3640

37-
writeFileSync(testFile, `${EDIT_COMMENT}${testFileContent}`, 'utf8')
41+
writeFileSync(testFile, editFile(testFileContent), 'utf8')
3842

39-
await waitFor(() => {
40-
expect(vitest.getOutput()).toMatch('RERUN math.test.ts')
41-
expect(vitest.getOutput()).toMatch('1 passed')
42-
})
43+
await vitest.waitForOutput('New code running')
44+
await vitest.waitForOutput('RERUN math.test.ts')
45+
await vitest.waitForOutput('1 passed')
4346
})
4447

4548
test('editing config file triggers re-run', async () => {
4649
const vitest = await startWatchMode()
4750

48-
writeFileSync(configFile, `${EDIT_COMMENT}${configFileContent}`, 'utf8')
51+
writeFileSync(configFile, editFile(configFileContent), 'utf8')
4952

50-
await waitFor(() => {
51-
expect(vitest.getOutput()).toMatch('Restarting due to config changes')
52-
expect(vitest.getOutput()).toMatch('2 passed')
53-
})
53+
await vitest.waitForOutput('New code running')
54+
await vitest.waitForOutput('Restarting due to config changes')
55+
await vitest.waitForOutput('2 passed')
5456
})
5557

5658
test('editing config file reloads new changes', async () => {
5759
const vitest = await startWatchMode()
5860

5961
writeFileSync(configFile, configFileContent.replace('reporters: \'verbose\'', 'reporters: \'tap\''), 'utf8')
6062

61-
await waitFor(() => {
62-
expect(vitest.getOutput()).toMatch('TAP version')
63-
expect(vitest.getOutput()).toMatch('ok 2')
63+
await vitest.waitForOutput('TAP version')
64+
await vitest.waitForOutput('ok 2')
65+
})
66+
67+
describe('browser', () => {
68+
test.runIf((process.platform !== 'win32'))('editing source file triggers re-run', async () => {
69+
const vitest = await startWatchMode('--browser.enabled', '--browser.headless', '--browser.name=chrome')
70+
71+
writeFileSync(sourceFile, editFile(sourceFileContent), 'utf8')
72+
73+
await vitest.waitForOutput('New code running')
74+
await vitest.waitForOutput('RERUN math.ts')
75+
await vitest.waitForOutput('1 passed')
76+
77+
vitest.write('q')
6478
})
6579
})

‎test/watch/test/stdin.test.ts

+8-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { expect, test } from 'vitest'
1+
import { test } from 'vitest'
22

3-
import { startWatchMode, waitFor } from './utils'
3+
import { startWatchMode } from './utils'
44

55
test('quit watch mode', async () => {
66
const vitest = await startWatchMode()
@@ -15,31 +15,23 @@ test('filter by filename', async () => {
1515

1616
vitest.write('p')
1717

18-
await waitFor(() => {
19-
expect(vitest.getOutput()).toMatch('Input filename pattern')
20-
})
18+
await vitest.waitForOutput('Input filename pattern')
2119

2220
vitest.write('math\n')
2321

24-
await waitFor(() => {
25-
expect(vitest.getOutput()).toMatch('Filename pattern: math')
26-
expect(vitest.getOutput()).toMatch('1 passed')
27-
})
22+
await vitest.waitForOutput('Filename pattern: math')
23+
await vitest.waitForOutput('1 passed')
2824
})
2925

3026
test('filter by test name', async () => {
3127
const vitest = await startWatchMode()
3228

3329
vitest.write('t')
3430

35-
await waitFor(() => {
36-
expect(vitest.getOutput()).toMatch('Input test name pattern')
37-
})
31+
await vitest.waitForOutput('Input test name pattern')
3832

3933
vitest.write('sum\n')
4034

41-
await waitFor(() => {
42-
expect(vitest.getOutput()).toMatch('Test name pattern: /sum/')
43-
expect(vitest.getOutput()).toMatch('1 passed')
44-
})
35+
await vitest.waitForOutput('Test name pattern: /sum/')
36+
await vitest.waitForOutput('1 passed')
4537
})

‎test/watch/test/stdout.test.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { readFileSync, writeFileSync } from 'fs'
2-
import { afterEach, expect, test } from 'vitest'
2+
import { afterEach, test } from 'vitest'
33

4-
import { startWatchMode, waitFor } from './utils'
4+
import { startWatchMode } from './utils'
55

66
const testFile = 'fixtures/math.test.ts'
77
const testFileContent = readFileSync(testFile, 'utf-8')
@@ -23,10 +23,8 @@ test('test with logging', () => {
2323

2424
writeFileSync(testFile, `${testFileContent}${testCase}`, 'utf8')
2525

26-
await waitFor(() => {
27-
expect(vitest.getOutput()).toMatch('stdout | math.test.ts > test with logging')
28-
expect(vitest.getOutput()).toMatch('First')
29-
expect(vitest.getOutput()).toMatch('Second')
30-
expect(vitest.getOutput()).toMatch('Third')
31-
})
26+
await vitest.waitForOutput('stdout | math.test.ts > test with logging')
27+
await vitest.waitForOutput('First')
28+
await vitest.waitForOutput('Second')
29+
await vitest.waitForOutput('Third')
3230
})

‎test/watch/test/utils.ts

+26-23
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,41 @@
1-
import { afterEach, expect } from 'vitest'
1+
import { afterEach } from 'vitest'
22
import { execa } from 'execa'
33
import stripAnsi from 'strip-ansi'
44

5-
export async function startWatchMode() {
6-
const subprocess = execa('vitest', ['--root', 'fixtures'])
5+
export async function startWatchMode(...args: string[]) {
6+
const subprocess = execa('vitest', ['--root', 'fixtures', ...args])
77

88
let setDone: (value?: unknown) => void
99
const isDone = new Promise(resolve => (setDone = resolve))
1010

1111
const vitest = {
1212
output: '',
13+
listeners: [] as (() => void)[],
1314
isDone,
1415
write(text: string) {
1516
this.resetOutput()
1617
subprocess.stdin!.write(text)
1718
},
18-
getOutput() {
19-
return this.output
19+
waitForOutput(expected: string) {
20+
return new Promise<void>((resolve, reject) => {
21+
if (this.output.includes(expected))
22+
return resolve()
23+
24+
const timeout = setTimeout(() => {
25+
reject(new Error(`Timeout when waiting for output "${expected}".\nReceived:\n${this.output}`))
26+
}, 20_000)
27+
28+
const listener = () => {
29+
if (this.output.includes(expected)) {
30+
if (timeout)
31+
clearTimeout(timeout)
32+
33+
resolve()
34+
}
35+
}
36+
37+
this.listeners.push(listener)
38+
})
2039
},
2140
resetOutput() {
2241
this.output = ''
@@ -25,6 +44,7 @@ export async function startWatchMode() {
2544

2645
subprocess.stdout!.on('data', (data) => {
2746
vitest.output += stripAnsi(data.toString())
47+
vitest.listeners.forEach(fn => fn())
2848
})
2949

3050
subprocess.on('exit', () => setDone())
@@ -38,25 +58,8 @@ export async function startWatchMode() {
3858
})
3959

4060
// Wait for initial test run to complete
41-
await waitFor(() => {
42-
expect(vitest.getOutput()).toMatch('Waiting for file changes')
43-
})
61+
await vitest.waitForOutput('Waiting for file changes')
4462
vitest.resetOutput()
4563

4664
return vitest
4765
}
48-
49-
export async function waitFor(method: () => unknown, retries = 100): Promise<void> {
50-
try {
51-
method()
52-
}
53-
catch (error) {
54-
if (retries === 0) {
55-
console.error(error)
56-
throw error
57-
}
58-
59-
await new Promise(resolve => setTimeout(resolve, 250))
60-
return waitFor(method, retries - 1)
61-
}
62-
}

‎tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"./packages/ui/cypress/**",
5252
"./examples/**/*.*",
5353
"./bench/**",
54-
"./test/typescript/**"
54+
"./test/typescript/**",
55+
"./test/browser/**"
5556
]
5657
}

0 commit comments

Comments
 (0)
Please sign in to comment.