diff --git a/packages/vitest/src/node/coverage.ts b/packages/vitest/src/node/coverage.ts index 052e7c613..77be48fe8 100644 --- a/packages/vitest/src/node/coverage.ts +++ b/packages/vitest/src/node/coverage.ts @@ -221,19 +221,22 @@ export class BaseCoverageProvider { include: string[], root: string, ): Promise { + const tested = new Set(testedFiles.map((file) => slash(file))) + let includedFiles = await glob(include, { cwd: root, - ignore: [...this.options.exclude, ...testedFiles.map((file) => slash(file))], + ignore: this.options.exclude, absolute: true, dot: true, onlyFiles: true, }) // Run again through picomatch as tinyglobby's exclude pattern is different ({ "exclude": ["math"] } should ignore "src/math.ts") - includedFiles = includedFiles.filter((file) => this.isIncluded(file, root)) + includedFiles = includedFiles.filter((file) => !tested.has(file) && this.isIncluded(file, root)) if (this.changedFiles) { - includedFiles = this.changedFiles.filter((file) => includedFiles.includes(file)) + const included = new Set(includedFiles) + includedFiles = this.changedFiles.filter((file) => included.has(file)) } return includedFiles.map((file) => slash(path.resolve(root, file))) diff --git a/test/coverage-test/test/include-exclude.unit.test.ts b/test/coverage-test/test/include-exclude.unit.test.ts index 9d482dc8f..1af9cfbe6 100644 --- a/test/coverage-test/test/include-exclude.unit.test.ts +++ b/test/coverage-test/test/include-exclude.unit.test.ts @@ -1,7 +1,8 @@ import type { BaseCoverageProvider, CoverageOptions } from 'vitest/node' -import { mkdirSync, rmSync } from 'node:fs' -import { join, resolve, sep } from 'node:path' +import { mkdirSync, rmSync, writeFileSync } from 'node:fs' +import { dirname, join, relative, resolve, sep } from 'node:path' import { Writable } from 'node:stream' +import { normalize } from 'pathe' import { expect, onTestFinished, test } from 'vitest' import { createVitest } from 'vitest/node' @@ -164,7 +165,35 @@ test('files with almost matching name, outside project when allowExternal: false expect(isIncluded(resolve(parent, './something-else/src/three.ts'))).toBe(false) }) +test('tested files with glob characters in their path are not uncovered', async () => { + const root = resolve(process.cwd(), `vitest-test-${crypto.randomUUID()}`) + const tested = resolve(root, 'src/(app)/[id]/{page}.ts') + const untested = resolve(root, 'src/(app)/[id]/untested.ts') + + mkdirSync(dirname(tested), { recursive: true }) + writeFileSync(tested, '') + writeFileSync(untested, '') + onTestFinished(() => { + rmSync(root, { recursive: true, force: true }) + }) + + const provider = await initProvider({ include: ['src/**'], root }) + const untestedFiles = await provider.getUntestedFiles([normalize(tested)]) + + expect(untestedFiles.map((file) => relative(root, file).replaceAll('\\', '/'))).toEqual([ + 'src/(app)/[id]/untested.ts', + ]) +}) + async function init(options: Partial & { testInclude?: string[]; root?: string }) { + const provider = await initProvider(options) + + return (path: string) => provider.isIncluded(resolve(process.cwd(), path)) +} + +async function initProvider( + options: Partial & { testInclude?: string[]; root?: string }, +) { const vitest = await createVitest( 'test', { @@ -184,7 +213,5 @@ async function init(options: Partial & { testInclude?: string[] onTestFinished(() => vitest.close()) await vitest.standalone() - const provider = vitest.coverageProvider as unknown as BaseCoverageProvider - - return (path: string) => provider.isIncluded(resolve(process.cwd(), path)) + return vitest.coverageProvider as unknown as BaseCoverageProvider }