diff --git a/docs/config/index.md b/docs/config/index.md index 35dc7d16be18..94eff5739fe1 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -985,6 +985,15 @@ Since Vitest 0.31.0, you can check your coverage report in Vitest UI: check [Vit Generate coverage report even when tests fail. +#### coverage.allowExternal + +- **Type:** `boolean` +- **Default:** `false` +- **Available for providers:** `'v8' | 'istanbul'` +- **CLI:** `--coverage.allowExternal`, `--coverage.allowExternal=false` + +Collect coverage of files outside the [project `root`](https://vitest.dev/config/#root). + #### coverage.skipFull - **Type:** `boolean` diff --git a/packages/coverage-istanbul/src/provider.ts b/packages/coverage-istanbul/src/provider.ts index 84af13763de9..89cc3752a9c2 100644 --- a/packages/coverage-istanbul/src/provider.ts +++ b/packages/coverage-istanbul/src/provider.ts @@ -23,6 +23,7 @@ interface TestExclude { exclude?: string | string[] extension?: string | string[] excludeNodeModules?: boolean + relativePath?: boolean }): { shouldInstrument(filePath: string): boolean glob(cwd: string): Promise @@ -79,6 +80,7 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co exclude: [...defaultExclude, ...defaultInclude, ...this.options.exclude], excludeNodeModules: true, extension: this.options.extension, + relativePath: !this.options.allowExternal, }) } diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts index 7733007804e3..7b3c28fa3095 100644 --- a/packages/coverage-v8/src/provider.ts +++ b/packages/coverage-v8/src/provider.ts @@ -30,6 +30,7 @@ interface TestExclude { exclude?: string | string[] extension?: string | string[] excludeNodeModules?: boolean + relativePath?: boolean }): { shouldInstrument(filePath: string): boolean glob(cwd: string): Promise @@ -79,6 +80,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage exclude: [...defaultExclude, ...defaultInclude, ...this.options.exclude], excludeNodeModules: true, extension: this.options.extension, + relativePath: !this.options.allowExternal, }) } diff --git a/packages/vitest/src/defaults.ts b/packages/vitest/src/defaults.ts index 518324bfe8e1..2d696fb216fc 100644 --- a/packages/vitest/src/defaults.ts +++ b/packages/vitest/src/defaults.ts @@ -39,6 +39,7 @@ export const coverageConfigDefaults: ResolvedCoverageOptions = { reportOnFailure: false, reporter: [['text', {}], ['html', {}], ['clover', {}], ['json', {}]], extension: ['.js', '.cjs', '.mjs', '.ts', '.mts', '.cts', '.tsx', '.jsx', '.vue', '.svelte'], + allowExternal: false, } export const fakeTimersDefaults = { diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index b5f04b9f4887..ae10f25d3858 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -279,7 +279,7 @@ export function resolveConfig( ?? resolve(resolved.root, file), ), ) - resolved.coverage.exclude.push(...resolved.setupFiles.map(file => relative(resolved.root, file))) + resolved.coverage.exclude.push(...resolved.setupFiles.map(file => `${resolved.coverage.allowExternal ? '**/' : ''}${relative(resolved.root, file)}`)) resolved.forceRerunTriggers = [ ...resolved.forceRerunTriggers, diff --git a/packages/vitest/src/types/coverage.ts b/packages/vitest/src/types/coverage.ts index 11c14578b0c5..e5caf29cd2f9 100644 --- a/packages/vitest/src/types/coverage.ts +++ b/packages/vitest/src/types/coverage.ts @@ -78,6 +78,7 @@ type FieldsWithDefaultValues = | 'exclude' | 'extension' | 'reportOnFailure' + | 'allowExternal' export type ResolvedCoverageOptions = & CoverageOptions @@ -216,6 +217,13 @@ export interface BaseCoverageOptions { * @default false */ reportOnFailure?: boolean + + /** + * Collect coverage of files outside the project `root`. + * + * @default false + */ + allowExternal?: boolean } export interface CoverageIstanbulOptions extends BaseCoverageOptions { diff --git a/test/coverage-test/coverage-report-tests/allow-external.test.ts b/test/coverage-test/coverage-report-tests/allow-external.test.ts new file mode 100644 index 000000000000..fecca62077c0 --- /dev/null +++ b/test/coverage-test/coverage-report-tests/allow-external.test.ts @@ -0,0 +1,20 @@ +import fs from 'node:fs' +import { expect, test } from 'vitest' + +const allowExternal = import.meta.env.VITE_COVERAGE_ALLOW_EXTERNAL + +test.skipIf(!allowExternal)('{ allowExternal: true } includes files outside project root', async () => { + expect(fs.existsSync('./coverage/test-utils/fixtures/math.ts.html')).toBe(true) + + // Files inside project root should always be included + expect(fs.existsSync('./coverage/coverage-test/src/utils.ts.html')).toBe(true) +}) + +test.skipIf(allowExternal)('{ allowExternal: false } excludes files outside project root', async () => { + expect(fs.existsSync('./coverage/test-utils/fixtures/math.ts.html')).toBe(false) + expect(fs.existsSync('./test-utils/fixtures/math.ts.html')).toBe(false) + expect(fs.existsSync('./fixtures/math.ts.html')).toBe(false) + + // Files inside project root should always be included + expect(fs.existsSync('./coverage/utils.ts.html')).toBe(true) +}) diff --git a/test/coverage-test/option-tests/allow-external.test.ts b/test/coverage-test/option-tests/allow-external.test.ts new file mode 100644 index 000000000000..95299ebf5d6b --- /dev/null +++ b/test/coverage-test/option-tests/allow-external.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from 'vitest' + +import { multiply } from '../src/utils' +import * as ExternalMath from '../../test-utils/fixtures/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/package.json b/test/coverage-test/package.json index a699b16448cb..16525ea6e1bb 100644 --- a/test/coverage-test/package.json +++ b/test/coverage-test/package.json @@ -2,11 +2,12 @@ "name": "@vitest/test-coverage", "private": true, "scripts": { - "test": "pnpm test:v8 && pnpm test:istanbul && pnpm test:custom && pnpm test:browser && pnpm test:types", + "test": "pnpm test:v8 && pnpm test:istanbul && pnpm test:custom && pnpm test:browser && pnpm test:options && pnpm test:types", "test:v8": "node ./testing.mjs --provider v8", "test:custom": "node ./testing.mjs --provider custom", "test:istanbul": "node ./testing.mjs --provider istanbul", "test:browser": "node ./testing.mjs --browser --provider istanbul", + "test:options": "node ./testing-options.mjs", "test:types": "vitest typecheck --run --reporter verbose" }, "devDependencies": { diff --git a/test/coverage-test/test/configuration-options.test-d.ts b/test/coverage-test/test/configuration-options.test-d.ts index f4b0731a84e3..a0f42c0d4c62 100644 --- a/test/coverage-test/test/configuration-options.test-d.ts +++ b/test/coverage-test/test/configuration-options.test-d.ts @@ -103,6 +103,7 @@ test('provider module', () => { reporter: [['html', {}], ['json', { file: 'string' }]], reportsDirectory: 'string', reportOnFailure: true, + allowExternal: true, } }, clean(_: boolean) {}, diff --git a/test/coverage-test/testing-options.mjs b/test/coverage-test/testing-options.mjs new file mode 100644 index 000000000000..f86568c0720e --- /dev/null +++ b/test/coverage-test/testing-options.mjs @@ -0,0 +1,69 @@ +import { startVitest } from 'vitest/node' + +/** @type {Record>[]} */ +const testCases = [ + { + testConfig: { + name: 'allowExternal: true', + include: ['option-tests/allow-external.test.ts'], + coverage: { + allowExternal: true, + include: ['**/src/**', '**/test-utils/fixtures/**'], + reporter: 'html', + }, + }, + assertionConfig: { + include: ['coverage-report-tests/allow-external.test.ts'], + env: { VITE_COVERAGE_ALLOW_EXTERNAL: true }, + }, + }, + { + testConfig: { + name: 'allowExternal: false', + include: ['option-tests/allow-external.test.ts'], + coverage: { + allowExternal: false, + include: ['**/src/**', '**/test-utils/fixtures/**'], + reporter: 'html', + }, + }, + assertionConfig: { + include: ['coverage-report-tests/allow-external.test.ts'], + }, + }, +] + +for (const provider of ['v8', 'istanbul']) { + for (const { testConfig, assertionConfig } of testCases) { + // Run test case + await startVitest('test', ['option-tests/'], { + config: false, + watch: false, + ...testConfig, + name: `${provider} - ${testConfig.name}`, + coverage: { + enabled: true, + clean: true, + provider, + ...testConfig.coverage, + }, + }) + + checkExit() + + // Check generated coverage report + await startVitest('test', ['coverage-report-tests'], { + config: false, + watch: false, + ...assertionConfig, + name: `${provider} - assert ${testConfig.name}`, + }) + + checkExit() + } +} + +function checkExit() { + if (process.exitCode) + process.exit(process.exitCode) +} diff --git a/test/test-utils/fixtures/math.ts b/test/test-utils/fixtures/math.ts new file mode 100644 index 000000000000..0859841e5814 --- /dev/null +++ b/test/test-utils/fixtures/math.ts @@ -0,0 +1,7 @@ +export function sum(a: number, b: number) { + return a + b +} + +export function multiply(a: number, b: number) { + return a * b +}