Skip to content

Commit 34199bd

Browse files
authoredAug 12, 2024··
feat(browser): support v8 coverage (#6273)
1 parent 198a3e6 commit 34199bd

28 files changed

+428
-202
lines changed
 

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

+6-13
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { loadDiffConfig, loadSnapshotSerializers, takeCoverageInsideWorker } fro
66
import { TraceMap, originalPositionFor } from 'vitest/utils'
77
import { page } from '@vitest/browser/context'
88
import { globalChannel } from '@vitest/browser/client'
9-
import { importFs, importId } from '../utils'
9+
import { executor } from '../utils'
1010
import { VitestBrowserSnapshotEnvironment } from './snapshot'
1111
import { rpc } from './rpc'
1212
import type { VitestBrowserClientMocker } from './mocker'
@@ -91,7 +91,7 @@ export function createBrowserRunner(
9191
if (coverage) {
9292
await rpc().onAfterSuiteRun({
9393
coverage,
94-
transformMode: 'web',
94+
transformMode: 'browser',
9595
projectName: this.config.name,
9696
})
9797
}
@@ -148,27 +148,20 @@ export async function initiateRunner(
148148
const runnerClass
149149
= config.mode === 'test' ? VitestTestRunner : NodeBenchmarkRunner
150150

151-
const executeId = (id: string) => {
152-
if (id[0] === '/' || id[1] === ':') {
153-
return importFs(id)
154-
}
155-
return importId(id)
156-
}
157-
158151
const BrowserRunner = createBrowserRunner(runnerClass, mocker, state, {
159152
takeCoverage: () =>
160-
takeCoverageInsideWorker(config.coverage, { executeId }),
153+
takeCoverageInsideWorker(config.coverage, executor),
161154
})
162155
if (!config.snapshotOptions.snapshotEnvironment) {
163156
config.snapshotOptions.snapshotEnvironment = new VitestBrowserSnapshotEnvironment()
164157
}
165158
const runner = new BrowserRunner({
166159
config,
167160
})
168-
const executor = { executeId } as VitestExecutor
161+
169162
const [diffOptions] = await Promise.all([
170-
loadDiffConfig(config, executor),
171-
loadSnapshotSerializers(config, executor),
163+
loadDiffConfig(config, executor as unknown as VitestExecutor),
164+
loadSnapshotSerializers(config, executor as unknown as VitestExecutor),
172165
])
173166
runner.config.diffOptions = diffOptions
174167
cachedRunner = runner

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { SpyModule, collectTests, setupCommonEnv, startTests } from 'vitest/browser'
1+
import { SpyModule, collectTests, setupCommonEnv, startCoverageInsideWorker, startTests, stopCoverageInsideWorker } from 'vitest/browser'
22
import { page } from '@vitest/browser/context'
33
import { channel, client, onCancel } from '@vitest/browser/client'
4-
import { getBrowserState, getConfig, getWorkerState } from '../utils'
4+
import { executor, getBrowserState, getConfig, getWorkerState } from '../utils'
55
import { setupDialogsSpy } from './dialog'
66
import { setupConsoleLogSpy } from './logger'
77
import { createSafeRpc } from './rpc'
@@ -114,6 +114,8 @@ async function executeTests(method: 'run' | 'collect', files: string[]) {
114114

115115
try {
116116
await setupCommonEnv(config)
117+
await startCoverageInsideWorker(config.coverage, executor)
118+
117119
for (const file of files) {
118120
state.filepath = file
119121

@@ -139,6 +141,8 @@ async function executeTests(method: 'run' | 'collect', files: string[]) {
139141
}, 'Cleanup Error')
140142
}
141143
state.environmentTeardownRun = true
144+
await stopCoverageInsideWorker(config.coverage, executor)
145+
142146
debug('finished running tests')
143147
done(files)
144148
}

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

+11
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ export async function importFs(id: string) {
1010
return getBrowserState().wrapModule(() => import(/* @vite-ignore */ name))
1111
}
1212

13+
export const executor = {
14+
isBrowser: true,
15+
16+
executeId: (id: string) => {
17+
if (id[0] === '/' || id[1] === ':') {
18+
return importFs(id)
19+
}
20+
return importId(id)
21+
},
22+
}
23+
1324
export function getConfig(): SerializedConfig {
1425
return getBrowserState().config
1526
}

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -195,14 +195,14 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
195195
return
196196
}
197197

198-
if (transformMode !== 'web' && transformMode !== 'ssr') {
198+
if (transformMode !== 'web' && transformMode !== 'ssr' && transformMode !== 'browser') {
199199
throw new Error(`Invalid transform mode: ${transformMode}`)
200200
}
201201

202202
let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT)
203203

204204
if (!entry) {
205-
entry = { web: [], ssr: [] }
205+
entry = { web: [], ssr: [], browser: [] }
206206
this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry)
207207
}
208208

@@ -251,6 +251,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
251251
for (const filenames of [
252252
coveragePerProject.ssr,
253253
coveragePerProject.web,
254+
coveragePerProject.browser,
254255
]) {
255256
const coverageMapByTransformMode = libCoverage.createCoverageMap({})
256257

‎packages/coverage-v8/package.json

+11
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
"types": "./dist/index.d.ts",
2929
"default": "./dist/index.js"
3030
},
31+
"./browser": {
32+
"types": "./dist/browser.d.ts",
33+
"default": "./dist/browser.js"
34+
},
3135
"./*": "./*"
3236
},
3337
"main": "./dist/index.js",
@@ -41,8 +45,14 @@
4145
"dev": "rollup -c --watch --watch.include 'src/**'"
4246
},
4347
"peerDependencies": {
48+
"@vitest/browser": "workspace:*",
4449
"vitest": "workspace:*"
4550
},
51+
"peerDependenciesMeta": {
52+
"@vitest/browser": {
53+
"optional": true
54+
}
55+
},
4656
"dependencies": {
4757
"@ampproject/remapping": "^2.3.0",
4858
"@bcoe/v8-coverage": "^0.2.3",
@@ -63,6 +73,7 @@
6373
"@types/istanbul-lib-report": "^3.0.3",
6474
"@types/istanbul-lib-source-maps": "^4.0.4",
6575
"@types/istanbul-reports": "^3.0.4",
76+
"@vitest/browser": "workspace:*",
6677
"pathe": "^1.1.2",
6778
"v8-to-istanbul": "^9.3.0",
6879
"vite-node": "workspace:*",

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

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const pkg = require('./package.json')
1111

1212
const entries = {
1313
index: 'src/index.ts',
14+
browser: 'src/browser.ts',
1415
provider: 'src/provider.ts',
1516
}
1617

‎packages/coverage-v8/src/browser.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { cdp } from '@vitest/browser/context'
2+
import type { V8CoverageProvider } from './provider'
3+
import { loadProvider } from './load-provider'
4+
5+
const session = cdp()
6+
7+
type ScriptCoverage = Awaited<ReturnType<typeof session.send<'Profiler.takePreciseCoverage'>>>
8+
9+
export default {
10+
async startCoverage() {
11+
await session.send('Profiler.enable')
12+
await session.send('Profiler.startPreciseCoverage', {
13+
callCount: true,
14+
detailed: true,
15+
})
16+
},
17+
18+
async takeCoverage(): Promise<{ result: any[] }> {
19+
const coverage = await session.send('Profiler.takePreciseCoverage')
20+
const result: typeof coverage.result = []
21+
22+
// Reduce amount of data sent over rpc by doing some early result filtering
23+
for (const entry of coverage.result) {
24+
if (filterResult(entry)) {
25+
result.push({
26+
...entry,
27+
url: decodeURIComponent(entry.url.replace(window.location.origin, '')),
28+
})
29+
}
30+
}
31+
32+
return { result }
33+
},
34+
35+
async stopCoverage() {
36+
await session.send('Profiler.stopPreciseCoverage')
37+
await session.send('Profiler.disable')
38+
},
39+
40+
async getProvider(): Promise<V8CoverageProvider> {
41+
return loadProvider()
42+
},
43+
}
44+
45+
function filterResult(coverage: ScriptCoverage['result'][number]): boolean {
46+
if (!coverage.url.startsWith(window.location.origin)) {
47+
return false
48+
}
49+
50+
if (coverage.url.includes('/node_modules/')) {
51+
return false
52+
}
53+
54+
if (coverage.url.includes('__vitest_browser__')) {
55+
return false
56+
}
57+
58+
if (coverage.url.includes('__vitest__/assets')) {
59+
return false
60+
}
61+
62+
return true
63+
}

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

+46-11
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,58 @@
1-
import type { Profiler } from 'node:inspector'
2-
import * as coverage from './takeCoverage'
1+
import inspector, { type Profiler } from 'node:inspector'
2+
import { provider } from 'std-env'
33
import type { V8CoverageProvider } from './provider'
4+
import { loadProvider } from './load-provider'
5+
6+
const session = new inspector.Session()
47

58
export default {
69
startCoverage(): void {
7-
return coverage.startCoverage()
10+
session.connect()
11+
session.post('Profiler.enable')
12+
session.post('Profiler.startPreciseCoverage', {
13+
callCount: true,
14+
detailed: true,
15+
})
816
},
17+
918
takeCoverage(): Promise<{ result: Profiler.ScriptCoverage[] }> {
10-
return coverage.takeCoverage()
19+
return new Promise((resolve, reject) => {
20+
session.post('Profiler.takePreciseCoverage', async (error, coverage) => {
21+
if (error) {
22+
return reject(error)
23+
}
24+
25+
// Reduce amount of data sent over rpc by doing some early result filtering
26+
const result = coverage.result.filter(filterResult)
27+
28+
resolve({ result })
29+
})
30+
31+
if (provider === 'stackblitz') {
32+
resolve({ result: [] })
33+
}
34+
})
1135
},
36+
1237
stopCoverage(): void {
13-
return coverage.stopCoverage()
38+
session.post('Profiler.stopPreciseCoverage')
39+
session.post('Profiler.disable')
40+
session.disconnect()
1441
},
42+
1543
async getProvider(): Promise<V8CoverageProvider> {
16-
// to not bundle the provider
17-
const name = './provider.js'
18-
const { V8CoverageProvider } = (await import(
19-
name
20-
)) as typeof import('./provider')
21-
return new V8CoverageProvider()
44+
return loadProvider()
2245
},
2346
}
47+
48+
function filterResult(coverage: Profiler.ScriptCoverage): boolean {
49+
if (!coverage.url.startsWith('file://')) {
50+
return false
51+
}
52+
53+
if (coverage.url.includes('/node_modules/')) {
54+
return false
55+
}
56+
57+
return true
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// to not bundle the provider
2+
const name = './provider.js'
3+
4+
export async function loadProvider() {
5+
const { V8CoverageProvider } = (await import(/* @vite-ignore */ name)) as typeof import('./provider')
6+
7+
return new V8CoverageProvider()
8+
}

‎packages/coverage-v8/src/provider.ts

+71-50
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,16 @@ import { provider } from 'std-env'
2222
import createDebug from 'debug'
2323
import { cleanUrl } from 'vite-node/utils'
2424
import type { EncodedSourceMap, FetchResult } from 'vite-node'
25-
import {
26-
coverageConfigDefaults,
27-
} from 'vitest/config'
25+
import { coverageConfigDefaults } from 'vitest/config'
2826
import { BaseCoverageProvider } from 'vitest/coverage'
27+
import type { Vitest, WorkspaceProject } from 'vitest/node'
2928
import type {
3029
AfterSuiteRunMeta,
3130
CoverageProvider,
3231
CoverageV8Options,
3332
ReportContext,
3433
ResolvedCoverageOptions,
3534
} from 'vitest'
36-
import type { Vitest } from 'vitest/node'
3735
// @ts-expect-error missing types
3836
import _TestExclude from 'test-exclude'
3937

@@ -65,6 +63,8 @@ type ProjectName =
6563
| NonNullable<AfterSuiteRunMeta['projectName']>
6664
| typeof DEFAULT_PROJECT
6765

66+
type Entries<T> = [keyof T, T[keyof T]][]
67+
6868
// TODO: vite-node should export this
6969
const WRAPPER_LENGTH = 185
7070

@@ -74,6 +74,7 @@ const VITE_EXPORTS_LINE_PATTERN
7474
const DECORATOR_METADATA_PATTERN
7575
= /_ts_metadata\("design:paramtypes", \[[^\]]*\]\),*/g
7676
const DEFAULT_PROJECT: unique symbol = Symbol.for('default-project')
77+
const FILE_PROTOCOL = 'file://'
7778

7879
const debug = createDebug('vitest:coverage')
7980
let uniqueId = 0
@@ -124,9 +125,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
124125
lines: config.thresholds['100'] ? 100 : config.thresholds.lines,
125126
branches: config.thresholds['100'] ? 100 : config.thresholds.branches,
126127
functions: config.thresholds['100'] ? 100 : config.thresholds.functions,
127-
statements: config.thresholds['100']
128-
? 100
129-
: config.thresholds.statements,
128+
statements: config.thresholds['100'] ? 100 : config.thresholds.statements,
130129
},
131130
}
132131

