Skip to content

Commit 34199bd

Browse files
authored
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+
}

0 commit comments

Comments
 (0)