diff --git a/packages/browser/src/client/runner.ts b/packages/browser/src/client/runner.ts index 0829e951c0f8..faf74af717bc 100644 --- a/packages/browser/src/client/runner.ts +++ b/packages/browser/src/client/runner.ts @@ -42,8 +42,14 @@ export function createBrowserRunner(original: any, coverageModule: CoverageHandl async onAfterRunFiles() { await super.onAfterRun?.() const coverage = await coverageModule?.takeCoverage?.() - if (coverage) - await rpc().onAfterSuiteRun({ coverage }) + + if (coverage) { + await rpc().onAfterSuiteRun({ + coverage, + transformMode: 'web', + projectName: this.config.name, + }) + } } onCollected(files: File[]): unknown { diff --git a/packages/coverage-istanbul/src/provider.ts b/packages/coverage-istanbul/src/provider.ts index 52013e0820e4..b0443dd6a4ee 100644 --- a/packages/coverage-istanbul/src/provider.ts +++ b/packages/coverage-istanbul/src/provider.ts @@ -6,7 +6,7 @@ import { BaseCoverageProvider } from 'vitest/coverage' import c from 'picocolors' import libReport from 'istanbul-lib-report' import reports from 'istanbul-reports' -import type { CoverageMap } from 'istanbul-lib-coverage' +import type { CoverageMap, CoverageMapData } from 'istanbul-lib-coverage' import libCoverage from 'istanbul-lib-coverage' import libSourceMaps from 'istanbul-lib-source-maps' import { type Instrumenter, createInstrumenter } from 'istanbul-lib-instrument' @@ -16,6 +16,8 @@ import _TestExclude from 'test-exclude' import { COVERAGE_STORE_KEY } from './constants' type Options = ResolvedCoverageOptions<'istanbul'> +type CoverageByTransformMode = Record +type ProjectName = NonNullable | typeof DEFAULT_PROJECT interface TestExclude { new(opts: { @@ -31,6 +33,8 @@ interface TestExclude { } } +const DEFAULT_PROJECT = Symbol.for('default-project') + export class IstanbulCoverageProvider extends BaseCoverageProvider implements CoverageProvider { name = 'istanbul' @@ -45,7 +49,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co * If storing in memory causes issues, we can simply write these into fs in `onAfterSuiteRun` * and read them back when merging coverage objects in `onAfterAllFilesRun`. */ - coverages: any[] = [] + coverages = new Map() initialize(ctx: Vitest) { const config: CoverageIstanbulOptions = ctx.config.coverage @@ -106,36 +110,52 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co return { code, map } } - onAfterSuiteRun({ coverage }: AfterSuiteRunMeta) { - this.coverages.push(coverage) + /* + * Coverage and meta information passed from Vitest runners. + * Note that adding new entries here and requiring on those without + * backwards compatibility is a breaking change. + */ + onAfterSuiteRun({ coverage, transformMode, projectName }: AfterSuiteRunMeta) { + if (transformMode !== 'web' && transformMode !== 'ssr') + throw new Error(`Invalid transform mode: ${transformMode}`) + + let entry = this.coverages.get(projectName || DEFAULT_PROJECT) + + if (!entry) { + entry = { web: [], ssr: [] } + this.coverages.set(projectName || DEFAULT_PROJECT, entry) + } + + entry[transformMode].push(coverage as CoverageMapData) } async clean(clean = true) { if (clean && existsSync(this.options.reportsDirectory)) await fs.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 }) - this.coverages = [] + this.coverages = new Map() } async reportCoverage({ allTestsRun }: ReportContext = {}) { - const mergedCoverage: CoverageMap = this.coverages.reduce((coverage, previousCoverageMap) => { - const map = libCoverage.createCoverageMap(coverage) - map.merge(previousCoverageMap) - return map - }, libCoverage.createCoverageMap({})) - - if (this.options.all && allTestsRun) - await this.includeUntestedFiles(mergedCoverage) - - includeImplicitElseBranches(mergedCoverage) + const coverageMaps = await Promise.all( + Array.from(this.coverages.values()).map(coverages => [ + mergeAndTransformCoverage(coverages.ssr), + mergeAndTransformCoverage(coverages.web), + ]).flat(), + ) + + if (this.options.all && allTestsRun) { + const coveredFiles = coverageMaps.map(map => map.files()).flat() + const uncoveredCoverage = await this.getCoverageMapForUncoveredFiles(coveredFiles) + + coverageMaps.push(await mergeAndTransformCoverage([uncoveredCoverage])) + } - const sourceMapStore = libSourceMaps.createSourceMapStore() - const coverageMap: CoverageMap = await sourceMapStore.transformCoverage(mergedCoverage) + const coverageMap = mergeCoverageMaps(...coverageMaps) const context = libReport.createContext({ dir: this.options.reportsDirectory, coverageMap, - sourceFinder: sourceMapStore.sourceFinder, watermarks: this.options.watermarks, }) @@ -181,19 +201,21 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co } } - async includeUntestedFiles(coverageMap: CoverageMap) { + async getCoverageMapForUncoveredFiles(coveredFiles: string[]) { // Load, instrument and collect empty coverages from all files which // are not already in the coverage map const includedFiles = await this.testExclude.glob(this.ctx.config.root) const uncoveredFiles = includedFiles .map(file => resolve(this.ctx.config.root, file)) - .filter(file => !coverageMap.data[file]) + .filter(file => !coveredFiles.includes(file)) const transformResults = await Promise.all(uncoveredFiles.map(async (filename) => { const transformResult = await this.ctx.vitenode.transformRequest(filename) return { transformResult, filename } })) + const coverageMap = libCoverage.createCoverageMap({}) + for (const { transformResult, filename } of transformResults) { const sourceMap = transformResult?.map @@ -209,9 +231,27 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co coverageMap.addFileCoverage(lastCoverage) } } + + return coverageMap.data } } +async function mergeAndTransformCoverage(coverages: CoverageMapData[]) { + const mergedCoverage = mergeCoverageMaps(...coverages) + includeImplicitElseBranches(mergedCoverage) + + const sourceMapStore = libSourceMaps.createSourceMapStore() + return await sourceMapStore.transformCoverage(mergedCoverage) +} + +function mergeCoverageMaps(...coverageMaps: (CoverageMap | CoverageMapData)[]) { + return coverageMaps.reduce((coverage, previousCoverageMap) => { + const map = libCoverage.createCoverageMap(coverage) + map.merge(previousCoverageMap) + return map + }, libCoverage.createCoverageMap({})) +} + /** * Remove possible query parameters from filenames * - From `/src/components/Header.component.ts?vue&type=script&src=true&lang.ts` diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts index bb22a7958ba2..0e8ee984356a 100644 --- a/packages/coverage-v8/src/provider.ts +++ b/packages/coverage-v8/src/provider.ts @@ -5,7 +5,7 @@ import v8ToIstanbul from 'v8-to-istanbul' import { mergeProcessCovs } from '@bcoe/v8-coverage' import libReport from 'istanbul-lib-report' import reports from 'istanbul-reports' -import type { CoverageMap } from 'istanbul-lib-coverage' +import type { CoverageMap, CoverageMapData } from 'istanbul-lib-coverage' import libCoverage from 'istanbul-lib-coverage' import libSourceMaps from 'istanbul-lib-source-maps' import MagicString from 'magic-string' @@ -39,12 +39,16 @@ interface TestExclude { type Options = ResolvedCoverageOptions<'v8'> type TransformResults = Map +type RawCoverage = Profiler.TakePreciseCoverageReturnType +type CoverageByTransformMode = Record +type ProjectName = NonNullable | typeof DEFAULT_PROJECT // TODO: vite-node should export this const WRAPPER_LENGTH = 185 // Note that this needs to match the line ending as well const VITE_EXPORTS_LINE_PATTERN = /Object\.defineProperty\(__vite_ssr_exports__.*\n/g +const DEFAULT_PROJECT = Symbol.for('default-project') export class V8CoverageProvider extends BaseCoverageProvider implements CoverageProvider { name = 'v8' @@ -52,7 +56,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage ctx!: Vitest options!: Options testExclude!: InstanceType - coverages: Profiler.TakePreciseCoverageReturnType[] = [] + coverages = new Map() initialize(ctx: Vitest) { const config: CoverageV8Options = ctx.config.coverage @@ -92,54 +96,52 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage if (clean && existsSync(this.options.reportsDirectory)) await fs.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 }) - this.coverages = [] + this.coverages = new Map() } - onAfterSuiteRun({ coverage }: AfterSuiteRunMeta) { - this.coverages.push(coverage as Profiler.TakePreciseCoverageReturnType) + /* + * Coverage and meta information passed from Vitest runners. + * Note that adding new entries here and requiring on those without + * backwards compatibility is a breaking change. + */ + onAfterSuiteRun({ coverage, transformMode, projectName }: AfterSuiteRunMeta) { + if (transformMode !== 'web' && transformMode !== 'ssr') + throw new Error(`Invalid transform mode: ${transformMode}`) + + let entry = this.coverages.get(projectName || DEFAULT_PROJECT) + + if (!entry) { + entry = { web: [], ssr: [] } + this.coverages.set(projectName || DEFAULT_PROJECT, entry) + } + + entry[transformMode].push(coverage as RawCoverage) } async reportCoverage({ allTestsRun }: ReportContext = {}) { if (provider === 'stackblitz') this.ctx.logger.log(c.blue(' % ') + c.yellow('@vitest/coverage-v8 does not work on Stackblitz. Report will be empty.')) - const transformResults = normalizeTransformResults(this.ctx.projects.map(project => project.vitenode.fetchCache)) - const merged = mergeProcessCovs(this.coverages) - const scriptCoverages = merged.result.filter(result => this.testExclude.shouldInstrument(fileURLToPath(result.url))) + const coverageMaps = await Promise.all( + Array.from(this.coverages.entries()).map(([projectName, coverages]) => [ + this.mergeAndTransformCoverage(coverages.ssr, projectName, 'ssr'), + this.mergeAndTransformCoverage(coverages.web, projectName, 'web'), + ]).flat(), + ) if (this.options.all && allTestsRun) { - const coveredFiles = Array.from(scriptCoverages.map(r => r.url)) - const untestedFiles = await this.getUntestedFiles(coveredFiles, transformResults) + const coveredFiles = coverageMaps.map(map => map.files()).flat() + const untestedCoverage = await this.getUntestedFiles(coveredFiles) + const untestedCoverageResults = untestedCoverage.map(files => ({ result: [files] })) - scriptCoverages.push(...untestedFiles) + coverageMaps.push(await this.mergeAndTransformCoverage(untestedCoverageResults)) } - const converted = await Promise.all(scriptCoverages.map(async ({ url, functions }) => { - const sources = await this.getSources(url, transformResults, functions) - - // If no source map was found from vite-node we can assume this file was not run in the wrapper - const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0 - - const converter = v8ToIstanbul(url, wrapperLength, sources) - await converter.load() - - converter.applyCoverage(functions) - return converter.toIstanbul() - })) - - const mergedCoverage = converted.reduce((coverage, previousCoverageMap) => { - const map = libCoverage.createCoverageMap(coverage) - map.merge(previousCoverageMap) - return map - }, libCoverage.createCoverageMap({})) - - const sourceMapStore = libSourceMaps.createSourceMapStore() - const coverageMap: CoverageMap = await sourceMapStore.transformCoverage(mergedCoverage) + const coverageMap = mergeCoverageMaps(...coverageMaps) const context = libReport.createContext({ dir: this.options.reportsDirectory, coverageMap, - sourceFinder: sourceMapStore.sourceFinder, watermarks: this.options.watermarks, }) @@ -185,11 +187,13 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage } } - private async getUntestedFiles(testedFiles: string[], transformResults: TransformResults): Promise { + private async getUntestedFiles(testedFiles: string[]): Promise { + const transformResults = normalizeTransformResults(this.ctx.vitenode.fetchCache) + const includedFiles = await this.testExclude.glob(this.ctx.config.root) const uncoveredFiles = includedFiles .map(file => pathToFileURL(resolve(this.ctx.config.root, file))) - .filter(file => !testedFiles.includes(file.href)) + .filter(file => !testedFiles.includes(file.pathname)) return await Promise.all(uncoveredFiles.map(async (uncoveredFile) => { const { source } = await this.getSources(uncoveredFile.href, transformResults) @@ -247,6 +251,41 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage }, } } + + private async mergeAndTransformCoverage(coverages: RawCoverage[], projectName?: ProjectName, transformMode?: 'web' | 'ssr') { + const viteNode = this.ctx.projects.find(project => project.getName() === projectName)?.vitenode || this.ctx.vitenode + const fetchCache = transformMode ? viteNode.fetchCaches[transformMode] : viteNode.fetchCache + const transformResults = normalizeTransformResults(fetchCache) + + const merged = mergeProcessCovs(coverages) + const scriptCoverages = merged.result.filter(result => this.testExclude.shouldInstrument(fileURLToPath(result.url))) + + const converted = await Promise.all(scriptCoverages.map(async ({ url, functions }) => { + const sources = await this.getSources(url, transformResults, functions) + + // If no source map was found from vite-node we can assume this file was not run in the wrapper + const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0 + + const converter = v8ToIstanbul(url, wrapperLength, sources) + await converter.load() + + converter.applyCoverage(functions) + return converter.toIstanbul() + })) + + const mergedCoverage = mergeCoverageMaps(...converted) + + const sourceMapStore = libSourceMaps.createSourceMapStore() + return sourceMapStore.transformCoverage(mergedCoverage) + } +} + +function mergeCoverageMaps(...coverageMaps: (CoverageMap | CoverageMapData)[]) { + return coverageMaps.reduce((coverage, previousCoverageMap) => { + const map = libCoverage.createCoverageMap(coverage) + map.merge(previousCoverageMap) + return map + }, libCoverage.createCoverageMap({})) } /** @@ -284,16 +323,14 @@ function findLongestFunctionLength(functions: Profiler.FunctionCoverage[]) { }, 0) } -function normalizeTransformResults(fetchCaches: Map[]) { +function normalizeTransformResults(fetchCache: Map) { const normalized: TransformResults = new Map() - for (const fetchCache of fetchCaches) { - for (const [key, value] of fetchCache.entries()) { - const cleanEntry = cleanUrl(key) + for (const [key, value] of fetchCache.entries()) { + const cleanEntry = cleanUrl(key) - if (!normalized.has(cleanEntry)) - normalized.set(cleanEntry, value.result) - } + if (!normalized.has(cleanEntry)) + normalized.set(cleanEntry, value.result) } return normalized diff --git a/packages/vitest/src/node/pools/child.ts b/packages/vitest/src/node/pools/child.ts index 3a341b6d9f22..6f6cc7351c2b 100644 --- a/packages/vitest/src/node/pools/child.ts +++ b/packages/vitest/src/node/pools/child.ts @@ -100,6 +100,7 @@ export function createChildProcessPool(ctx: Vitest, { execArgv, env, forksPath } invalidates, environment, workerId, + projectName: project.getName(), } try { await pool.run(data, { name, channel }) diff --git a/packages/vitest/src/node/pools/threads.ts b/packages/vitest/src/node/pools/threads.ts index 60d9a85ad337..9e8023c8656a 100644 --- a/packages/vitest/src/node/pools/threads.ts +++ b/packages/vitest/src/node/pools/threads.ts @@ -88,6 +88,7 @@ export function createThreadsPool(ctx: Vitest, { execArgv, env, workerPath }: Po invalidates, environment, workerId, + projectName: project.getName(), } try { await pool.run(data, { transferList: [workerPort], name }) diff --git a/packages/vitest/src/node/pools/vm-threads.ts b/packages/vitest/src/node/pools/vm-threads.ts index 32e63189c574..35a1c6ae5f7d 100644 --- a/packages/vitest/src/node/pools/vm-threads.ts +++ b/packages/vitest/src/node/pools/vm-threads.ts @@ -95,6 +95,7 @@ export function createVmThreadsPool(ctx: Vitest, { execArgv, env, vmPath }: Pool invalidates, environment, workerId, + projectName: project.getName(), } try { await pool.run(data, { transferList: [workerPort], name }) diff --git a/packages/vitest/src/runtime/runners/index.ts b/packages/vitest/src/runtime/runners/index.ts index a9ed72112d3d..16e1a6925f22 100644 --- a/packages/vitest/src/runtime/runners/index.ts +++ b/packages/vitest/src/runtime/runners/index.ts @@ -64,8 +64,14 @@ export async function resolveTestRunner(config: ResolvedConfig, executor: Vitest const originalOnAfterRun = testRunner.onAfterRunFiles testRunner.onAfterRunFiles = async (files) => { + const state = getWorkerState() const coverage = await takeCoverageInsideWorker(config.coverage, executor) - rpc().onAfterSuiteRun({ coverage }) + rpc().onAfterSuiteRun({ + coverage, + transformMode: state.environment.transformMode, + projectName: state.ctx.projectName, + }) + await originalOnAfterRun?.call(testRunner, files) } diff --git a/packages/vitest/src/types/rpc.ts b/packages/vitest/src/types/rpc.ts index bd93be04c432..faad4cfe5107 100644 --- a/packages/vitest/src/types/rpc.ts +++ b/packages/vitest/src/types/rpc.ts @@ -47,6 +47,7 @@ export interface ResolvedTestEnvironment { export interface ContextRPC { config: ResolvedConfig + projectName: string files: string[] invalidates?: string[] environment: ContextTestEnvironment diff --git a/packages/vitest/src/types/worker.ts b/packages/vitest/src/types/worker.ts index afd77d668ed1..23e88a5eb21f 100644 --- a/packages/vitest/src/types/worker.ts +++ b/packages/vitest/src/types/worker.ts @@ -16,6 +16,8 @@ export type ResolveIdFunction = (id: string, importer?: string) => Promise diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 672b5370b18e..710c98d44cf5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1658,6 +1658,9 @@ importers: test/coverage-test: devDependencies: + '@ampproject/remapping': + specifier: ^2.2.1 + version: 2.2.1 '@types/istanbul-lib-coverage': specifier: ^2.0.4 version: 2.0.4 @@ -2064,6 +2067,9 @@ importers: test/workspaces: devDependencies: + '@ampproject/remapping': + specifier: ^2.2.1 + version: 2.2.1 '@types/istanbul-lib-coverage': specifier: ^2.0.4 version: 2.0.4 diff --git a/test/coverage-test/coverage-report-tests/__snapshots__/custom.report.test.ts.snap b/test/coverage-test/coverage-report-tests/__snapshots__/custom.report.test.ts.snap index ed9e39ee813f..1a271a091b87 100644 --- a/test/coverage-test/coverage-report-tests/__snapshots__/custom.report.test.ts.snap +++ b/test/coverage-test/coverage-report-tests/__snapshots__/custom.report.test.ts.snap @@ -6,9 +6,13 @@ exports[`custom json report 1`] = ` "initialized with context", "resolveOptions", "clean with force", - "onAfterSuiteRun with {"coverage":{"customCoverage":"Coverage report passed from workers to main thread"}}", + "onAfterSuiteRun", "reportCoverage with {"allTestsRun":true}", ], + "coverageReports": [ + "{"coverage":{"customCoverage":"Coverage report passed from workers to main thread"},"transformMode":"ssr","projectName":true}", + "{"coverage":{"customCoverage":"Coverage report passed from workers to main thread"},"transformMode":"web","projectName":true}", + ], "transformedFiles": [ "/src/Counter/Counter.component.ts", "/src/Counter/Counter.vue", @@ -22,6 +26,7 @@ exports[`custom json report 1`] = ` "/src/implicitElse.ts", "/src/importEnv.ts", "/src/index.mts", + "/src/multi-environment.ts", "/src/multi-suite.ts", "/src/utils.ts", ], diff --git a/test/coverage-test/coverage-report-tests/__snapshots__/istanbul.report.test.ts.snap b/test/coverage-test/coverage-report-tests/__snapshots__/istanbul.report.test.ts.snap index 9af54d6026c4..852af788a740 100644 --- a/test/coverage-test/coverage-report-tests/__snapshots__/istanbul.report.test.ts.snap +++ b/test/coverage-test/coverage-report-tests/__snapshots__/istanbul.report.test.ts.snap @@ -1342,6 +1342,467 @@ exports[`istanbul json report 1`] = ` }, }, }, + "/src/multi-environment.ts": { + "b": { + "0": [ + 0, + 4, + ], + "1": [ + 4, + 0, + ], + "2": [ + 1, + 3, + ], + "3": [ + 4, + 1, + ], + "4": [ + 0, + 3, + ], + "5": [ + 3, + 0, + ], + "6": [ + 1, + 2, + ], + "7": [ + 3, + 1, + ], + }, + "branchMap": { + "0": { + "loc": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 11, + }, + }, + "locations": [ + { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 11, + }, + }, + { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 14, + }, + }, + ], + "type": "if", + }, + "1": { + "loc": { + "end": { + "column": 26, + "line": 11, + }, + "start": { + "column": 6, + "line": 11, + }, + }, + "locations": [ + { + "end": { + "column": 17, + "line": 11, + }, + "start": { + "column": 6, + "line": 11, + }, + }, + { + "end": { + "column": 26, + "line": 11, + }, + "start": { + "column": 17, + "line": 11, + }, + }, + ], + "type": "binary-expr", + }, + "2": { + "loc": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 14, + }, + }, + "locations": [ + { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 14, + }, + }, + { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 19, + }, + }, + ], + "type": "if", + }, + "3": { + "loc": { + "end": { + "column": 31, + "line": 16, + }, + "start": { + "column": 11, + "line": 16, + }, + }, + "locations": [ + { + "end": { + "column": 22, + "line": 16, + }, + "start": { + "column": 11, + "line": 16, + }, + }, + { + "end": { + "column": 31, + "line": 16, + }, + "start": { + "column": 22, + "line": 16, + }, + }, + ], + "type": "binary-expr", + }, + "4": { + "loc": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 19, + }, + }, + "locations": [ + { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 19, + }, + }, + { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 23, + }, + }, + ], + "type": "if", + }, + "5": { + "loc": { + "end": { + "column": 33, + "line": 20, + }, + "start": { + "column": 11, + "line": 20, + }, + }, + "locations": [ + { + "end": { + "column": 23, + "line": 20, + }, + "start": { + "column": 11, + "line": 20, + }, + }, + { + "end": { + "column": 33, + "line": 20, + }, + "start": { + "column": 23, + "line": 20, + }, + }, + ], + "type": "binary-expr", + }, + "6": { + "loc": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 23, + }, + }, + "locations": [ + { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 23, + }, + }, + { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 23, + }, + }, + ], + "type": "if", + }, + "7": { + "loc": { + "end": { + "column": 33, + "line": 24, + }, + "start": { + "column": 11, + "line": 24, + }, + }, + "locations": [ + { + "end": { + "column": 23, + "line": 24, + }, + "start": { + "column": 11, + "line": 24, + }, + }, + { + "end": { + "column": 33, + "line": 24, + }, + "start": { + "column": 23, + "line": 24, + }, + }, + ], + "type": "binary-expr", + }, + }, + "f": { + "0": 4, + }, + "fnMap": { + "0": { + "decl": { + "end": { + "column": 20, + "line": 6, + }, + "start": { + "column": 16, + "line": 6, + }, + }, + "loc": { + "end": { + "column": null, + "line": 31, + }, + "start": { + "column": 42, + "line": 6, + }, + }, + "name": "sum", + }, + }, + "path": "/src/multi-environment.ts", + "s": { + "0": 2, + "1": 4, + "2": 0, + "3": 4, + "4": 1, + "5": 3, + "6": 0, + "7": 3, + "8": 1, + "9": 2, + }, + "statementMap": { + "0": { + "end": { + "column": null, + "line": 4, + }, + "start": { + "column": 23, + "line": 4, + }, + }, + "1": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 11, + }, + }, + "2": { + "end": { + "column": null, + "line": 13, + }, + "start": { + "column": 4, + "line": 13, + }, + }, + "3": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 14, + }, + }, + "4": { + "end": { + "column": null, + "line": 18, + }, + "start": { + "column": 4, + "line": 18, + }, + }, + "5": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 19, + }, + }, + "6": { + "end": { + "column": null, + "line": 22, + }, + "start": { + "column": 4, + "line": 22, + }, + }, + "7": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 2, + "line": 23, + }, + }, + "8": { + "end": { + "column": null, + "line": 26, + }, + "start": { + "column": 4, + "line": 26, + }, + }, + "9": { + "end": { + "column": null, + "line": 30, + }, + "start": { + "column": 2, + "line": 30, + }, + }, + }, + }, "/src/multi-suite.ts": { "b": {}, "branchMap": {}, diff --git a/test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap b/test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap index 08c1cc21d00e..402a1ab0d14f 100644 --- a/test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap +++ b/test/coverage-test/coverage-report-tests/__snapshots__/v8.report.test.ts.snap @@ -3305,6 +3305,707 @@ exports[`v8 json report 1`] = ` }, }, }, + "/src/multi-environment.ts": { + "all": false, + "b": { + "0": [ + 4, + ], + "1": [ + 0, + ], + "10": [ + 1, + ], + "2": [ + 0, + ], + "3": [ + 1, + ], + "4": [ + 1, + ], + "5": [ + 0, + ], + "6": [ + 0, + ], + "7": [ + 1, + ], + "8": [ + 0, + ], + "9": [ + 0, + ], + }, + "branchMap": { + "0": { + "line": 6, + "loc": { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 7, + "line": 6, + }, + }, + "locations": [ + { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 7, + "line": 6, + }, + }, + ], + "type": "branch", + }, + "1": { + "line": 11, + "loc": { + "end": { + "column": 26, + "line": 11, + }, + "start": { + "column": 12, + "line": 11, + }, + }, + "locations": [ + { + "end": { + "column": 26, + "line": 11, + }, + "start": { + "column": 12, + "line": 11, + }, + }, + ], + "type": "branch", + }, + "10": { + "line": 24, + "loc": { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 33, + "line": 24, + }, + }, + "locations": [ + { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 33, + "line": 24, + }, + }, + ], + "type": "branch", + }, + "2": { + "line": 11, + "loc": { + "end": { + "column": 3, + "line": 14, + }, + "start": { + "column": 26, + "line": 11, + }, + }, + "locations": [ + { + "end": { + "column": 3, + "line": 14, + }, + "start": { + "column": 26, + "line": 11, + }, + }, + ], + "type": "branch", + }, + "3": { + "line": 16, + "loc": { + "end": { + "column": 31, + "line": 16, + }, + "start": { + "column": 17, + "line": 16, + }, + }, + "locations": [ + { + "end": { + "column": 31, + "line": 16, + }, + "start": { + "column": 17, + "line": 16, + }, + }, + ], + "type": "branch", + }, + "4": { + "line": 16, + "loc": { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 31, + "line": 16, + }, + }, + "locations": [ + { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 31, + "line": 16, + }, + }, + ], + "type": "branch", + }, + "5": { + "line": 20, + "loc": { + "end": { + "column": 33, + "line": 20, + }, + "start": { + "column": 17, + "line": 20, + }, + }, + "locations": [ + { + "end": { + "column": 33, + "line": 20, + }, + "start": { + "column": 17, + "line": 20, + }, + }, + ], + "type": "branch", + }, + "6": { + "line": 20, + "loc": { + "end": { + "column": 3, + "line": 23, + }, + "start": { + "column": 33, + "line": 20, + }, + }, + "locations": [ + { + "end": { + "column": 3, + "line": 23, + }, + "start": { + "column": 33, + "line": 20, + }, + }, + ], + "type": "branch", + }, + "7": { + "line": 24, + "loc": { + "end": { + "column": 33, + "line": 24, + }, + "start": { + "column": 17, + "line": 24, + }, + }, + "locations": [ + { + "end": { + "column": 33, + "line": 24, + }, + "start": { + "column": 17, + "line": 24, + }, + }, + ], + "type": "branch", + }, + "8": { + "line": 24, + "loc": { + "end": { + "column": 3, + "line": 27, + }, + "start": { + "column": 33, + "line": 24, + }, + }, + "locations": [ + { + "end": { + "column": 3, + "line": 27, + }, + "start": { + "column": 33, + "line": 24, + }, + }, + ], + "type": "branch", + }, + "9": { + "line": 16, + "loc": { + "end": { + "column": 3, + "line": 19, + }, + "start": { + "column": 31, + "line": 16, + }, + }, + "locations": [ + { + "end": { + "column": 3, + "line": 19, + }, + "start": { + "column": 31, + "line": 16, + }, + }, + ], + "type": "branch", + }, + }, + "f": { + "0": 4, + }, + "fnMap": { + "0": { + "decl": { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 7, + "line": 6, + }, + }, + "line": 6, + "loc": { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 7, + "line": 6, + }, + }, + "name": "sum", + }, + }, + "path": "/src/multi-environment.ts", + "s": { + "0": 2, + "1": 2, + "10": 4, + "11": 0, + "12": 0, + "13": 0, + "14": 4, + "15": 4, + "16": 1, + "17": 1, + "18": 1, + "19": 3, + "2": 2, + "20": 0, + "21": 0, + "22": 0, + "23": 3, + "24": 1, + "25": 1, + "26": 1, + "27": 2, + "28": 2, + "29": 2, + "3": 2, + "30": 2, + "4": 2, + "5": 2, + "6": 4, + "7": 4, + "8": 4, + "9": 4, + }, + "statementMap": { + "0": { + "end": { + "column": 3, + "line": 1, + }, + "start": { + "column": 0, + "line": 1, + }, + }, + "1": { + "end": { + "column": 55, + "line": 2, + }, + "start": { + "column": 0, + "line": 2, + }, + }, + "10": { + "end": { + "column": 27, + "line": 11, + }, + "start": { + "column": 0, + "line": 11, + }, + }, + "11": { + "end": { + "column": 38, + "line": 12, + }, + "start": { + "column": 0, + "line": 12, + }, + }, + "12": { + "end": { + "column": 13, + "line": 13, + }, + "start": { + "column": 0, + "line": 13, + }, + }, + "13": { + "end": { + "column": 3, + "line": 14, + }, + "start": { + "column": 0, + "line": 14, + }, + }, + "14": { + "end": { + "column": 12, + "line": 15, + }, + "start": { + "column": 0, + "line": 15, + }, + }, + "15": { + "end": { + "column": 32, + "line": 16, + }, + "start": { + "column": 0, + "line": 16, + }, + }, + "16": { + "end": { + "column": 34, + "line": 17, + }, + "start": { + "column": 0, + "line": 17, + }, + }, + "17": { + "end": { + "column": 12, + "line": 18, + }, + "start": { + "column": 0, + "line": 18, + }, + }, + "18": { + "end": { + "column": 3, + "line": 19, + }, + "start": { + "column": 0, + "line": 19, + }, + }, + "19": { + "end": { + "column": 34, + "line": 20, + }, + "start": { + "column": 0, + "line": 20, + }, + }, + "2": { + "end": { + "column": 3, + "line": 3, + }, + "start": { + "column": 0, + "line": 3, + }, + }, + "20": { + "end": { + "column": 38, + "line": 21, + }, + "start": { + "column": 0, + "line": 21, + }, + }, + "21": { + "end": { + "column": 13, + "line": 22, + }, + "start": { + "column": 0, + "line": 22, + }, + }, + "22": { + "end": { + "column": 3, + "line": 23, + }, + "start": { + "column": 0, + "line": 23, + }, + }, + "23": { + "end": { + "column": 34, + "line": 24, + }, + "start": { + "column": 0, + "line": 24, + }, + }, + "24": { + "end": { + "column": 34, + "line": 25, + }, + "start": { + "column": 0, + "line": 25, + }, + }, + "25": { + "end": { + "column": 13, + "line": 26, + }, + "start": { + "column": 0, + "line": 26, + }, + }, + "26": { + "end": { + "column": 3, + "line": 27, + }, + "start": { + "column": 0, + "line": 27, + }, + }, + "27": { + "end": { + "column": 0, + "line": 28, + }, + "start": { + "column": 0, + "line": 28, + }, + }, + "28": { + "end": { + "column": 61, + "line": 29, + }, + "start": { + "column": 0, + "line": 29, + }, + }, + "29": { + "end": { + "column": 14, + "line": 30, + }, + "start": { + "column": 0, + "line": 30, + }, + }, + "3": { + "end": { + "column": 40, + "line": 4, + }, + "start": { + "column": 0, + "line": 4, + }, + }, + "30": { + "end": { + "column": 1, + "line": 31, + }, + "start": { + "column": 0, + "line": 31, + }, + }, + "4": { + "end": { + "column": 0, + "line": 5, + }, + "start": { + "column": 0, + "line": 5, + }, + }, + "5": { + "end": { + "column": 43, + "line": 6, + }, + "start": { + "column": 0, + "line": 6, + }, + }, + "6": { + "end": { + "column": 4, + "line": 7, + }, + "start": { + "column": 0, + "line": 7, + }, + }, + "7": { + "end": { + "column": 64, + "line": 8, + }, + "start": { + "column": 0, + "line": 8, + }, + }, + "8": { + "end": { + "column": 35, + "line": 9, + }, + "start": { + "column": 0, + "line": 9, + }, + }, + "9": { + "end": { + "column": 5, + "line": 10, + }, + "start": { + "column": 0, + "line": 10, + }, + }, + }, + }, "/src/multi-suite.ts": { "all": false, "b": { diff --git a/test/coverage-test/coverage-report-tests/generic.report.test.ts b/test/coverage-test/coverage-report-tests/generic.report.test.ts index 350c23c3677e..fd2102756671 100644 --- a/test/coverage-test/coverage-report-tests/generic.report.test.ts +++ b/test/coverage-test/coverage-report-tests/generic.report.test.ts @@ -117,3 +117,25 @@ test('virtual files should be excluded', () => { expect(file).not.toContain('\x00') } }) + +test('multi environment coverage is merged correctly', async () => { + const coverageJson = await readCoverageJson() + const coverageMap = libCoverage.createCoverageMap(coverageJson as any) + const fileCoverage = coverageMap.fileCoverageFor('/src/multi-environment.ts') + const lineCoverage = fileCoverage.getLineCoverage() + + // Condition not covered by any test + expect(lineCoverage[13]).toBe(0) + + // Condition covered by SSR test but not by Web + expect(lineCoverage[18]).toBe(1) + + // Condition not covered by any test + expect(lineCoverage[22]).toBe(0) + + // Condition covered by Web test but not by SSR + expect(lineCoverage[26]).toBe(1) + + // Condition covered by both tests + expect(lineCoverage[30]).toBe(2) +}) diff --git a/test/coverage-test/custom-provider.ts b/test/coverage-test/custom-provider.ts index eae1bbcbe4ad..2a8f7d6e60a7 100644 --- a/test/coverage-test/custom-provider.ts +++ b/test/coverage-test/custom-provider.ts @@ -43,6 +43,7 @@ class CustomCoverageProvider implements CoverageProvider { options!: ResolvedCoverageOptions calls: Set = new Set() + coverageReports: Set = new Set() transformedFiles: Set = new Set() initialize(ctx: Vitest) { @@ -56,7 +57,16 @@ class CustomCoverageProvider implements CoverageProvider { } onAfterSuiteRun(meta: AfterSuiteRunMeta) { - this.calls.add(`onAfterSuiteRun with ${JSON.stringify(meta)}`) + // Do not include coverage info here, as order of tests is not guaranteed + this.calls.add('onAfterSuiteRun') + + // Keep coverage info separate from calls and ignore its order + this.coverageReports.add(JSON.stringify({ + ...meta, + + // Project name keeps changing so let's simply check that its present + projectName: meta.projectName && typeof meta.projectName === 'string', + })) } reportCoverage(reportContext?: ReportContext) { @@ -64,6 +74,7 @@ class CustomCoverageProvider implements CoverageProvider { const jsonReport = JSON.stringify({ calls: Array.from(this.calls.values()), + coverageReports: Array.from(this.coverageReports.values()).sort(), transformedFiles: Array.from(this.transformedFiles.values()).sort(), }, null, 2) diff --git a/test/coverage-test/package.json b/test/coverage-test/package.json index a2ab46d6e726..2a74f2524ee9 100644 --- a/test/coverage-test/package.json +++ b/test/coverage-test/package.json @@ -11,6 +11,7 @@ "test:types": "vitest --typecheck.only --run --reporter verbose" }, "devDependencies": { + "@ampproject/remapping": "^2.2.1", "@types/istanbul-lib-coverage": "^2.0.4", "@vitejs/plugin-vue": "latest", "@vitest/browser": "workspace:*", diff --git a/test/coverage-test/src/multi-environment.ts b/test/coverage-test/src/multi-environment.ts new file mode 100644 index 000000000000..e151f08d74cc --- /dev/null +++ b/test/coverage-test/src/multi-environment.ts @@ -0,0 +1,31 @@ +/** + * The variable below is modified by custom Vite plugin + */ +export const padding = 'default-padding' + +export function sum(a: number, b: number) { + /* + * These if-branches should show correctly on coverage report. + * Otherwise source maps are off. + */ + if (a === 8 && b === 9) { + // This is not covered by any test + return 17 + } + // Comment + else if (a === 2 && b === 2) { + // This is covered by SSR test + return 4 + } + else if (a === 11 && b === 22) { + // This is not covered by any test + return 33 + } + else if (a === 10 && b === 23) { + // This is covered by Web test + return 33 + } + + // This is covered by SSR and Web test, should show 2x hits + return a + b +} diff --git a/test/coverage-test/test/ssr.test.ts b/test/coverage-test/test/ssr.test.ts new file mode 100644 index 000000000000..039bf7c69a3d --- /dev/null +++ b/test/coverage-test/test/ssr.test.ts @@ -0,0 +1,9 @@ +// @vitest-environment node + +import { expect, test } from 'vitest' +import { sum } from '../src/multi-environment' + +test('runs on server', () => { + expect(sum(2, 2)).toBe(4) + expect(sum(100, 200)).toBe(300) +}) diff --git a/test/coverage-test/test/web.test.ts b/test/coverage-test/test/web.test.ts new file mode 100644 index 000000000000..6740d9bd0f89 --- /dev/null +++ b/test/coverage-test/test/web.test.ts @@ -0,0 +1,9 @@ +// @vitest-environment jsdom + +import { expect, test } from 'vitest' +import { sum } from '../src/multi-environment' + +test('runs on client', () => { + expect(sum(1, 2)).toBe(3) + expect(sum(10, 23)).toBe(33) +}) diff --git a/test/coverage-test/vitest.config.ts b/test/coverage-test/vitest.config.ts index 93d04fbfacce..abf3c1e21afd 100644 --- a/test/coverage-test/vitest.config.ts +++ b/test/coverage-test/vitest.config.ts @@ -1,12 +1,40 @@ import { resolve } from 'pathe' import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' +import MagicString from 'magic-string' +import remapping from '@ampproject/remapping' const provider = process.argv[1 + process.argv.indexOf('--provider')] export default defineConfig({ plugins: [ vue(), + /* + * Transforms `multi-environment.ts` differently based on test environment (JSDOM/Node) + * so that there are multiple different source maps for a single file. + * This causes a case where coverage report is incorrect if sourcemaps are not picked based on transform mode. + */ + { + name: 'vitest-custom-multi-transform', + enforce: 'pre', + transform(code, id, options) { + if (id.includes('src/multi-environment')) { + const ssr = options?.ssr || false + const transforMode = `transformMode is ${ssr ? 'ssr' : 'csr'}` + const padding = '\n*****'.repeat(ssr ? 0 : 15) + + const transformed = new MagicString(code) + transformed.replace('\'default-padding\'', `\`${transforMode} ${padding}\``) + + const map = remapping( + [transformed.generateMap({ hires: true }), this.getCombinedSourcemap() as any], + () => null, + ) as any + + return { code: transformed.toString(), map } + } + }, + }, { // Simulates Vite's virtual files: https://vitejs.dev/guide/api-plugin.html#virtual-modules-convention name: 'vitest-custom-virtual-files', @@ -45,6 +73,7 @@ export default defineConfig({ customProviderModule: provider === 'custom' ? 'custom-provider' : undefined, include: ['src/**'], clean: true, + reportOnFailure: true, reporter: [ 'text', ['html'], diff --git a/test/workspaces/coverage-report-tests/check-coverage.test.ts b/test/workspaces/coverage-report-tests/check-coverage.test.ts index 2f089fbca30b..704d6a978a43 100644 --- a/test/workspaces/coverage-report-tests/check-coverage.test.ts +++ b/test/workspaces/coverage-report-tests/check-coverage.test.ts @@ -1,5 +1,4 @@ import { existsSync, readFileSync } from 'node:fs' -import { normalize } from 'node:path' import { expect, test } from 'vitest' import libCoverage from 'istanbul-lib-coverage' import { resolve } from 'pathe' @@ -12,7 +11,7 @@ test('coverage exists', () => { test('file coverage summary matches', () => { const coverageJson = JSON.parse(readFileSync('./coverage/coverage-final.json', 'utf-8')) const coverageMap = libCoverage.createCoverageMap(coverageJson) - const fileCoverage = coverageMap.fileCoverageFor(normalize(resolve('./src/math.ts'))) + const fileCoverage = coverageMap.fileCoverageFor(resolve('./src/math.ts')) // There should be 1 uncovered branch and 1 uncovered function. See math.ts. const { branches, functions } = fileCoverage.toSummary() @@ -20,3 +19,25 @@ test('file coverage summary matches', () => { expect(branches.total - branches.covered).toBe(1) expect(functions.total - functions.covered).toBe(1) }) + +test('coverage of file transformed by multiple plugins is merged correctly', async () => { + const coverageJson = JSON.parse(readFileSync('./coverage/coverage-final.json', 'utf-8')) + const coverageMap = libCoverage.createCoverageMap(coverageJson) + const fileCoverage = coverageMap.fileCoverageFor(resolve('./space-multi-transform/src/multi-transform.ts')) + const lineCoverage = fileCoverage.getLineCoverage() + + // Condition not covered by any test + expect(lineCoverage[13]).toBe(0) + + // Condition covered by Project #1 but not by Project #2 + expect(lineCoverage[18]).toBe(1) + + // Condition not covered by any test + expect(lineCoverage[22]).toBe(0) + + // Condition covered by Project #2 but not by Project #1 + expect(lineCoverage[26]).toBe(1) + + // Condition covered by both tests + expect(lineCoverage[30]).toBe(2) +}) diff --git a/test/workspaces/globalTest.ts b/test/workspaces/globalTest.ts index 17792196fbda..dccb99c98c08 100644 --- a/test/workspaces/globalTest.ts +++ b/test/workspaces/globalTest.ts @@ -6,9 +6,9 @@ export async function teardown() { try { assert.ok(results.success) - assert.equal(results.numTotalTestSuites, 7) - assert.equal(results.numTotalTests, 8) - assert.equal(results.numPassedTests, 8) + assert.equal(results.numTotalTestSuites, 9) + assert.equal(results.numTotalTests, 10) + assert.equal(results.numPassedTests, 10) const shared = results.testResults.filter((r: any) => r.name.includes('space_shared/test.spec.ts')) diff --git a/test/workspaces/package.json b/test/workspaces/package.json index 10bc7a91f8bf..064db8df4f8d 100644 --- a/test/workspaces/package.json +++ b/test/workspaces/package.json @@ -8,6 +8,7 @@ "test:coverage": "vitest run --root coverage-report-tests" }, "devDependencies": { + "@ampproject/remapping": "^2.2.1", "@types/istanbul-lib-coverage": "^2.0.4", "istanbul-lib-coverage": "^3.2.0", "jsdom": "latest", diff --git a/test/workspaces/space-multi-transform/src/multi-transform.ts b/test/workspaces/space-multi-transform/src/multi-transform.ts new file mode 100644 index 000000000000..ee8748ba9f2e --- /dev/null +++ b/test/workspaces/space-multi-transform/src/multi-transform.ts @@ -0,0 +1,31 @@ +/** + * The variable below is modified by custom Vite plugin + */ +export const padding = 'default-padding' + +export function run(name: string) { + /* + * These if-branches should show correctly on coverage report. + * Otherwise source maps are off. + */ + if (name === 'not-covered') { + // This is not covered by any test + return 0 + } + // Comment + else if (name === 'project-1') { + // This is covered by Project #1 + return 1 + } + else if (name === 'not-covered-2') { + // This is not covered by any test + return 0 + } + else if (name === 'project-2') { + // This is covered by Project #2 + return 2 + } + + // This is covered by both projects, should show 2x hits + return 3 +} diff --git a/test/workspaces/space-multi-transform/test/project-1.test.ts b/test/workspaces/space-multi-transform/test/project-1.test.ts new file mode 100644 index 000000000000..e36ba28c0484 --- /dev/null +++ b/test/workspaces/space-multi-transform/test/project-1.test.ts @@ -0,0 +1,9 @@ +import { expect, test } from 'vitest' + +import { run } from '../src/multi-transform' + +test('cover some branches', () => { + expect(run('project-1')).toBe(1) + + expect(run('last branch')).toBe(3) +}) diff --git a/test/workspaces/space-multi-transform/test/project-2.test.ts b/test/workspaces/space-multi-transform/test/project-2.test.ts new file mode 100644 index 000000000000..ba9b85713d61 --- /dev/null +++ b/test/workspaces/space-multi-transform/test/project-2.test.ts @@ -0,0 +1,9 @@ +import { expect, test } from 'vitest' + +import { run } from '../src/multi-transform' + +test('cover some branches', () => { + expect(run('project-2')).toBe(2) + + expect(run('last branch')).toBe(3) +}) diff --git a/test/workspaces/vitest.config.ts b/test/workspaces/vitest.config.ts index 5a9cf2e2004e..7e7b028bf25d 100644 --- a/test/workspaces/vitest.config.ts +++ b/test/workspaces/vitest.config.ts @@ -10,6 +10,7 @@ export default defineConfig({ test: { coverage: { enabled: true, + provider: 'istanbul', }, reporters: ['default', 'json'], outputFile: './results.json', diff --git a/test/workspaces/vitest.workspace.ts b/test/workspaces/vitest.workspace.ts index 4c31ef3957cb..08876bcbd56e 100644 --- a/test/workspaces/vitest.workspace.ts +++ b/test/workspaces/vitest.workspace.ts @@ -1,4 +1,7 @@ import { defineWorkspace } from 'vitest/config' +import MagicString from 'magic-string' +import remapping from '@ampproject/remapping' +import type { Plugin } from 'vite' export default defineWorkspace([ './space_2/*', @@ -19,4 +22,45 @@ export default defineWorkspace([ setupFiles: ['./setup.node.ts'], }, }, + + // These two projects run on same environment but still transform + // a single file differently due to Vite plugins + { + plugins: [customPlugin(0)], + test: { + name: 'Project with custom plugin #1', + environment: 'node', + include: ['./space-multi-transform/test/project-1.test.ts'], + }, + }, + { + plugins: [customPlugin(15)], + test: { + name: 'Project with custom plugin #2', + environment: 'node', + include: ['./space-multi-transform/test/project-2.test.ts'], + }, + }, ]) + +function customPlugin(offset: number): Plugin { + return { + name: 'vitest-custom-multi-transform', + enforce: 'pre', + transform(code, id) { + if (id.includes('space-multi-transform/src/multi-transform.ts')) { + const padding = '\n*****'.repeat(offset) + + const transformed = new MagicString(code) + transformed.replace('\'default-padding\'', `\`${padding}\``) + + const map = remapping( + [transformed.generateMap({ hires: true }), this.getCombinedSourcemap() as any], + () => null, + ) as any + + return { code: transformed.toString(), map } + } + }, + } +}