@@ -183,14 +182,14 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
183182
* backwards compatibility is a breaking change.
184183
*/
185184
onAfterSuiteRun({ coverage, transformMode, projectName }: AfterSuiteRunMeta): void {
186-
if (transformMode !== 'web' && transformMode !== 'ssr') {
185+
if (transformMode !== 'web' && transformMode !== 'ssr' && transformMode !== 'browser') {
187186
throw new Error(`Invalid transform mode: ${transformMode}`)
188187
}
189188

190189
let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT)
191190

192191
if (!entry) {
193-
entry = { web: [], ssr: [] }
192+
entry = { web: [], ssr: [], browser: [] }
194193
this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry)
195194
}
196195

@@ -212,36 +211,30 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
212211
await Promise.all(this.pendingPromises)
213212
this.pendingPromises = []
214213

215-
for (const [
216-
projectName,
217-
coveragePerProject,
218-
] of this.coverageFiles.entries()) {
219-
for (const [transformMode, filenames] of Object.entries(
220-
coveragePerProject,
221-
) as [AfterSuiteRunMeta['transformMode'], Filename[]][]) {
214+
for (const [projectName, coveragePerProject] of this.coverageFiles.entries()) {
215+
for (const [transformMode, filenames] of Object.entries(coveragePerProject) as Entries<CoverageFilesByTransformMode>) {
222216
let merged: RawCoverage = { result: [] }
223217

224-
for (const chunk of this.toSlices(
225-
filenames,
226-
this.options.processingConcurrency,
227-
)) {
218+
const project = this.ctx.projects.find(p => p.getName() === projectName) || this.ctx.getCoreWorkspaceProject()
219+
220+
for (const chunk of this.toSlices(filenames, this.options.processingConcurrency)) {
228221
if (debug.enabled) {
229222
index += chunk.length
230223
debug('Covered files %d/%d', index, total)
231224
}
232225

233-
await Promise.all(
234-
chunk.map(async (filename) => {
235-
const contents = await fs.readFile(filename, 'utf-8')
236-
const coverage = JSON.parse(contents) as RawCoverage
237-
merged = mergeProcessCovs([merged, coverage])
238-
}),
226+
await Promise.all(chunk.map(async (filename) => {
227+
const contents = await fs.readFile(filename, 'utf-8')
228+
const coverage = JSON.parse(contents) as RawCoverage
229+
230+
merged = mergeProcessCovs([merged, coverage])
231+
}),
239232
)
240233
}
241234

242235
const converted = await this.convertCoverage(
243236
merged,
244-
projectName,
237+
project,
245238
transformMode,
246239
)
247240

@@ -404,6 +397,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
404397
const { originalSource } = await this.getSources(
405398
filename.href,
406399
transformResults,
400+
file => this.ctx.vitenode.transformRequest(file),
407401
)
408402

409403
const coverage = {
@@ -441,9 +435,10 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
441435
return merged
442436
}
443437

444-
private async getSources(
438+
private async getSources<TransformResult extends (FetchResult | Awaited<ReturnType<typeof this.ctx.vitenode.transformRequest>>)>(
445439
url: string,
446440
transformResults: TransformResults,
441+
onTransform: (filepath: string) => Promise<TransformResult>,
447442
functions: Profiler.FunctionCoverage[] = [],
448443
): Promise<{
449444
source: string
@@ -454,16 +449,11 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
454449
const filePath = normalize(fileURLToPath(url))
455450

456451
let isExecuted = true
457-
let transformResult:
458-
| FetchResult
459-
| Awaited<ReturnType<typeof this.ctx.vitenode.transformRequest>>
460-
= transformResults.get(filePath)
452+
let transformResult: FetchResult | TransformResult | undefined = transformResults.get(filePath)
461453

462454
if (!transformResult) {
463455
isExecuted = false
464-
transformResult = await this.ctx.vitenode
465-
.transformRequest(filePath)
466-
.catch(() => null)
456+
transformResult = await onTransform(removeStartsWith(url, FILE_PROTOCOL)).catch(() => undefined)
467457
}
468458

469459
const map = transformResult?.map as EncodedSourceMap | undefined
@@ -513,27 +503,49 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
513503

514504
private async convertCoverage(
515505
coverage: RawCoverage,
516-
projectName?: ProjectName,
517-
transformMode?: 'web' | 'ssr',
506+
project: WorkspaceProject = this.ctx.getCoreWorkspaceProject(),
507+
transformMode?: keyof CoverageFilesByTransformMode,
518508
): Promise<CoverageMap> {
519-
const viteNode
520-
= this.ctx.projects.find(project => project.getName() === projectName)
521-
?.vitenode || this.ctx.vitenode
522-
const fetchCache = transformMode
523-
? viteNode.fetchCaches[transformMode]
524-
: viteNode.fetchCache
509+
let fetchCache = project.vitenode.fetchCache
510+
511+
if (transformMode) {
512+
fetchCache = transformMode === 'browser' ? new Map() : project.vitenode.fetchCaches[transformMode]
513+
}
514+
525515
const transformResults = normalizeTransformResults(fetchCache)
526516

527-
const scriptCoverages = coverage.result.filter(result =>
528-
this.testExclude.shouldInstrument(fileURLToPath(result.url)),
529-
)
517+
async function onTransform(filepath: string) {
518+
if (transformMode === 'browser' && project.browser) {
519+
const result = await project.browser.vite.transformRequest(removeStartsWith(filepath, project.config.root))
520+
521+
if (result) {
522+
return { ...result, code: `${result.code}// <inline-source-map>` }
523+
}
524+
}
525+
return project.vitenode.transformRequest(filepath)
526+
}
527+
528+
const scriptCoverages = []
529+
530+
for (const result of coverage.result) {
531+
if (transformMode === 'browser') {
532+
if (result.url.startsWith('/@fs')) {
533+
result.url = `${FILE_PROTOCOL}${removeStartsWith(result.url, '/@fs')}`
534+
}
535+
else {
536+
result.url = `${FILE_PROTOCOL}${project.config.root}${result.url}`
537+
}
538+
}
539+
540+
if (this.testExclude.shouldInstrument(fileURLToPath(result.url))) {
541+
scriptCoverages.push(result)
542+
}
543+
}
544+
530545
const coverageMap = libCoverage.createCoverageMap({})
531546
let index = 0
532547

533-
for (const chunk of this.toSlices(
534-
scriptCoverages,
535-
this.options.processingConcurrency,
536-
)) {
548+
for (const chunk of this.toSlices(scriptCoverages, this.options.processingConcurrency)) {
537549
if (debug.enabled) {
538550
index += chunk.length
539551
debug('Converting %d/%d', index, scriptCoverages.length)
@@ -544,6 +556,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
544556
const sources = await this.getSources(
545557
url,
546558
transformResults,
559+
onTransform,
547560
functions,
548561
)
549562

@@ -645,3 +658,11 @@ function normalizeTransformResults(
645658

646659
return normalized
647660
}
661+
662+
function removeStartsWith(filepath: string, start: string) {
663+
if (filepath.startsWith(start)) {
664+
return filepath.slice(start.length)
665+
}
666+
667+
return filepath
668+
}

‎packages/coverage-v8/src/takeCoverage.ts

-55
This file was deleted.

‎packages/coverage-v8/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"extends": "../../tsconfig.base.json",
33
"compilerOptions": {
4-
"moduleResolution": "Bundler"
4+
"moduleResolution": "Bundler",
5+
"types": ["@vitest/browser/providers/playwright"]
56
},
67
"include": ["./src/**/*.ts"],
78
"exclude": ["./dist"]

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66

77
interface Loader {
88
executeId: (id: string) => Promise<{ default: CoverageProviderModule }>
9+
isBrowser?: boolean
910
}
1011

1112
export const CoverageProviderMap: Record<string, string> = {
@@ -24,9 +25,13 @@ async function resolveCoverageProviderModule(
2425
const provider = options.provider
2526

2627
if (provider === 'v8' || provider === 'istanbul') {
27-
const { default: coverageModule } = await loader.executeId(
28-
CoverageProviderMap[provider],
29-
)
28+
let builtInModule = CoverageProviderMap[provider]
29+
30+
if (provider === 'v8' && loader.isBrowser) {
31+
builtInModule += '/browser'
32+
}
33+
34+
const { default: coverageModule } = await loader.executeId(builtInModule)
3035

3136
if (!coverageModule) {
3237
throw new Error(

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

+11-8
Original file line numberDiff line numberDiff line change
@@ -222,14 +222,17 @@ export function resolveConfig(
222222
}
223223
}
224224

225-
if (
226-
resolved.coverage.provider === 'v8'
227-
&& resolved.coverage.enabled
228-
&& isBrowserEnabled(resolved)
229-
) {
230-
throw new Error(
231-
'@vitest/coverage-v8 does not work with --browser. Use @vitest/coverage-istanbul instead',
232-
)
225+
// In browser-mode v8-coverage works only with playwright + chromium
226+
if (resolved.browser.enabled && resolved.coverage.enabled && resolved.coverage.provider === 'v8') {
227+
if (!(resolved.browser.provider === 'playwright' && resolved.browser.name === 'chromium')) {
228+
const browserConfig = { browser: { provider: resolved.browser.provider, name: resolved.browser.name } }
229+
230+
throw new Error(
231+
`@vitest/coverage-v8 does not work with\n${JSON.stringify(browserConfig, null, 2)}\n`
232+
+ `\nUse either:\n${JSON.stringify({ browser: { provider: 'playwright', name: 'chromium' } }, null, 2)}`
233+
+ `\n\n...or change your coverage provider to:\n${JSON.stringify({ coverage: { provider: 'istanbul' } }, null, 2)}\n`,
234+
)
235+
}
233236
}
234237

235238
resolved.coverage.reporter = resolveCoverageReporters(resolved.coverage.reporter)

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export interface ModuleCache {
2424

2525
export interface AfterSuiteRunMeta {
2626
coverage?: unknown
27-
transformMode: TransformMode
27+
transformMode: TransformMode | 'browser'
2828
projectName?: string
2929
}
3030

‎pnpm-lock.yaml

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

‎test/config/fixtures/workspace/browser/workspace-with-browser.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export default defineWorkspace([
66
name: "Browser project",
77
browser: {
88
enabled: true,
9+
provider: 'webdriverio',
910
name: 'chrome'
1011
},
1112
}

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

+72-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test } from 'vitest'
1+
import { beforeEach, expect, test } from 'vitest'
22
import type { UserConfig } from 'vitest'
33
import { version } from 'vitest/package.json'
44

@@ -13,6 +13,20 @@ function runVitestCli(...cliArgs: string[]) {
1313
return testUtils.runVitestCli('run', 'fixtures/test/', ...cliArgs)
1414
}
1515

16+
beforeEach((ctx) => {
17+
const errors: Parameters<typeof console.error>[] = []
18+
const original = console.error
19+
console.error = (...args) => errors.push(args)
20+
21+
ctx.onTestFailed(() => {
22+
errors.forEach(args => original(...args))
23+
})
24+
25+
return () => {
26+
console.error = original
27+
}
28+
})
29+
1630
test('shard cannot be used with watch mode', async () => {
1731
const { stderr } = await runVitest({ watch: true, shard: '1/2' })
1832

@@ -49,16 +63,67 @@ test('inspect-brk cannot be used with multi processing', async () => {
4963
expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"')
5064
})
5165

52-
test('v8 coverage provider cannot be used with browser', async () => {
53-
const { stderr } = await runVitest({ coverage: { enabled: true }, browser: { enabled: true, name: 'chrome' } })
66+
test('v8 coverage provider throws when not playwright + chromium', async () => {
67+
const providers = ['playwright', 'webdriverio', 'preview']
68+
const names = ['edge', 'chromium', 'webkit', 'chrome', 'firefox', 'safari']
69+
70+
for (const provider of providers) {
71+
for (const name of names) {
72+
if (provider === 'playwright' && name === 'chromium') {
73+
continue
74+
}
75+
76+
const { stderr } = await runVitest({
77+
coverage: {
78+
enabled: true,
79+
},
80+
browser: {
81+
enabled: true,
82+
provider,
83+
name,
84+
},
85+
})
86+
87+
expect(stderr).toMatch(
88+
`Error: @vitest/coverage-v8 does not work with
89+
{
90+
"browser": {
91+
"provider": "${provider}",
92+
"name": "${name}"
93+
}
94+
}
95+
96+
Use either:
97+
{
98+
"browser": {
99+
"provider": "playwright",
100+
"name": "chromium"
101+
}
102+
}
54103
55-
expect(stderr).toMatch('Error: @vitest/coverage-v8 does not work with --browser. Use @vitest/coverage-istanbul instead')
104+
...or change your coverage provider to:
105+
{
106+
"coverage": {
107+
"provider": "istanbul"
108+
}
109+
}
110+
`,
111+
)
112+
}
113+
}
56114
})
57115

58-
test('v8 coverage provider cannot be used with browser in workspace', async () => {
116+
test('v8 coverage provider cannot be used in workspace without playwright + chromium', async () => {
59117
const { stderr } = await runVitest({ coverage: { enabled: true }, workspace: './fixtures/workspace/browser/workspace-with-browser.ts' })
60-
61-
expect(stderr).toMatch('Error: @vitest/coverage-v8 does not work with --browser. Use @vitest/coverage-istanbul instead')
118+
expect(stderr).toMatch(
119+
`Error: @vitest/coverage-v8 does not work with
120+
{
121+
"browser": {
122+
"provider": "webdriverio",
123+
"name": "chrome"
124+
}
125+
}`,
126+
)
62127
})
63128

64129
test('coverage reportsDirectory cannot be current working directory', async () => {

‎test/coverage-test/fixtures/src/dynamic-files.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function run() {
1717
function uncovered() {}
1818
`.trim(), 'utf-8')
1919

20-
const { run } = await import(filename)
20+
const { run } = await import(/* @vite-ignore */ filename)
2121

2222
if (run() !== 'Import works') {
2323
throw new Error(`Failed to run ${filename}`)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { expect, test } from 'vitest'
2+
import { multiply } from '../src/math'
3+
import * as ExternalMath from '../../../test-utils/fixtures/external-math'
4+
5+
test('calling files outside project root', () => {
6+
expect(ExternalMath.sum(2, 3)).toBe(5)
7+
})
8+
9+
test('multiply - add some files to report', () => {
10+
expect(multiply(2, 3)).toBe(6)
11+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { expect, test } from 'vitest'
2+
import * as transpiled from '../src/pre-transpiled/transpiled.js'
3+
4+
test('run pre-transpiled sources', () => {
5+
expect(transpiled.hello).toBeTypeOf('function')
6+
expect(transpiled.hello()).toBeUndefined()
7+
})
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,32 @@
11
import { expect } from 'vitest'
2-
import { coverageTest, normalizeURL, readCoverageMap, runVitest, test } from '../utils'
3-
import { multiply } from '../fixtures/src/math'
4-
import * as ExternalMath from '../../test-utils/fixtures/math'
2+
import { readCoverageMap, runVitest, test } from '../utils'
53

64
test('{ allowExternal: true } includes files outside project root', async () => {
75
await runVitest({
8-
include: [normalizeURL(import.meta.url)],
9-
coverage: { allowExternal: true, reporter: 'json', include: ['**/fixtures/**'] },
6+
include: ['fixtures/test/allow-external-fixture.test.ts'],
7+
coverage: { allowExternal: true, reporter: 'json', include: ['**/fixtures/**'], all: false },
108
})
119
const coverageMap = await readCoverageMap()
1210
const files = coverageMap.files()
1311

1412
// File outside project root
15-
expect(files).toContain('<project-root>/test/test-utils/fixtures/math.ts')
13+
expect(files).toContain('<project-root>/test/test-utils/fixtures/external-math.ts')
1614

1715
// Files inside project root should always be included
1816
expect(files).toContain('<process-cwd>/fixtures/src/math.ts')
1917
})
2018

2119
test('{ allowExternal: false } excludes files outside project root', async () => {
2220
await runVitest({
23-
include: [normalizeURL(import.meta.url)],
21+
include: ['fixtures/test/allow-external-fixture.test.ts'],
2422
coverage: { allowExternal: false, reporter: 'json', include: ['**/fixtures/**'] },
2523
})
2624
const coverageMap = await readCoverageMap()
2725
const files = coverageMap.files()
2826

2927
// File outside project root
30-
expect(files.find(file => file.includes('test-utils/fixtures/math.ts'))).toBeFalsy()
28+
expect(files.find(file => file.includes('test-utils/fixtures/external-math.ts'))).toBeFalsy()
3129

3230
// Files inside project root should always be included
3331
expect(files).toContain('<process-cwd>/fixtures/src/math.ts')
3432
})
35-
36-
coverageTest('calling files outside project root', () => {
37-
expect(ExternalMath.sum(2, 3)).toBe(5)
38-
})
39-
40-
coverageTest('multiply - add some files to report', () => {
41-
expect(multiply(2, 3)).toBe(6)
42-
})

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { resolve } from 'node:path'
33
import { afterAll, beforeAll, expect } from 'vitest'
44
import { readCoverageMap, runVitest, test } from '../utils'
55

6+
// Note that this test may fail if you have new files in "vitest/test/coverage/src"
7+
// and have not yet committed those
8+
const SKIP = !!process.env.ECOSYSTEM_CI || !process.env.GITHUB_ACTIONS
9+
610
const FILE_TO_CHANGE = resolve('./fixtures/src/file-to-change.ts')
711
const NEW_UNCOVERED_FILE = resolve('./fixtures/src/new-uncovered-file.ts')
812

@@ -39,8 +43,6 @@ test('{ changed: "HEAD" }', async () => {
3943

4044
const coverageMap = await readCoverageMap()
4145

42-
// Note that this test may fail if you have new files in "vitest/test/coverage/src"
43-
// and have not yet committed those
4446
expect(coverageMap.files()).toMatchInlineSnapshot(`
4547
[
4648
"<process-cwd>/fixtures/src/file-to-change.ts",
@@ -53,4 +55,4 @@ test('{ changed: "HEAD" }', async () => {
5355

5456
const changedFile = coverageMap.fileCoverageFor('<process-cwd>/fixtures/src/file-to-change.ts').toSummary()
5557
expect(changedFile.lines.pct).toBeGreaterThanOrEqual(50)
56-
}, !!process.env.ECOSYSTEM_CI)
58+
}, SKIP)

‎test/coverage-test/test/pre-transpiled-source.test.ts

+2-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import libCoverage from 'istanbul-lib-coverage'
22
import { expect } from 'vitest'
3-
import { coverageTest, isV8Provider, normalizeURL, readCoverageJson, runVitest, test } from '../utils'
4-
import * as transpiled from '../fixtures/src/pre-transpiled/transpiled.js'
3+
import { isV8Provider, readCoverageJson, runVitest, test } from '../utils'
54

65
test('pre-transpiled code with source maps to original (#5341)', async () => {
76
await runVitest({
8-
include: [normalizeURL(import.meta.url)],
7+
include: ['fixtures/test/pre-transpiled-fixture.test.ts'],
98
coverage: {
109
include: ['fixtures/src/**'],
1110
reporter: 'json',
@@ -25,8 +24,3 @@ test('pre-transpiled code with source maps to original (#5341)', async () => {
2524

2625
expect(JSON.stringify(coverageJson, null, 2)).toMatchFileSnapshot(`__snapshots__/pre-transpiled-${isV8Provider() ? 'v8' : 'istanbul'}.snapshot.json`)
2726
})
28-
29-
coverageTest('run pre-transpiled sources', () => {
30-
expect(transpiled.hello).toBeTypeOf('function')
31-
expect(transpiled.hello()).toBeUndefined()
32-
})

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

+42-20
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { resolve } from 'node:path'
22
import { readdirSync } from 'node:fs'
33
import { beforeAll, expect } from 'vitest'
4-
import { isV8Provider, readCoverageMap, runVitest, test } from '../utils'
4+
import { isBrowser, isV8Provider, readCoverageMap, runVitest, test } from '../utils'
55

66
beforeAll(async () => {
77
await runVitest({
@@ -25,40 +25,62 @@ test('coverage results matches snapshot', async () => {
2525
const summary = coverageMap.getCoverageSummary()
2626

2727
if (isV8Provider()) {
28-
expect(summary).toMatchInlineSnapshot(`
28+
const { branches, functions, lines, statements } = summary
29+
30+
expect({ branches, functions }).toMatchInlineSnapshot(`
2931
{
3032
"branches": {
3133
"covered": 5,
3234
"pct": 83.33,
3335
"skipped": 0,
3436
"total": 6,
3537
},
36-
"branchesTrue": {
37-
"covered": 0,
38-
"pct": "Unknown",
39-
"skipped": 0,
40-
"total": 0,
41-
},
4238
"functions": {
4339
"covered": 3,
4440
"pct": 60,
4541
"skipped": 0,
4642
"total": 5,
4743
},
48-
"lines": {
49-
"covered": 36,
50-
"pct": 81.81,
51-
"skipped": 0,
52-
"total": 44,
53-
},
54-
"statements": {
55-
"covered": 36,
56-
"pct": 81.81,
57-
"skipped": 0,
58-
"total": 44,
59-
},
6044
}
6145
`)
46+
47+
// Lines and statements are not 100% identical between node and browser - not sure if it's Vue, Vite or Vitest issue
48+
if (isBrowser()) {
49+
expect({ lines, statements }).toMatchInlineSnapshot(`
50+
{
51+
"lines": {
52+
"covered": 40,
53+
"pct": 83.33,
54+
"skipped": 0,
55+
"total": 48,
56+
},
57+
"statements": {
58+
"covered": 40,
59+
"pct": 83.33,
60+
"skipped": 0,
61+
"total": 48,
62+
},
63+
}
64+
`)
65+
}
66+
else {
67+
expect({ lines, statements }).toMatchInlineSnapshot(`
68+
{
69+
"lines": {
70+
"covered": 36,
71+
"pct": 81.81,
72+
"skipped": 0,
73+
"total": 44,
74+
},
75+
"statements": {
76+
"covered": 36,
77+
"pct": 81.81,
78+
"skipped": 0,
79+
"total": 44,
80+
},
81+
}
82+
`)
83+
}
6284
}
6385
else {
6486
expect(summary).toMatchInlineSnapshot(`

‎test/coverage-test/utils.ts

+4
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ export function isV8Provider() {
9696
return process.env.COVERAGE_PROVIDER === 'v8'
9797
}
9898

99+
export function isBrowser() {
100+
return process.env.COVERAGE_BROWSER === 'true'
101+
}
102+
99103
export function normalizeURL(importMetaURL: string) {
100104
return normalize(fileURLToPath(importMetaURL))
101105
}

‎test/coverage-test/vitest.workspace.custom.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,39 @@ export default defineWorkspace([
6060
{
6161
test: {
6262
...config.test,
63-
name: 'browser',
63+
name: 'istanbul-browser',
6464
env: { COVERAGE_PROVIDER: 'istanbul', COVERAGE_BROWSER: 'true' },
6565
include: [
6666
BROWSER_TESTS,
6767

6868
// Other non-provider-specific tests that should be run on browser mode as well
69+
'**/allow-external.test.ts',
6970
'**/ignore-hints.test.ts',
7071
'**/import-attributes.test.ts',
72+
'**/pre-transpiled-source.test.ts',
73+
'**/multi-suite.test.ts',
74+
'**/setup-files.test.ts',
75+
'**/results-snapshot.test.ts',
76+
'**/reporters.test.ts',
77+
'**/temporary-files.test.ts',
78+
'**/test-reporter-conflicts.test.ts',
79+
'**/vue.test.ts',
80+
],
81+
},
82+
},
83+
{
84+
test: {
85+
...config.test,
86+
name: 'v8-browser',
87+
env: { COVERAGE_PROVIDER: 'v8', COVERAGE_BROWSER: 'true' },
88+
include: [
89+
BROWSER_TESTS,
90+
91+
// Other non-provider-specific tests that should be run on browser mode as well
92+
'**/allow-external.test.ts',
93+
'**/ignore-hints.test.ts',
94+
'**/import-attributes.test.ts',
95+
'**/pre-transpiled-source.test.ts',
7196
'**/multi-suite.test.ts',
7297
'**/setup-files.test.ts',
7398
'**/results-snapshot.test.ts',
File renamed without changes.

0 commit comments

Comments
 (0)
Please sign in to comment.