diff --git a/packages/browser/src/client/tester/runner.ts b/packages/browser/src/client/tester/runner.ts index 90eab69ce85a..5a2012fac025 100644 --- a/packages/browser/src/client/tester/runner.ts +++ b/packages/browser/src/client/tester/runner.ts @@ -6,7 +6,7 @@ import { loadDiffConfig, loadSnapshotSerializers, takeCoverageInsideWorker } fro import { TraceMap, originalPositionFor } from 'vitest/utils' import { page } from '@vitest/browser/context' import { globalChannel } from '@vitest/browser/client' -import { importFs, importId } from '../utils' +import { executor } from '../utils' import { VitestBrowserSnapshotEnvironment } from './snapshot' import { rpc } from './rpc' import type { VitestBrowserClientMocker } from './mocker' @@ -91,7 +91,7 @@ export function createBrowserRunner( if (coverage) { await rpc().onAfterSuiteRun({ coverage, - transformMode: 'web', + transformMode: 'browser', projectName: this.config.name, }) } @@ -148,16 +148,9 @@ export async function initiateRunner( const runnerClass = config.mode === 'test' ? VitestTestRunner : NodeBenchmarkRunner - const executeId = (id: string) => { - if (id[0] === '/' || id[1] === ':') { - return importFs(id) - } - return importId(id) - } - const BrowserRunner = createBrowserRunner(runnerClass, mocker, state, { takeCoverage: () => - takeCoverageInsideWorker(config.coverage, { executeId }), + takeCoverageInsideWorker(config.coverage, executor), }) if (!config.snapshotOptions.snapshotEnvironment) { config.snapshotOptions.snapshotEnvironment = new VitestBrowserSnapshotEnvironment() @@ -165,10 +158,10 @@ export async function initiateRunner( const runner = new BrowserRunner({ config, }) - const executor = { executeId } as VitestExecutor + const [diffOptions] = await Promise.all([ - loadDiffConfig(config, executor), - loadSnapshotSerializers(config, executor), + loadDiffConfig(config, executor as unknown as VitestExecutor), + loadSnapshotSerializers(config, executor as unknown as VitestExecutor), ]) runner.config.diffOptions = diffOptions cachedRunner = runner diff --git a/packages/browser/src/client/tester/tester.ts b/packages/browser/src/client/tester/tester.ts index 631111c0fc9e..abc46387ef6d 100644 --- a/packages/browser/src/client/tester/tester.ts +++ b/packages/browser/src/client/tester/tester.ts @@ -1,7 +1,7 @@ -import { SpyModule, collectTests, setupCommonEnv, startTests } from 'vitest/browser' +import { SpyModule, collectTests, setupCommonEnv, startCoverageInsideWorker, startTests, stopCoverageInsideWorker } from 'vitest/browser' import { page } from '@vitest/browser/context' import { channel, client, onCancel } from '@vitest/browser/client' -import { getBrowserState, getConfig, getWorkerState } from '../utils' +import { executor, getBrowserState, getConfig, getWorkerState } from '../utils' import { setupDialogsSpy } from './dialog' import { setupConsoleLogSpy } from './logger' import { createSafeRpc } from './rpc' @@ -114,6 +114,8 @@ async function executeTests(method: 'run' | 'collect', files: string[]) { try { await setupCommonEnv(config) + await startCoverageInsideWorker(config.coverage, executor) + for (const file of files) { state.filepath = file @@ -139,6 +141,8 @@ async function executeTests(method: 'run' | 'collect', files: string[]) { }, 'Cleanup Error') } state.environmentTeardownRun = true + await stopCoverageInsideWorker(config.coverage, executor) + debug('finished running tests') done(files) } diff --git a/packages/browser/src/client/utils.ts b/packages/browser/src/client/utils.ts index 2a328813fabe..168704878cb3 100644 --- a/packages/browser/src/client/utils.ts +++ b/packages/browser/src/client/utils.ts @@ -10,6 +10,17 @@ export async function importFs(id: string) { return getBrowserState().wrapModule(() => import(/* @vite-ignore */ name)) } +export const executor = { + isBrowser: true, + + executeId: (id: string) => { + if (id[0] === '/' || id[1] === ':') { + return importFs(id) + } + return importId(id) + }, +} + export function getConfig(): SerializedConfig { return getBrowserState().config } diff --git a/packages/coverage-istanbul/src/provider.ts b/packages/coverage-istanbul/src/provider.ts index 19fd5dc7a47d..98ed3df75bb1 100644 --- a/packages/coverage-istanbul/src/provider.ts +++ b/packages/coverage-istanbul/src/provider.ts @@ -195,14 +195,14 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co return } - if (transformMode !== 'web' && transformMode !== 'ssr') { + if (transformMode !== 'web' && transformMode !== 'ssr' && transformMode !== 'browser') { throw new Error(`Invalid transform mode: ${transformMode}`) } let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT) if (!entry) { - entry = { web: [], ssr: [] } + entry = { web: [], ssr: [], browser: [] } this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry) } @@ -251,6 +251,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co for (const filenames of [ coveragePerProject.ssr, coveragePerProject.web, + coveragePerProject.browser, ]) { const coverageMapByTransformMode = libCoverage.createCoverageMap({}) diff --git a/packages/coverage-v8/package.json b/packages/coverage-v8/package.json index a3ae9f0a5fbb..df2d8b9f65f6 100644 --- a/packages/coverage-v8/package.json +++ b/packages/coverage-v8/package.json @@ -28,6 +28,10 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./browser": { + "types": "./dist/browser.d.ts", + "default": "./dist/browser.js" + }, "./*": "./*" }, "main": "./dist/index.js", @@ -41,8 +45,14 @@ "dev": "rollup -c --watch --watch.include 'src/**'" }, "peerDependencies": { + "@vitest/browser": "workspace:*", "vitest": "workspace:*" }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + }, "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", @@ -63,6 +73,7 @@ "@types/istanbul-lib-report": "^3.0.3", "@types/istanbul-lib-source-maps": "^4.0.4", "@types/istanbul-reports": "^3.0.4", + "@vitest/browser": "workspace:*", "pathe": "^1.1.2", "v8-to-istanbul": "^9.3.0", "vite-node": "workspace:*", diff --git a/packages/coverage-v8/rollup.config.js b/packages/coverage-v8/rollup.config.js index 577e9d2f61e1..5ed7d273a5f8 100644 --- a/packages/coverage-v8/rollup.config.js +++ b/packages/coverage-v8/rollup.config.js @@ -11,6 +11,7 @@ const pkg = require('./package.json') const entries = { index: 'src/index.ts', + browser: 'src/browser.ts', provider: 'src/provider.ts', } diff --git a/packages/coverage-v8/src/browser.ts b/packages/coverage-v8/src/browser.ts new file mode 100644 index 000000000000..068a009653bf --- /dev/null +++ b/packages/coverage-v8/src/browser.ts @@ -0,0 +1,63 @@ +import { cdp } from '@vitest/browser/context' +import type { V8CoverageProvider } from './provider' +import { loadProvider } from './load-provider' + +const session = cdp() + +type ScriptCoverage = Awaited>> + +export default { + async startCoverage() { + await session.send('Profiler.enable') + await session.send('Profiler.startPreciseCoverage', { + callCount: true, + detailed: true, + }) + }, + + async takeCoverage(): Promise<{ result: any[] }> { + const coverage = await session.send('Profiler.takePreciseCoverage') + const result: typeof coverage.result = [] + + // Reduce amount of data sent over rpc by doing some early result filtering + for (const entry of coverage.result) { + if (filterResult(entry)) { + result.push({ + ...entry, + url: decodeURIComponent(entry.url.replace(window.location.origin, '')), + }) + } + } + + return { result } + }, + + async stopCoverage() { + await session.send('Profiler.stopPreciseCoverage') + await session.send('Profiler.disable') + }, + + async getProvider(): Promise { + return loadProvider() + }, +} + +function filterResult(coverage: ScriptCoverage['result'][number]): boolean { + if (!coverage.url.startsWith(window.location.origin)) { + return false + } + + if (coverage.url.includes('/node_modules/')) { + return false + } + + if (coverage.url.includes('__vitest_browser__')) { + return false + } + + if (coverage.url.includes('__vitest__/assets')) { + return false + } + + return true +} diff --git a/packages/coverage-v8/src/index.ts b/packages/coverage-v8/src/index.ts index fb03b3b8b387..a38625fb7038 100644 --- a/packages/coverage-v8/src/index.ts +++ b/packages/coverage-v8/src/index.ts @@ -1,23 +1,58 @@ -import type { Profiler } from 'node:inspector' -import * as coverage from './takeCoverage' +import inspector, { type Profiler } from 'node:inspector' +import { provider } from 'std-env' import type { V8CoverageProvider } from './provider' +import { loadProvider } from './load-provider' + +const session = new inspector.Session() export default { startCoverage(): void { - return coverage.startCoverage() + session.connect() + session.post('Profiler.enable') + session.post('Profiler.startPreciseCoverage', { + callCount: true, + detailed: true, + }) }, + takeCoverage(): Promise<{ result: Profiler.ScriptCoverage[] }> { - return coverage.takeCoverage() + return new Promise((resolve, reject) => { + session.post('Profiler.takePreciseCoverage', async (error, coverage) => { + if (error) { + return reject(error) + } + + // Reduce amount of data sent over rpc by doing some early result filtering + const result = coverage.result.filter(filterResult) + + resolve({ result }) + }) + + if (provider === 'stackblitz') { + resolve({ result: [] }) + } + }) }, + stopCoverage(): void { - return coverage.stopCoverage() + session.post('Profiler.stopPreciseCoverage') + session.post('Profiler.disable') + session.disconnect() }, + async getProvider(): Promise { - // to not bundle the provider - const name = './provider.js' - const { V8CoverageProvider } = (await import( - name - )) as typeof import('./provider') - return new V8CoverageProvider() + return loadProvider() }, } + +function filterResult(coverage: Profiler.ScriptCoverage): boolean { + if (!coverage.url.startsWith('file://')) { + return false + } + + if (coverage.url.includes('/node_modules/')) { + return false + } + + return true +} diff --git a/packages/coverage-v8/src/load-provider.ts b/packages/coverage-v8/src/load-provider.ts new file mode 100644 index 000000000000..d2c8d9efdece --- /dev/null +++ b/packages/coverage-v8/src/load-provider.ts @@ -0,0 +1,8 @@ +// to not bundle the provider +const name = './provider.js' + +export async function loadProvider() { + const { V8CoverageProvider } = (await import(/* @vite-ignore */ name)) as typeof import('./provider') + + return new V8CoverageProvider() +} diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts index 982da3052e9d..532b9d437e08 100644 --- a/packages/coverage-v8/src/provider.ts +++ b/packages/coverage-v8/src/provider.ts @@ -22,10 +22,9 @@ import { provider } from 'std-env' import createDebug from 'debug' import { cleanUrl } from 'vite-node/utils' import type { EncodedSourceMap, FetchResult } from 'vite-node' -import { - coverageConfigDefaults, -} from 'vitest/config' +import { coverageConfigDefaults } from 'vitest/config' import { BaseCoverageProvider } from 'vitest/coverage' +import type { Vitest, WorkspaceProject } from 'vitest/node' import type { AfterSuiteRunMeta, CoverageProvider, @@ -33,7 +32,6 @@ import type { ReportContext, ResolvedCoverageOptions, } from 'vitest' -import type { Vitest } from 'vitest/node' // @ts-expect-error missing types import _TestExclude from 'test-exclude' @@ -65,6 +63,8 @@ type ProjectName = | NonNullable | typeof DEFAULT_PROJECT +type Entries = [keyof T, T[keyof T]][] + // TODO: vite-node should export this const WRAPPER_LENGTH = 185 @@ -74,6 +74,7 @@ const VITE_EXPORTS_LINE_PATTERN const DECORATOR_METADATA_PATTERN = /_ts_metadata\("design:paramtypes", \[[^\]]*\]\),*/g const DEFAULT_PROJECT: unique symbol = Symbol.for('default-project') +const FILE_PROTOCOL = 'file://' const debug = createDebug('vitest:coverage') let uniqueId = 0 @@ -124,9 +125,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage lines: config.thresholds['100'] ? 100 : config.thresholds.lines, branches: config.thresholds['100'] ? 100 : config.thresholds.branches, functions: config.thresholds['100'] ? 100 : config.thresholds.functions, - statements: config.thresholds['100'] - ? 100 - : config.thresholds.statements, + statements: config.thresholds['100'] ? 100 : config.thresholds.statements, }, } @@ -183,14 +182,14 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage * backwards compatibility is a breaking change. */ onAfterSuiteRun({ coverage, transformMode, projectName }: AfterSuiteRunMeta): void { - if (transformMode !== 'web' && transformMode !== 'ssr') { + if (transformMode !== 'web' && transformMode !== 'ssr' && transformMode !== 'browser') { throw new Error(`Invalid transform mode: ${transformMode}`) } let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT) if (!entry) { - entry = { web: [], ssr: [] } + entry = { web: [], ssr: [], browser: [] } this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry) } @@ -212,36 +211,30 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage await Promise.all(this.pendingPromises) this.pendingPromises = [] - for (const [ - projectName, - coveragePerProject, - ] of this.coverageFiles.entries()) { - for (const [transformMode, filenames] of Object.entries( - coveragePerProject, - ) as [AfterSuiteRunMeta['transformMode'], Filename[]][]) { + for (const [projectName, coveragePerProject] of this.coverageFiles.entries()) { + for (const [transformMode, filenames] of Object.entries(coveragePerProject) as Entries) { let merged: RawCoverage = { result: [] } - for (const chunk of this.toSlices( - filenames, - this.options.processingConcurrency, - )) { + const project = this.ctx.projects.find(p => p.getName() === projectName) || this.ctx.getCoreWorkspaceProject() + + for (const chunk of this.toSlices(filenames, this.options.processingConcurrency)) { if (debug.enabled) { index += chunk.length debug('Covered files %d/%d', index, total) } - await Promise.all( - chunk.map(async (filename) => { - const contents = await fs.readFile(filename, 'utf-8') - const coverage = JSON.parse(contents) as RawCoverage - merged = mergeProcessCovs([merged, coverage]) - }), + await Promise.all(chunk.map(async (filename) => { + const contents = await fs.readFile(filename, 'utf-8') + const coverage = JSON.parse(contents) as RawCoverage + + merged = mergeProcessCovs([merged, coverage]) + }), ) } const converted = await this.convertCoverage( merged, - projectName, + project, transformMode, ) @@ -404,6 +397,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage const { originalSource } = await this.getSources( filename.href, transformResults, + file => this.ctx.vitenode.transformRequest(file), ) const coverage = { @@ -441,9 +435,10 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage return merged } - private async getSources( + private async getSources>)>( url: string, transformResults: TransformResults, + onTransform: (filepath: string) => Promise, functions: Profiler.FunctionCoverage[] = [], ): Promise<{ source: string @@ -454,16 +449,11 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage const filePath = normalize(fileURLToPath(url)) let isExecuted = true - let transformResult: - | FetchResult - | Awaited> - = transformResults.get(filePath) + let transformResult: FetchResult | TransformResult | undefined = transformResults.get(filePath) if (!transformResult) { isExecuted = false - transformResult = await this.ctx.vitenode - .transformRequest(filePath) - .catch(() => null) + transformResult = await onTransform(removeStartsWith(url, FILE_PROTOCOL)).catch(() => undefined) } const map = transformResult?.map as EncodedSourceMap | undefined @@ -513,27 +503,49 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage private async convertCoverage( coverage: RawCoverage, - projectName?: ProjectName, - transformMode?: 'web' | 'ssr', + project: WorkspaceProject = this.ctx.getCoreWorkspaceProject(), + transformMode?: keyof CoverageFilesByTransformMode, ): Promise { - const viteNode - = this.ctx.projects.find(project => project.getName() === projectName) - ?.vitenode || this.ctx.vitenode - const fetchCache = transformMode - ? viteNode.fetchCaches[transformMode] - : viteNode.fetchCache + let fetchCache = project.vitenode.fetchCache + + if (transformMode) { + fetchCache = transformMode === 'browser' ? new Map() : project.vitenode.fetchCaches[transformMode] + } + const transformResults = normalizeTransformResults(fetchCache) - const scriptCoverages = coverage.result.filter(result => - this.testExclude.shouldInstrument(fileURLToPath(result.url)), - ) + async function onTransform(filepath: string) { + if (transformMode === 'browser' && project.browser) { + const result = await project.browser.vite.transformRequest(removeStartsWith(filepath, project.config.root)) + + if (result) { + return { ...result, code: `${result.code}// ` } + } + } + return project.vitenode.transformRequest(filepath) + } + + const scriptCoverages = [] + + for (const result of coverage.result) { + if (transformMode === 'browser') { + if (result.url.startsWith('/@fs')) { + result.url = `${FILE_PROTOCOL}${removeStartsWith(result.url, '/@fs')}` + } + else { + result.url = `${FILE_PROTOCOL}${project.config.root}${result.url}` + } + } + + if (this.testExclude.shouldInstrument(fileURLToPath(result.url))) { + scriptCoverages.push(result) + } + } + const coverageMap = libCoverage.createCoverageMap({}) let index = 0 - for (const chunk of this.toSlices( - scriptCoverages, - this.options.processingConcurrency, - )) { + for (const chunk of this.toSlices(scriptCoverages, this.options.processingConcurrency)) { if (debug.enabled) { index += chunk.length debug('Converting %d/%d', index, scriptCoverages.length) @@ -544,6 +556,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage const sources = await this.getSources( url, transformResults, + onTransform, functions, ) @@ -645,3 +658,11 @@ function normalizeTransformResults( return normalized } + +function removeStartsWith(filepath: string, start: string) { + if (filepath.startsWith(start)) { + return filepath.slice(start.length) + } + + return filepath +} diff --git a/packages/coverage-v8/src/takeCoverage.ts b/packages/coverage-v8/src/takeCoverage.ts deleted file mode 100644 index 4ffabbd93017..000000000000 --- a/packages/coverage-v8/src/takeCoverage.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * For details about the Profiler.* messages see https://chromedevtools.github.io/devtools-protocol/v8/Profiler/ - */ - -import inspector from 'node:inspector' -import type { Profiler } from 'node:inspector' -import { provider } from 'std-env' - -const session = new inspector.Session() - -export function startCoverage(): void { - session.connect() - session.post('Profiler.enable') - session.post('Profiler.startPreciseCoverage', { - callCount: true, - detailed: true, - }) -} - -export async function takeCoverage(): Promise<{ result: Profiler.ScriptCoverage[] }> { - return new Promise((resolve, reject) => { - session.post('Profiler.takePreciseCoverage', async (error, coverage) => { - if (error) { - return reject(error) - } - - // Reduce amount of data sent over rpc by doing some early result filtering - const result = coverage.result.filter(filterResult) - - resolve({ result }) - }) - - if (provider === 'stackblitz') { - resolve({ result: [] }) - } - }) -} - -export function stopCoverage(): void { - session.post('Profiler.stopPreciseCoverage') - session.post('Profiler.disable') - session.disconnect() -} - -function filterResult(coverage: Profiler.ScriptCoverage): boolean { - if (!coverage.url.startsWith('file://')) { - return false - } - - if (coverage.url.includes('/node_modules/')) { - return false - } - - return true -} diff --git a/packages/coverage-v8/tsconfig.json b/packages/coverage-v8/tsconfig.json index 73cb131061ac..27360a45c746 100644 --- a/packages/coverage-v8/tsconfig.json +++ b/packages/coverage-v8/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "moduleResolution": "Bundler" + "moduleResolution": "Bundler", + "types": ["@vitest/browser/providers/playwright"] }, "include": ["./src/**/*.ts"], "exclude": ["./dist"] diff --git a/packages/vitest/src/integrations/coverage.ts b/packages/vitest/src/integrations/coverage.ts index c3c6ed831d05..0cfbbabe9f97 100644 --- a/packages/vitest/src/integrations/coverage.ts +++ b/packages/vitest/src/integrations/coverage.ts @@ -6,6 +6,7 @@ import type { interface Loader { executeId: (id: string) => Promise<{ default: CoverageProviderModule }> + isBrowser?: boolean } export const CoverageProviderMap: Record = { @@ -24,9 +25,13 @@ async function resolveCoverageProviderModule( const provider = options.provider if (provider === 'v8' || provider === 'istanbul') { - const { default: coverageModule } = await loader.executeId( - CoverageProviderMap[provider], - ) + let builtInModule = CoverageProviderMap[provider] + + if (provider === 'v8' && loader.isBrowser) { + builtInModule += '/browser' + } + + const { default: coverageModule } = await loader.executeId(builtInModule) if (!coverageModule) { throw new Error( diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index e75e9d617ba0..443c2d8a6d5e 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -222,14 +222,17 @@ export function resolveConfig( } } - if ( - resolved.coverage.provider === 'v8' - && resolved.coverage.enabled - && isBrowserEnabled(resolved) - ) { - throw new Error( - '@vitest/coverage-v8 does not work with --browser. Use @vitest/coverage-istanbul instead', - ) + // In browser-mode v8-coverage works only with playwright + chromium + if (resolved.browser.enabled && resolved.coverage.enabled && resolved.coverage.provider === 'v8') { + if (!(resolved.browser.provider === 'playwright' && resolved.browser.name === 'chromium')) { + const browserConfig = { browser: { provider: resolved.browser.provider, name: resolved.browser.name } } + + throw new Error( + `@vitest/coverage-v8 does not work with\n${JSON.stringify(browserConfig, null, 2)}\n` + + `\nUse either:\n${JSON.stringify({ browser: { provider: 'playwright', name: 'chromium' } }, null, 2)}` + + `\n\n...or change your coverage provider to:\n${JSON.stringify({ coverage: { provider: 'istanbul' } }, null, 2)}\n`, + ) + } } resolved.coverage.reporter = resolveCoverageReporters(resolved.coverage.reporter) diff --git a/packages/vitest/src/types/general.ts b/packages/vitest/src/types/general.ts index 22f86a00fe04..74b7f2e27be9 100644 --- a/packages/vitest/src/types/general.ts +++ b/packages/vitest/src/types/general.ts @@ -24,7 +24,7 @@ export interface ModuleCache { export interface AfterSuiteRunMeta { coverage?: unknown - transformMode: TransformMode + transformMode: TransformMode | 'browser' projectName?: string } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5810dc31086..4b9a3b359b71 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -610,6 +610,9 @@ importers: '@types/istanbul-reports': specifier: ^3.0.4 version: 3.0.4 + '@vitest/browser': + specifier: workspace:* + version: link:../browser pathe: specifier: ^1.1.2 version: 1.1.2 diff --git a/test/config/fixtures/workspace/browser/workspace-with-browser.ts b/test/config/fixtures/workspace/browser/workspace-with-browser.ts index 993a69926b3f..96764a79256c 100644 --- a/test/config/fixtures/workspace/browser/workspace-with-browser.ts +++ b/test/config/fixtures/workspace/browser/workspace-with-browser.ts @@ -6,6 +6,7 @@ export default defineWorkspace([ name: "Browser project", browser: { enabled: true, + provider: 'webdriverio', name: 'chrome' }, } diff --git a/test/config/test/failures.test.ts b/test/config/test/failures.test.ts index 7bb8a64337da..b633229a8eff 100644 --- a/test/config/test/failures.test.ts +++ b/test/config/test/failures.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from 'vitest' +import { beforeEach, expect, test } from 'vitest' import type { UserConfig } from 'vitest' import { version } from 'vitest/package.json' @@ -13,6 +13,20 @@ function runVitestCli(...cliArgs: string[]) { return testUtils.runVitestCli('run', 'fixtures/test/', ...cliArgs) } +beforeEach((ctx) => { + const errors: Parameters[] = [] + const original = console.error + console.error = (...args) => errors.push(args) + + ctx.onTestFailed(() => { + errors.forEach(args => original(...args)) + }) + + return () => { + console.error = original + } +}) + test('shard cannot be used with watch mode', async () => { const { stderr } = await runVitest({ watch: true, shard: '1/2' }) @@ -49,16 +63,67 @@ test('inspect-brk cannot be used with multi processing', async () => { expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"') }) -test('v8 coverage provider cannot be used with browser', async () => { - const { stderr } = await runVitest({ coverage: { enabled: true }, browser: { enabled: true, name: 'chrome' } }) +test('v8 coverage provider throws when not playwright + chromium', async () => { + const providers = ['playwright', 'webdriverio', 'preview'] + const names = ['edge', 'chromium', 'webkit', 'chrome', 'firefox', 'safari'] + + for (const provider of providers) { + for (const name of names) { + if (provider === 'playwright' && name === 'chromium') { + continue + } + + const { stderr } = await runVitest({ + coverage: { + enabled: true, + }, + browser: { + enabled: true, + provider, + name, + }, + }) + + expect(stderr).toMatch( +`Error: @vitest/coverage-v8 does not work with +{ + "browser": { + "provider": "${provider}", + "name": "${name}" + } +} + +Use either: +{ + "browser": { + "provider": "playwright", + "name": "chromium" + } +} - expect(stderr).toMatch('Error: @vitest/coverage-v8 does not work with --browser. Use @vitest/coverage-istanbul instead') +...or change your coverage provider to: +{ + "coverage": { + "provider": "istanbul" + } +} +`, + ) + } + } }) -test('v8 coverage provider cannot be used with browser in workspace', async () => { +test('v8 coverage provider cannot be used in workspace without playwright + chromium', async () => { const { stderr } = await runVitest({ coverage: { enabled: true }, workspace: './fixtures/workspace/browser/workspace-with-browser.ts' }) - - expect(stderr).toMatch('Error: @vitest/coverage-v8 does not work with --browser. Use @vitest/coverage-istanbul instead') + expect(stderr).toMatch( +`Error: @vitest/coverage-v8 does not work with +{ + "browser": { + "provider": "webdriverio", + "name": "chrome" + } +}`, + ) }) test('coverage reportsDirectory cannot be current working directory', async () => { diff --git a/test/coverage-test/fixtures/src/dynamic-files.ts b/test/coverage-test/fixtures/src/dynamic-files.ts index fc3bdb19cce9..1d06ef3e9653 100644 --- a/test/coverage-test/fixtures/src/dynamic-files.ts +++ b/test/coverage-test/fixtures/src/dynamic-files.ts @@ -17,7 +17,7 @@ export function run() { function uncovered() {} `.trim(), 'utf-8') - const { run } = await import(filename) + const { run } = await import(/* @vite-ignore */ filename) if (run() !== 'Import works') { throw new Error(`Failed to run ${filename}`) diff --git a/test/coverage-test/fixtures/test/allow-external-fixture.test.ts b/test/coverage-test/fixtures/test/allow-external-fixture.test.ts new file mode 100644 index 000000000000..0b7f631e1676 --- /dev/null +++ b/test/coverage-test/fixtures/test/allow-external-fixture.test.ts @@ -0,0 +1,11 @@ +import { expect, test } from 'vitest' +import { multiply } from '../src/math' +import * as ExternalMath from '../../../test-utils/fixtures/external-math' + +test('calling files outside project root', () => { + expect(ExternalMath.sum(2, 3)).toBe(5) +}) + +test('multiply - add some files to report', () => { + expect(multiply(2, 3)).toBe(6) +}) diff --git a/test/coverage-test/fixtures/test/pre-transpiled-fixture.test.ts b/test/coverage-test/fixtures/test/pre-transpiled-fixture.test.ts new file mode 100644 index 000000000000..608cf9292442 --- /dev/null +++ b/test/coverage-test/fixtures/test/pre-transpiled-fixture.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from 'vitest' +import * as transpiled from '../src/pre-transpiled/transpiled.js' + +test('run pre-transpiled sources', () => { + expect(transpiled.hello).toBeTypeOf('function') + expect(transpiled.hello()).toBeUndefined() +}) diff --git a/test/coverage-test/test/allow-external.test.ts b/test/coverage-test/test/allow-external.test.ts index 08b6e1da0760..854fcbfaf970 100644 --- a/test/coverage-test/test/allow-external.test.ts +++ b/test/coverage-test/test/allow-external.test.ts @@ -1,18 +1,16 @@ import { expect } from 'vitest' -import { coverageTest, normalizeURL, readCoverageMap, runVitest, test } from '../utils' -import { multiply } from '../fixtures/src/math' -import * as ExternalMath from '../../test-utils/fixtures/math' +import { readCoverageMap, runVitest, test } from '../utils' test('{ allowExternal: true } includes files outside project root', async () => { await runVitest({ - include: [normalizeURL(import.meta.url)], - coverage: { allowExternal: true, reporter: 'json', include: ['**/fixtures/**'] }, + include: ['fixtures/test/allow-external-fixture.test.ts'], + coverage: { allowExternal: true, reporter: 'json', include: ['**/fixtures/**'], all: false }, }) const coverageMap = await readCoverageMap() const files = coverageMap.files() // File outside project root - expect(files).toContain('/test/test-utils/fixtures/math.ts') + expect(files).toContain('/test/test-utils/fixtures/external-math.ts') // Files inside project root should always be included expect(files).toContain('/fixtures/src/math.ts') @@ -20,23 +18,15 @@ test('{ allowExternal: true } includes files outside project root', async () => test('{ allowExternal: false } excludes files outside project root', async () => { await runVitest({ - include: [normalizeURL(import.meta.url)], + include: ['fixtures/test/allow-external-fixture.test.ts'], coverage: { allowExternal: false, reporter: 'json', include: ['**/fixtures/**'] }, }) const coverageMap = await readCoverageMap() const files = coverageMap.files() // File outside project root - expect(files.find(file => file.includes('test-utils/fixtures/math.ts'))).toBeFalsy() + expect(files.find(file => file.includes('test-utils/fixtures/external-math.ts'))).toBeFalsy() // Files inside project root should always be included expect(files).toContain('/fixtures/src/math.ts') }) - -coverageTest('calling files outside project root', () => { - expect(ExternalMath.sum(2, 3)).toBe(5) -}) - -coverageTest('multiply - add some files to report', () => { - expect(multiply(2, 3)).toBe(6) -}) diff --git a/test/coverage-test/test/changed.test.ts b/test/coverage-test/test/changed.test.ts index 33ed43d6d139..ada4a686306d 100644 --- a/test/coverage-test/test/changed.test.ts +++ b/test/coverage-test/test/changed.test.ts @@ -3,6 +3,10 @@ import { resolve } from 'node:path' import { afterAll, beforeAll, expect } from 'vitest' import { readCoverageMap, runVitest, test } from '../utils' +// Note that this test may fail if you have new files in "vitest/test/coverage/src" +// and have not yet committed those +const SKIP = !!process.env.ECOSYSTEM_CI || !process.env.GITHUB_ACTIONS + const FILE_TO_CHANGE = resolve('./fixtures/src/file-to-change.ts') const NEW_UNCOVERED_FILE = resolve('./fixtures/src/new-uncovered-file.ts') @@ -39,8 +43,6 @@ test('{ changed: "HEAD" }', async () => { const coverageMap = await readCoverageMap() - // Note that this test may fail if you have new files in "vitest/test/coverage/src" - // and have not yet committed those expect(coverageMap.files()).toMatchInlineSnapshot(` [ "/fixtures/src/file-to-change.ts", @@ -53,4 +55,4 @@ test('{ changed: "HEAD" }', async () => { const changedFile = coverageMap.fileCoverageFor('/fixtures/src/file-to-change.ts').toSummary() expect(changedFile.lines.pct).toBeGreaterThanOrEqual(50) -}, !!process.env.ECOSYSTEM_CI) +}, SKIP) diff --git a/test/coverage-test/test/pre-transpiled-source.test.ts b/test/coverage-test/test/pre-transpiled-source.test.ts index 49bc87415553..35ab9d7dc5d1 100644 --- a/test/coverage-test/test/pre-transpiled-source.test.ts +++ b/test/coverage-test/test/pre-transpiled-source.test.ts @@ -1,11 +1,10 @@ import libCoverage from 'istanbul-lib-coverage' import { expect } from 'vitest' -import { coverageTest, isV8Provider, normalizeURL, readCoverageJson, runVitest, test } from '../utils' -import * as transpiled from '../fixtures/src/pre-transpiled/transpiled.js' +import { isV8Provider, readCoverageJson, runVitest, test } from '../utils' test('pre-transpiled code with source maps to original (#5341)', async () => { await runVitest({ - include: [normalizeURL(import.meta.url)], + include: ['fixtures/test/pre-transpiled-fixture.test.ts'], coverage: { include: ['fixtures/src/**'], reporter: 'json', @@ -25,8 +24,3 @@ test('pre-transpiled code with source maps to original (#5341)', async () => { expect(JSON.stringify(coverageJson, null, 2)).toMatchFileSnapshot(`__snapshots__/pre-transpiled-${isV8Provider() ? 'v8' : 'istanbul'}.snapshot.json`) }) - -coverageTest('run pre-transpiled sources', () => { - expect(transpiled.hello).toBeTypeOf('function') - expect(transpiled.hello()).toBeUndefined() -}) diff --git a/test/coverage-test/test/vue.test.ts b/test/coverage-test/test/vue.test.ts index d78cf51e200c..ee4ca7debfa8 100644 --- a/test/coverage-test/test/vue.test.ts +++ b/test/coverage-test/test/vue.test.ts @@ -1,7 +1,7 @@ import { resolve } from 'node:path' import { readdirSync } from 'node:fs' import { beforeAll, expect } from 'vitest' -import { isV8Provider, readCoverageMap, runVitest, test } from '../utils' +import { isBrowser, isV8Provider, readCoverageMap, runVitest, test } from '../utils' beforeAll(async () => { await runVitest({ @@ -25,7 +25,9 @@ test('coverage results matches snapshot', async () => { const summary = coverageMap.getCoverageSummary() if (isV8Provider()) { - expect(summary).toMatchInlineSnapshot(` + const { branches, functions, lines, statements } = summary + + expect({ branches, functions }).toMatchInlineSnapshot(` { "branches": { "covered": 5, @@ -33,32 +35,52 @@ test('coverage results matches snapshot', async () => { "skipped": 0, "total": 6, }, - "branchesTrue": { - "covered": 0, - "pct": "Unknown", - "skipped": 0, - "total": 0, - }, "functions": { "covered": 3, "pct": 60, "skipped": 0, "total": 5, }, - "lines": { - "covered": 36, - "pct": 81.81, - "skipped": 0, - "total": 44, - }, - "statements": { - "covered": 36, - "pct": 81.81, - "skipped": 0, - "total": 44, - }, } `) + + // Lines and statements are not 100% identical between node and browser - not sure if it's Vue, Vite or Vitest issue + if (isBrowser()) { + expect({ lines, statements }).toMatchInlineSnapshot(` + { + "lines": { + "covered": 40, + "pct": 83.33, + "skipped": 0, + "total": 48, + }, + "statements": { + "covered": 40, + "pct": 83.33, + "skipped": 0, + "total": 48, + }, + } + `) + } + else { + expect({ lines, statements }).toMatchInlineSnapshot(` + { + "lines": { + "covered": 36, + "pct": 81.81, + "skipped": 0, + "total": 44, + }, + "statements": { + "covered": 36, + "pct": 81.81, + "skipped": 0, + "total": 44, + }, + } + `) + } } else { expect(summary).toMatchInlineSnapshot(` diff --git a/test/coverage-test/utils.ts b/test/coverage-test/utils.ts index dde5e15583ab..865b7d8d7cb1 100644 --- a/test/coverage-test/utils.ts +++ b/test/coverage-test/utils.ts @@ -96,6 +96,10 @@ export function isV8Provider() { return process.env.COVERAGE_PROVIDER === 'v8' } +export function isBrowser() { + return process.env.COVERAGE_BROWSER === 'true' +} + export function normalizeURL(importMetaURL: string) { return normalize(fileURLToPath(importMetaURL)) } diff --git a/test/coverage-test/vitest.workspace.custom.ts b/test/coverage-test/vitest.workspace.custom.ts index 0e434e19b21e..5d40bec498e8 100644 --- a/test/coverage-test/vitest.workspace.custom.ts +++ b/test/coverage-test/vitest.workspace.custom.ts @@ -60,14 +60,39 @@ export default defineWorkspace([ { test: { ...config.test, - name: 'browser', + name: 'istanbul-browser', env: { COVERAGE_PROVIDER: 'istanbul', COVERAGE_BROWSER: 'true' }, include: [ BROWSER_TESTS, // Other non-provider-specific tests that should be run on browser mode as well + '**/allow-external.test.ts', '**/ignore-hints.test.ts', '**/import-attributes.test.ts', + '**/pre-transpiled-source.test.ts', + '**/multi-suite.test.ts', + '**/setup-files.test.ts', + '**/results-snapshot.test.ts', + '**/reporters.test.ts', + '**/temporary-files.test.ts', + '**/test-reporter-conflicts.test.ts', + '**/vue.test.ts', + ], + }, + }, + { + test: { + ...config.test, + name: 'v8-browser', + env: { COVERAGE_PROVIDER: 'v8', COVERAGE_BROWSER: 'true' }, + include: [ + BROWSER_TESTS, + + // Other non-provider-specific tests that should be run on browser mode as well + '**/allow-external.test.ts', + '**/ignore-hints.test.ts', + '**/import-attributes.test.ts', + '**/pre-transpiled-source.test.ts', '**/multi-suite.test.ts', '**/setup-files.test.ts', '**/results-snapshot.test.ts', diff --git a/test/test-utils/fixtures/math.ts b/test/test-utils/fixtures/external-math.ts similarity index 100% rename from test/test-utils/fixtures/math.ts rename to test/test-utils/fixtures/external-math.ts