[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.

feat(browser): support v8 coverage (#6273)

authored by

Ari Perkkiö and committed by
GitHub
(Aug 12, 2024, 4:05 PM +0200) 34199bdf 198a3e6e

+428 -202
+6 -13
packages/browser/src/client/tester/runner.ts
··· 6 6 import { TraceMap, originalPositionFor } from 'vitest/utils' 7 7 import { page } from '@vitest/browser/context' 8 8 import { globalChannel } from '@vitest/browser/client' 9 - import { importFs, importId } from '../utils' 9 + import { executor } from '../utils' 10 10 import { VitestBrowserSnapshotEnvironment } from './snapshot' 11 11 import { rpc } from './rpc' 12 12 import type { VitestBrowserClientMocker } from './mocker' ··· 91 91 if (coverage) { 92 92 await rpc().onAfterSuiteRun({ 93 93 coverage, 94 - transformMode: 'web', 94 + transformMode: 'browser', 95 95 projectName: this.config.name, 96 96 }) 97 97 } ··· 148 148 const runnerClass 149 149 = config.mode === 'test' ? VitestTestRunner : NodeBenchmarkRunner 150 150 151 - const executeId = (id: string) => { 152 - if (id[0] === '/' || id[1] === ':') { 153 - return importFs(id) 154 - } 155 - return importId(id) 156 - } 157 - 158 151 const BrowserRunner = createBrowserRunner(runnerClass, mocker, state, { 159 152 takeCoverage: () => 160 - takeCoverageInsideWorker(config.coverage, { executeId }), 153 + takeCoverageInsideWorker(config.coverage, executor), 161 154 }) 162 155 if (!config.snapshotOptions.snapshotEnvironment) { 163 156 config.snapshotOptions.snapshotEnvironment = new VitestBrowserSnapshotEnvironment() ··· 165 158 const runner = new BrowserRunner({ 166 159 config, 167 160 }) 168 - const executor = { executeId } as VitestExecutor 161 + 169 162 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), 172 165 ]) 173 166 runner.config.diffOptions = diffOptions 174 167 cachedRunner = runner
+6 -2
packages/browser/src/client/tester/tester.ts
··· 1 - import { SpyModule, collectTests, setupCommonEnv, startTests } from 'vitest/browser' 1 + import { SpyModule, collectTests, setupCommonEnv, startCoverageInsideWorker, startTests, stopCoverageInsideWorker } from 'vitest/browser' 2 2 import { page } from '@vitest/browser/context' 3 3 import { channel, client, onCancel } from '@vitest/browser/client' 4 - import { getBrowserState, getConfig, getWorkerState } from '../utils' 4 + import { executor, getBrowserState, getConfig, getWorkerState } from '../utils' 5 5 import { setupDialogsSpy } from './dialog' 6 6 import { setupConsoleLogSpy } from './logger' 7 7 import { createSafeRpc } from './rpc' ··· 114 114 115 115 try { 116 116 await setupCommonEnv(config) 117 + await startCoverageInsideWorker(config.coverage, executor) 118 + 117 119 for (const file of files) { 118 120 state.filepath = file 119 121 ··· 139 141 }, 'Cleanup Error') 140 142 } 141 143 state.environmentTeardownRun = true 144 + await stopCoverageInsideWorker(config.coverage, executor) 145 + 142 146 debug('finished running tests') 143 147 done(files) 144 148 }
+11
packages/browser/src/client/utils.ts
··· 10 10 return getBrowserState().wrapModule(() => import(/* @vite-ignore */ name)) 11 11 } 12 12 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 + 13 24 export function getConfig(): SerializedConfig { 14 25 return getBrowserState().config 15 26 }
+3 -2
packages/coverage-istanbul/src/provider.ts
··· 195 195 return 196 196 } 197 197 198 - if (transformMode !== 'web' && transformMode !== 'ssr') { 198 + if (transformMode !== 'web' && transformMode !== 'ssr' && transformMode !== 'browser') { 199 199 throw new Error(`Invalid transform mode: ${transformMode}`) 200 200 } 201 201 202 202 let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT) 203 203 204 204 if (!entry) { 205 - entry = { web: [], ssr: [] } 205 + entry = { web: [], ssr: [], browser: [] } 206 206 this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry) 207 207 } 208 208 ··· 251 251 for (const filenames of [ 252 252 coveragePerProject.ssr, 253 253 coveragePerProject.web, 254 + coveragePerProject.browser, 254 255 ]) { 255 256 const coverageMapByTransformMode = libCoverage.createCoverageMap({}) 256 257
+11
packages/coverage-v8/package.json
··· 28 28 "types": "./dist/index.d.ts", 29 29 "default": "./dist/index.js" 30 30 }, 31 + "./browser": { 32 + "types": "./dist/browser.d.ts", 33 + "default": "./dist/browser.js" 34 + }, 31 35 "./*": "./*" 32 36 }, 33 37 "main": "./dist/index.js", ··· 41 45 "dev": "rollup -c --watch --watch.include 'src/**'" 42 46 }, 43 47 "peerDependencies": { 48 + "@vitest/browser": "workspace:*", 44 49 "vitest": "workspace:*" 45 50 }, 51 + "peerDependenciesMeta": { 52 + "@vitest/browser": { 53 + "optional": true 54 + } 55 + }, 46 56 "dependencies": { 47 57 "@ampproject/remapping": "^2.3.0", 48 58 "@bcoe/v8-coverage": "^0.2.3", ··· 63 73 "@types/istanbul-lib-report": "^3.0.3", 64 74 "@types/istanbul-lib-source-maps": "^4.0.4", 65 75 "@types/istanbul-reports": "^3.0.4", 76 + "@vitest/browser": "workspace:*", 66 77 "pathe": "^1.1.2", 67 78 "v8-to-istanbul": "^9.3.0", 68 79 "vite-node": "workspace:*",
+1
packages/coverage-v8/rollup.config.js
··· 11 11 12 12 const entries = { 13 13 index: 'src/index.ts', 14 + browser: 'src/browser.ts', 14 15 provider: 'src/provider.ts', 15 16 } 16 17
+63
packages/coverage-v8/src/browser.ts
··· 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 + }
+46 -11
packages/coverage-v8/src/index.ts
··· 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' 3 3 import type { V8CoverageProvider } from './provider' 4 + import { loadProvider } from './load-provider' 5 + 6 + const session = new inspector.Session() 4 7 5 8 export default { 6 9 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 + }) 8 16 }, 17 + 9 18 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 + }) 11 35 }, 36 + 12 37 stopCoverage(): void { 13 - return coverage.stopCoverage() 38 + session.post('Profiler.stopPreciseCoverage') 39 + session.post('Profiler.disable') 40 + session.disconnect() 14 41 }, 42 + 15 43 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() 22 45 }, 23 46 } 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 + }
+8
packages/coverage-v8/src/load-provider.ts
··· 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 + }
+71 -50
packages/coverage-v8/src/provider.ts
··· 22 22 import createDebug from 'debug' 23 23 import { cleanUrl } from 'vite-node/utils' 24 24 import type { EncodedSourceMap, FetchResult } from 'vite-node' 25 - import { 26 - coverageConfigDefaults, 27 - } from 'vitest/config' 25 + import { coverageConfigDefaults } from 'vitest/config' 28 26 import { BaseCoverageProvider } from 'vitest/coverage' 27 + import type { Vitest, WorkspaceProject } from 'vitest/node' 29 28 import type { 30 29 AfterSuiteRunMeta, 31 30 CoverageProvider, ··· 33 32 ReportContext, 34 33 ResolvedCoverageOptions, 35 34 } from 'vitest' 36 - import type { Vitest } from 'vitest/node' 37 35 // @ts-expect-error missing types 38 36 import _TestExclude from 'test-exclude' 39 37 ··· 64 62 type ProjectName = 65 63 | NonNullable<AfterSuiteRunMeta['projectName']> 66 64 | typeof DEFAULT_PROJECT 65 + 66 + type Entries<T> = [keyof T, T[keyof T]][] 67 67 68 68 // TODO: vite-node should export this 69 69 const WRAPPER_LENGTH = 185 ··· 74 74 const DECORATOR_METADATA_PATTERN 75 75 = /_ts_metadata\("design:paramtypes", \[[^\]]*\]\),*/g 76 76 const DEFAULT_PROJECT: unique symbol = Symbol.for('default-project') 77 + const FILE_PROTOCOL = 'file://' 77 78 78 79 const debug = createDebug('vitest:coverage') 79 80 let uniqueId = 0 ··· 124 125 lines: config.thresholds['100'] ? 100 : config.thresholds.lines, 125 126 branches: config.thresholds['100'] ? 100 : config.thresholds.branches, 126 127 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, 130 129 }, 131 130 } 132 131 ··· 183 182 * backwards compatibility is a breaking change. 184 183 */ 185 184 onAfterSuiteRun({ coverage, transformMode, projectName }: AfterSuiteRunMeta): void { 186 - if (transformMode !== 'web' && transformMode !== 'ssr') { 185 + if (transformMode !== 'web' && transformMode !== 'ssr' && transformMode !== 'browser') { 187 186 throw new Error(`Invalid transform mode: ${transformMode}`) 188 187 } 189 188 190 189 let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT) 191 190 192 191 if (!entry) { 193 - entry = { web: [], ssr: [] } 192 + entry = { web: [], ssr: [], browser: [] } 194 193 this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry) 195 194 } 196 195 ··· 212 211 await Promise.all(this.pendingPromises) 213 212 this.pendingPromises = [] 214 213 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>) { 222 216 let merged: RawCoverage = { result: [] } 223 217 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)) { 228 221 if (debug.enabled) { 229 222 index += chunk.length 230 223 debug('Covered files %d/%d', index, total) 231 224 } 232 225 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 + }), 239 232 ) 240 233 } 241 234 242 235 const converted = await this.convertCoverage( 243 236 merged, 244 - projectName, 237 + project, 245 238 transformMode, 246 239 ) 247 240 ··· 404 397 const { originalSource } = await this.getSources( 405 398 filename.href, 406 399 transformResults, 400 + file => this.ctx.vitenode.transformRequest(file), 407 401 ) 408 402 409 403 const coverage = { ··· 441 435 return merged 442 436 } 443 437 444 - private async getSources( 438 + private async getSources<TransformResult extends (FetchResult | Awaited<ReturnType<typeof this.ctx.vitenode.transformRequest>>)>( 445 439 url: string, 446 440 transformResults: TransformResults, 441 + onTransform: (filepath: string) => Promise<TransformResult>, 447 442 functions: Profiler.FunctionCoverage[] = [], 448 443 ): Promise<{ 449 444 source: string ··· 454 449 const filePath = normalize(fileURLToPath(url)) 455 450 456 451 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) 461 453 462 454 if (!transformResult) { 463 455 isExecuted = false 464 - transformResult = await this.ctx.vitenode 465 - .transformRequest(filePath) 466 - .catch(() => null) 456 + transformResult = await onTransform(removeStartsWith(url, FILE_PROTOCOL)).catch(() => undefined) 467 457 } 468 458 469 459 const map = transformResult?.map as EncodedSourceMap | undefined ··· 513 503 514 504 private async convertCoverage( 515 505 coverage: RawCoverage, 516 - projectName?: ProjectName, 517 - transformMode?: 'web' | 'ssr', 506 + project: WorkspaceProject = this.ctx.getCoreWorkspaceProject(), 507 + transformMode?: keyof CoverageFilesByTransformMode, 518 508 ): 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 + 525 515 const transformResults = normalizeTransformResults(fetchCache) 526 516 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 + 530 545 const coverageMap = libCoverage.createCoverageMap({}) 531 546 let index = 0 532 547 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)) { 537 549 if (debug.enabled) { 538 550 index += chunk.length 539 551 debug('Converting %d/%d', index, scriptCoverages.length) ··· 544 556 const sources = await this.getSources( 545 557 url, 546 558 transformResults, 559 + onTransform, 547 560 functions, 548 561 ) 549 562 ··· 645 658 646 659 return normalized 647 660 } 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 + }
-55
packages/coverage-v8/src/takeCoverage.ts
··· 1 - /* 2 - * For details about the Profiler.* messages see https://chromedevtools.github.io/devtools-protocol/v8/Profiler/ 3 - */ 4 - 5 - import inspector from 'node:inspector' 6 - import type { Profiler } from 'node:inspector' 7 - import { provider } from 'std-env' 8 - 9 - const session = new inspector.Session() 10 - 11 - export function startCoverage(): void { 12 - session.connect() 13 - session.post('Profiler.enable') 14 - session.post('Profiler.startPreciseCoverage', { 15 - callCount: true, 16 - detailed: true, 17 - }) 18 - } 19 - 20 - export async function takeCoverage(): Promise<{ result: Profiler.ScriptCoverage[] }> { 21 - return new Promise((resolve, reject) => { 22 - session.post('Profiler.takePreciseCoverage', async (error, coverage) => { 23 - if (error) { 24 - return reject(error) 25 - } 26 - 27 - // Reduce amount of data sent over rpc by doing some early result filtering 28 - const result = coverage.result.filter(filterResult) 29 - 30 - resolve({ result }) 31 - }) 32 - 33 - if (provider === 'stackblitz') { 34 - resolve({ result: [] }) 35 - } 36 - }) 37 - } 38 - 39 - export function stopCoverage(): void { 40 - session.post('Profiler.stopPreciseCoverage') 41 - session.post('Profiler.disable') 42 - session.disconnect() 43 - } 44 - 45 - function filterResult(coverage: Profiler.ScriptCoverage): boolean { 46 - if (!coverage.url.startsWith('file://')) { 47 - return false 48 - } 49 - 50 - if (coverage.url.includes('/node_modules/')) { 51 - return false 52 - } 53 - 54 - return true 55 - }
+2 -1
packages/coverage-v8/tsconfig.json
··· 1 1 { 2 2 "extends": "../../tsconfig.base.json", 3 3 "compilerOptions": { 4 - "moduleResolution": "Bundler" 4 + "moduleResolution": "Bundler", 5 + "types": ["@vitest/browser/providers/playwright"] 5 6 }, 6 7 "include": ["./src/**/*.ts"], 7 8 "exclude": ["./dist"]
+8 -3
packages/vitest/src/integrations/coverage.ts
··· 6 6 7 7 interface Loader { 8 8 executeId: (id: string) => Promise<{ default: CoverageProviderModule }> 9 + isBrowser?: boolean 9 10 } 10 11 11 12 export const CoverageProviderMap: Record<string, string> = { ··· 24 25 const provider = options.provider 25 26 26 27 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) 30 35 31 36 if (!coverageModule) { 32 37 throw new Error(
+11 -8
packages/vitest/src/node/config/resolveConfig.ts
··· 222 222 } 223 223 } 224 224 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 + } 233 236 } 234 237 235 238 resolved.coverage.reporter = resolveCoverageReporters(resolved.coverage.reporter)
+1 -1
packages/vitest/src/types/general.ts
··· 24 24 25 25 export interface AfterSuiteRunMeta { 26 26 coverage?: unknown 27 - transformMode: TransformMode 27 + transformMode: TransformMode | 'browser' 28 28 projectName?: string 29 29 } 30 30
+3
pnpm-lock.yaml
··· 610 610 '@types/istanbul-reports': 611 611 specifier: ^3.0.4 612 612 version: 3.0.4 613 + '@vitest/browser': 614 + specifier: workspace:* 615 + version: link:../browser 613 616 pathe: 614 617 specifier: ^1.1.2 615 618 version: 1.1.2
+1
test/config/fixtures/workspace/browser/workspace-with-browser.ts
··· 6 6 name: "Browser project", 7 7 browser: { 8 8 enabled: true, 9 + provider: 'webdriverio', 9 10 name: 'chrome' 10 11 }, 11 12 }
+72 -7
test/config/test/failures.test.ts
··· 1 - import { expect, test } from 'vitest' 1 + import { beforeEach, expect, test } from 'vitest' 2 2 import type { UserConfig } from 'vitest' 3 3 import { version } from 'vitest/package.json' 4 4 ··· 12 12 function runVitestCli(...cliArgs: string[]) { 13 13 return testUtils.runVitestCli('run', 'fixtures/test/', ...cliArgs) 14 14 } 15 + 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 + }) 15 29 16 30 test('shard cannot be used with watch mode', async () => { 17 31 const { stderr } = await runVitest({ watch: true, shard: '1/2' }) ··· 49 63 expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"') 50 64 }) 51 65 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 + } 54 95 55 - expect(stderr).toMatch('Error: @vitest/coverage-v8 does not work with --browser. Use @vitest/coverage-istanbul instead') 96 + Use either: 97 + { 98 + "browser": { 99 + "provider": "playwright", 100 + "name": "chromium" 101 + } 102 + } 103 + 104 + ...or change your coverage provider to: 105 + { 106 + "coverage": { 107 + "provider": "istanbul" 108 + } 109 + } 110 + `, 111 + ) 112 + } 113 + } 56 114 }) 57 115 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 () => { 59 117 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 + ) 62 127 }) 63 128 64 129 test('coverage reportsDirectory cannot be current working directory', async () => {
+1 -1
test/coverage-test/fixtures/src/dynamic-files.ts
··· 17 17 function uncovered() {} 18 18 `.trim(), 'utf-8') 19 19 20 - const { run } = await import(filename) 20 + const { run } = await import(/* @vite-ignore */ filename) 21 21 22 22 if (run() !== 'Import works') { 23 23 throw new Error(`Failed to run ${filename}`)
+11
test/coverage-test/fixtures/test/allow-external-fixture.test.ts
··· 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 + })
+7
test/coverage-test/fixtures/test/pre-transpiled-fixture.test.ts
··· 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 + })
+6 -16
test/coverage-test/test/allow-external.test.ts
··· 1 1 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' 5 3 6 4 test('{ allowExternal: true } includes files outside project root', async () => { 7 5 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 }, 10 8 }) 11 9 const coverageMap = await readCoverageMap() 12 10 const files = coverageMap.files() 13 11 14 12 // 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') 16 14 17 15 // Files inside project root should always be included 18 16 expect(files).toContain('<process-cwd>/fixtures/src/math.ts') ··· 20 18 21 19 test('{ allowExternal: false } excludes files outside project root', async () => { 22 20 await runVitest({ 23 - include: [normalizeURL(import.meta.url)], 21 + include: ['fixtures/test/allow-external-fixture.test.ts'], 24 22 coverage: { allowExternal: false, reporter: 'json', include: ['**/fixtures/**'] }, 25 23 }) 26 24 const coverageMap = await readCoverageMap() 27 25 const files = coverageMap.files() 28 26 29 27 // 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() 31 29 32 30 // Files inside project root should always be included 33 31 expect(files).toContain('<process-cwd>/fixtures/src/math.ts') 34 32 }) 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 - })
+5 -3
test/coverage-test/test/changed.test.ts
··· 3 3 import { afterAll, beforeAll, expect } from 'vitest' 4 4 import { readCoverageMap, runVitest, test } from '../utils' 5 5 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 + 6 10 const FILE_TO_CHANGE = resolve('./fixtures/src/file-to-change.ts') 7 11 const NEW_UNCOVERED_FILE = resolve('./fixtures/src/new-uncovered-file.ts') 8 12 ··· 39 43 40 44 const coverageMap = await readCoverageMap() 41 45 42 - // Note that this test may fail if you have new files in "vitest/test/coverage/src" 43 - // and have not yet committed those 44 46 expect(coverageMap.files()).toMatchInlineSnapshot(` 45 47 [ 46 48 "<process-cwd>/fixtures/src/file-to-change.ts", ··· 53 55 54 56 const changedFile = coverageMap.fileCoverageFor('<process-cwd>/fixtures/src/file-to-change.ts').toSummary() 55 57 expect(changedFile.lines.pct).toBeGreaterThanOrEqual(50) 56 - }, !!process.env.ECOSYSTEM_CI) 58 + }, SKIP)
+2 -8
test/coverage-test/test/pre-transpiled-source.test.ts
··· 1 1 import libCoverage from 'istanbul-lib-coverage' 2 2 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' 5 4 6 5 test('pre-transpiled code with source maps to original (#5341)', async () => { 7 6 await runVitest({ 8 - include: [normalizeURL(import.meta.url)], 7 + include: ['fixtures/test/pre-transpiled-fixture.test.ts'], 9 8 coverage: { 10 9 include: ['fixtures/src/**'], 11 10 reporter: 'json', ··· 25 24 26 25 expect(JSON.stringify(coverageJson, null, 2)).toMatchFileSnapshot(`__snapshots__/pre-transpiled-${isV8Provider() ? 'v8' : 'istanbul'}.snapshot.json`) 27 26 }) 28 - 29 - coverageTest('run pre-transpiled sources', () => { 30 - expect(transpiled.hello).toBeTypeOf('function') 31 - expect(transpiled.hello()).toBeUndefined() 32 - })
+42 -20
test/coverage-test/test/vue.test.ts
··· 1 1 import { resolve } from 'node:path' 2 2 import { readdirSync } from 'node:fs' 3 3 import { beforeAll, expect } from 'vitest' 4 - import { isV8Provider, readCoverageMap, runVitest, test } from '../utils' 4 + import { isBrowser, isV8Provider, readCoverageMap, runVitest, test } from '../utils' 5 5 6 6 beforeAll(async () => { 7 7 await runVitest({ ··· 25 25 const summary = coverageMap.getCoverageSummary() 26 26 27 27 if (isV8Provider()) { 28 - expect(summary).toMatchInlineSnapshot(` 28 + const { branches, functions, lines, statements } = summary 29 + 30 + expect({ branches, functions }).toMatchInlineSnapshot(` 29 31 { 30 32 "branches": { 31 33 "covered": 5, ··· 33 35 "skipped": 0, 34 36 "total": 6, 35 37 }, 36 - "branchesTrue": { 37 - "covered": 0, 38 - "pct": "Unknown", 39 - "skipped": 0, 40 - "total": 0, 41 - }, 42 38 "functions": { 43 39 "covered": 3, 44 40 "pct": 60, 45 41 "skipped": 0, 46 42 "total": 5, 47 43 }, 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 - }, 60 44 } 61 45 `) 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 + } 62 84 } 63 85 else { 64 86 expect(summary).toMatchInlineSnapshot(`
+4
test/coverage-test/utils.ts
··· 96 96 return process.env.COVERAGE_PROVIDER === 'v8' 97 97 } 98 98 99 + export function isBrowser() { 100 + return process.env.COVERAGE_BROWSER === 'true' 101 + } 102 + 99 103 export function normalizeURL(importMetaURL: string) { 100 104 return normalize(fileURLToPath(importMetaURL)) 101 105 }
+26 -1
test/coverage-test/vitest.workspace.custom.ts
··· 60 60 { 61 61 test: { 62 62 ...config.test, 63 - name: 'browser', 63 + name: 'istanbul-browser', 64 64 env: { COVERAGE_PROVIDER: 'istanbul', COVERAGE_BROWSER: 'true' }, 65 65 include: [ 66 66 BROWSER_TESTS, 67 67 68 68 // Other non-provider-specific tests that should be run on browser mode as well 69 + '**/allow-external.test.ts', 69 70 '**/ignore-hints.test.ts', 70 71 '**/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', 71 96 '**/multi-suite.test.ts', 72 97 '**/setup-files.test.ts', 73 98 '**/results-snapshot.test.ts',
test/test-utils/fixtures/math.ts test/test-utils/fixtures/external-math.ts