From 47af5106d670944bf0673c62cf1bf8ede653f009 Mon Sep 17 00:00:00 2001 From: claneo Date: Wed, 31 Dec 2025 16:05:54 +0800 Subject: [PATCH 1/3] fix: coverage cannot includes parent folders --- .gitignore | 1 + packages/core/package.json | 23 ++-- packages/core/src/config.ts | 1 - packages/core/src/coverage/generate.ts | 7 +- .../tests/__snapshots__/config.test.ts.snap | 1 - .../core/__snapshots__/rstest.test.ts.snap | 3 - packages/core/tests/coverage/include.test.ts | 125 ++++++++++++++++++ pnpm-lock.yaml | 3 + website/docs/en/config/test/coverage.mdx | 1 - website/docs/zh/config/test/coverage.mdx | 1 - 10 files changed, 145 insertions(+), 21 deletions(-) create mode 100644 packages/core/tests/coverage/include.test.ts diff --git a/.gitignore b/.gitignore index f0fdddf58..eb31ce03c 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ fixtures-test/ fixtures-test-*/ .rslib/ !packages/core/src/coverage +!packages/core/tests/coverage tests-dist/ .vscode-test/ packages/vscode/*.vsix diff --git a/packages/core/package.json b/packages/core/package.json index b92f39bb5..b2415f1f8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -52,16 +52,11 @@ "test": "npx rstest --globals" }, "dependencies": { - "@types/chai": "^5.2.3", "@rsbuild/core": "1.7.1", + "@types/chai": "^5.2.3", "tinypool": "^1.1.1" }, "devDependencies": { - "chai": "^5.3.3", - "pathe": "^2.0.3", - "birpc": "2.9.0", - "@vitest/expect": "^3.2.4", - "@vitest/snapshot": "^3.2.4", "@babel/code-frame": "^7.27.1", "@jridgewell/trace-mapping": "0.3.31", "@microsoft/api-extractor": "^7.55.2", @@ -69,29 +64,35 @@ "@rstest/tsconfig": "workspace:*", "@sinonjs/fake-timers": "^15.1.0", "@types/babel__code-frame": "^7.0.6", - "@types/istanbul-reports": "^3.0.4", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-lib-report": "^3.0.3", + "@types/istanbul-reports": "^3.0.4", "@types/jsdom": "^21.1.7", + "@types/picomatch": "^4.0.2", "@types/sinonjs__fake-timers": "^8.1.5", "@types/source-map-support": "^0.5.10", - "@types/picomatch": "^4.0.2", + "@vitest/expect": "^3.2.4", + "@vitest/snapshot": "^3.2.4", + "birpc": "2.9.0", "cac": "^6.7.14", + "chai": "^5.3.3", "chokidar": "^4.0.3", "happy-dom": "^20.0.11", "jest-diff": "^30.2.0", "jsdom": "^26.1.0", - "webpack-license-plugin": "^4.5.1", + "memfs": "^4.51.1", + "pathe": "^2.0.3", "picocolors": "^1.1.1", + "picomatch": "^4.0.3", "pretty-format": "^30.2.0", "rslog": "^1.3.2", "source-map-support": "^0.5.21", - "std-env": "^3.10.0", "stacktrace-parser": "0.1.11", + "std-env": "^3.10.0", "strip-ansi": "^7.1.2", "tinyglobby": "^0.2.15", "tinyspy": "^4.0.4", - "picomatch": "^4.0.3" + "webpack-license-plugin": "^4.5.1" }, "peerDependencies": { "happy-dom": "*", diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 8f92882e6..feda71994 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -175,7 +175,6 @@ const createDefaultConfig = (): NormalizedConfig => ({ coverage: { exclude: [ '**/node_modules/**', - '**/[.]*', '**/dist/**', '**/test/**', '**/__tests__/**', diff --git a/packages/core/src/coverage/generate.ts b/packages/core/src/coverage/generate.ts index 47e44f3ac..e49c23cfa 100644 --- a/packages/core/src/coverage/generate.ts +++ b/packages/core/src/coverage/generate.ts @@ -1,5 +1,5 @@ import { normalize } from 'pathe'; -import { glob, isDynamicPattern } from 'tinyglobby'; +import { type GlobOptions, glob, isDynamicPattern } from 'tinyglobby'; import type { RstestContext, TestFileResult } from '../types'; import type { CoverageMap, @@ -8,9 +8,10 @@ import type { } from '../types/coverage'; import { logger } from '../utils'; -const getIncludedFiles = async ( +export const getIncludedFiles = async ( coverage: CoverageOptions, rootPath: string, + fs?: GlobOptions['fs'], ): Promise => { // fix issue with glob not working correctly when exclude path was not in the cwd const ignoredPatterns = coverage.exclude?.filter( @@ -24,8 +25,8 @@ const getIncludedFiles = async ( absolute: true, onlyFiles: true, ignore: ignoredPatterns, - dot: true, expandDirectories: false, + fs, }); // 'a.ts' should match 'src/a.ts' diff --git a/packages/core/tests/__snapshots__/config.test.ts.snap b/packages/core/tests/__snapshots__/config.test.ts.snap index 7d6487371..7cd3f6545 100644 --- a/packages/core/tests/__snapshots__/config.test.ts.snap +++ b/packages/core/tests/__snapshots__/config.test.ts.snap @@ -9,7 +9,6 @@ exports[`mergeRstestConfig > should merge config correctly with default config 1 "enabled": false, "exclude": [ "**/node_modules/**", - "**/[.]*", "**/dist/**", "**/test/**", "**/__tests__/**", diff --git a/packages/core/tests/core/__snapshots__/rstest.test.ts.snap b/packages/core/tests/core/__snapshots__/rstest.test.ts.snap index f3832f4d5..be7c5f9dc 100644 --- a/packages/core/tests/core/__snapshots__/rstest.test.ts.snap +++ b/packages/core/tests/core/__snapshots__/rstest.test.ts.snap @@ -9,7 +9,6 @@ exports[`rstest context > should generate rstest context correctly 1`] = ` "enabled": false, "exclude": [ "**/node_modules/**", - "**/[.]*", "**/dist/**", "**/test/**", "**/__tests__/**", @@ -88,7 +87,6 @@ exports[`rstest context > should generate rstest context correctly with multiple "enabled": false, "exclude": [ "**/node_modules/**", - "**/[.]*", "**/dist/**", "**/test/**", "**/__tests__/**", @@ -170,7 +168,6 @@ exports[`rstest context > should generate rstest context correctly with multiple "enabled": false, "exclude": [ "**/node_modules/**", - "**/[.]*", "**/dist/**", "**/test/**", "**/__tests__/**", diff --git a/packages/core/tests/coverage/include.test.ts b/packages/core/tests/coverage/include.test.ts new file mode 100644 index 000000000..cbb0073b4 --- /dev/null +++ b/packages/core/tests/coverage/include.test.ts @@ -0,0 +1,125 @@ +import path from 'node:path'; +import type { GlobOptions } from 'tinyglobby'; +import { withDefaultConfig } from '../../src/config'; +import { getIncludedFiles } from '../../src/coverage/generate'; + +describe('getIncludedFiles', () => { + let defaultExclude: string[] = []; + let memfs!: GlobOptions['fs']; + beforeAll(async () => { + const { fs } = await import('memfs'); + defaultExclude = withDefaultConfig({}).coverage.exclude; + memfs = fs as any; + + [ + '/apps/a.ts', + '/apps/b.js', + '/apps/.c.ts', + '/apps/node_modules/a.ts', + '/apps/dist/a.ts', + '/apps/test/a.ts', + '/apps/__tests__/a.ts', + '/apps/__mocks__/a.ts', + '/apps/a.d.ts', + '/apps/a.test.ts', + '/apps/a.spec.ts', + '/apps/a.test.js', + '/apps/a.spec.js', + '/apps/a.test.mts', + '/apps/a.spec.mts', + '/apps/a.test.cts', + '/apps/a.spec.cts', + '/apps/a.test.tsx', + '/apps/a.spec.tsx', + '/packages/a.ts', + '/packages/b.js', + '/packages/.c.ts', + ].forEach((file) => { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, ''); + }); + }); + + it('should include visible files by default', async () => { + expect( + await getIncludedFiles( + { + include: ['**/*.{js,ts}', '../packages/*.{js,ts}'], + exclude: [...defaultExclude], + }, + '/apps', + memfs, + ), + ).toMatchInlineSnapshot(` + [ + "/apps/a.ts", + "/apps/b.js", + "/packages/a.ts", + "/packages/b.js", + ] + `); + }); + + it('should include hidden files if explicitly specified', async () => { + expect( + await getIncludedFiles( + { + include: ['**/*.{js,ts}', '**/.c.ts'], + exclude: [...defaultExclude], + }, + '/apps', + memfs, + ), + ).toMatchInlineSnapshot(` + [ + "/apps/.c.ts", + "/apps/a.ts", + "/apps/b.js", + ] + `); + }); + + it('should exclude node_modules, dist, test, __tests__, __mocks__ by default', async () => { + expect( + await getIncludedFiles( + { + include: ['**/*'], + exclude: [...defaultExclude], + }, + '/apps', + memfs, + ), + ).toMatchInlineSnapshot(` + [ + "/apps/a.ts", + "/apps/b.js", + ] + `); + }); + + it('should exclude .d.ts files by default', async () => { + expect( + await getIncludedFiles( + { + include: ['**/*.d.ts'], + exclude: [...defaultExclude], + }, + '/apps', + memfs, + ), + ).toMatchInlineSnapshot('[]'); + }); + + it('should exclude test and spec files by default', async () => { + expect( + await getIncludedFiles( + { + include: ['**/*.{test,spec}.*'], + exclude: [...defaultExclude], + }, + '/apps', + memfs, + ), + ).toMatchInlineSnapshot('[]'); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc14b1452..d79df4c81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -549,6 +549,9 @@ importers: jsdom: specifier: ^26.1.0 version: 26.1.0 + memfs: + specifier: ^4.51.1 + version: 4.51.1 pathe: specifier: ^2.0.3 version: 2.0.3 diff --git a/website/docs/en/config/test/coverage.mdx b/website/docs/en/config/test/coverage.mdx index 1430a9ac8..b15353224 100644 --- a/website/docs/en/config/test/coverage.mdx +++ b/website/docs/en/config/test/coverage.mdx @@ -125,7 +125,6 @@ export default defineConfig({ '**/test/**', '**/__tests__/**', '**/__mocks__/**', - '**/[.]*', '**/*.d.ts', '**/*.{test,spec}.[jt]s', '**/*.{test,spec}.[cm][jt]s', diff --git a/website/docs/zh/config/test/coverage.mdx b/website/docs/zh/config/test/coverage.mdx index cd3519af6..2010b7027 100644 --- a/website/docs/zh/config/test/coverage.mdx +++ b/website/docs/zh/config/test/coverage.mdx @@ -125,7 +125,6 @@ export default defineConfig({ '**/test/**', '**/__tests__/**', '**/__mocks__/**', - '**/[.]*', '**/*.d.ts', '**/*.{test,spec}.[jt]s', '**/*.{test,spec}.[cm][jt]s', From df590ff28ef916b8586d958cc0fbf4ef0f1f687f Mon Sep 17 00:00:00 2001 From: claneo Date: Sun, 4 Jan 2026 10:05:01 +0800 Subject: [PATCH 2/3] fix windows test cases --- packages/core/tests/coverage/include.test.ts | 131 ++++++++----------- 1 file changed, 52 insertions(+), 79 deletions(-) diff --git a/packages/core/tests/coverage/include.test.ts b/packages/core/tests/coverage/include.test.ts index cbb0073b4..ab8533b22 100644 --- a/packages/core/tests/coverage/include.test.ts +++ b/packages/core/tests/coverage/include.test.ts @@ -11,115 +11,88 @@ describe('getIncludedFiles', () => { defaultExclude = withDefaultConfig({}).coverage.exclude; memfs = fs as any; + // There's a bug in tinyglobby: the computed common root path cannot be the root of the Windows volume. [ - '/apps/a.ts', - '/apps/b.js', - '/apps/.c.ts', - '/apps/node_modules/a.ts', - '/apps/dist/a.ts', - '/apps/test/a.ts', - '/apps/__tests__/a.ts', - '/apps/__mocks__/a.ts', - '/apps/a.d.ts', - '/apps/a.test.ts', - '/apps/a.spec.ts', - '/apps/a.test.js', - '/apps/a.spec.js', - '/apps/a.test.mts', - '/apps/a.spec.mts', - '/apps/a.test.cts', - '/apps/a.spec.cts', - '/apps/a.test.tsx', - '/apps/a.spec.tsx', - '/packages/a.ts', - '/packages/b.js', - '/packages/.c.ts', + '/root/apps/a.ts', + '/root/apps/b.js', + '/root/apps/.c.ts', + '/root/apps/node_modules/a.ts', + '/root/apps/dist/a.ts', + '/root/apps/test/a.ts', + '/root/apps/__tests__/a.ts', + '/root/apps/__mocks__/a.ts', + '/root/apps/a.d.ts', + '/root/apps/a.test.ts', + '/root/apps/a.spec.ts', + '/root/apps/a.test.js', + '/root/apps/a.spec.js', + '/root/apps/a.test.mts', + '/root/apps/a.spec.mts', + '/root/apps/a.test.cts', + '/root/apps/a.spec.cts', + '/root/apps/a.test.tsx', + '/root/apps/a.spec.tsx', + '/root/packages/a.ts', + '/root/packages/b.js', + '/root/packages/.c.ts', ].forEach((file) => { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, ''); }); }); + const glob = async (include: string[], exclude: string[] = []) => { + const files = await getIncludedFiles( + { + include, + exclude: [...defaultExclude, ...exclude], + }, + '/root/apps', + memfs, + ); + // ensure consistent paths across platforms + return files + .map((file) => path.relative('/root', file).replaceAll(path.sep, '/')) + .sort(); + }; + it('should include visible files by default', async () => { expect( - await getIncludedFiles( - { - include: ['**/*.{js,ts}', '../packages/*.{js,ts}'], - exclude: [...defaultExclude], - }, - '/apps', - memfs, - ), + await glob(['**/*.{js,ts}', '../packages/*.{js,ts}']), ).toMatchInlineSnapshot(` [ - "/apps/a.ts", - "/apps/b.js", - "/packages/a.ts", - "/packages/b.js", + "apps/a.ts", + "apps/b.js", + "packages/a.ts", + "packages/b.js", ] `); }); it('should include hidden files if explicitly specified', async () => { - expect( - await getIncludedFiles( - { - include: ['**/*.{js,ts}', '**/.c.ts'], - exclude: [...defaultExclude], - }, - '/apps', - memfs, - ), - ).toMatchInlineSnapshot(` + expect(await glob(['**/*.{js,ts}', '**/.c.ts'])).toMatchInlineSnapshot(` [ - "/apps/.c.ts", - "/apps/a.ts", - "/apps/b.js", + "apps/.c.ts", + "apps/a.ts", + "apps/b.js", ] `); }); it('should exclude node_modules, dist, test, __tests__, __mocks__ by default', async () => { - expect( - await getIncludedFiles( - { - include: ['**/*'], - exclude: [...defaultExclude], - }, - '/apps', - memfs, - ), - ).toMatchInlineSnapshot(` + expect(await glob(['**/*'])).toMatchInlineSnapshot(` [ - "/apps/a.ts", - "/apps/b.js", + "apps/a.ts", + "apps/b.js", ] `); }); it('should exclude .d.ts files by default', async () => { - expect( - await getIncludedFiles( - { - include: ['**/*.d.ts'], - exclude: [...defaultExclude], - }, - '/apps', - memfs, - ), - ).toMatchInlineSnapshot('[]'); + expect(await glob(['**/*.d.ts'])).toMatchInlineSnapshot('[]'); }); it('should exclude test and spec files by default', async () => { - expect( - await getIncludedFiles( - { - include: ['**/*.{test,spec}.*'], - exclude: [...defaultExclude], - }, - '/apps', - memfs, - ), - ).toMatchInlineSnapshot('[]'); + expect(await glob(['**/*.{test,spec}.*'])).toMatchInlineSnapshot('[]'); }); }); From 46b14bf154d6a6c77b695657e184c40802fd1e1a Mon Sep 17 00:00:00 2001 From: claneo Date: Sun, 4 Jan 2026 10:12:16 +0800 Subject: [PATCH 3/3] comment --- packages/core/tests/coverage/include.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/tests/coverage/include.test.ts b/packages/core/tests/coverage/include.test.ts index ab8533b22..c6eb2550d 100644 --- a/packages/core/tests/coverage/include.test.ts +++ b/packages/core/tests/coverage/include.test.ts @@ -11,7 +11,7 @@ describe('getIncludedFiles', () => { defaultExclude = withDefaultConfig({}).coverage.exclude; memfs = fs as any; - // There's a bug in tinyglobby: the computed common root path cannot be the root of the Windows volume. + // There's a bug in tinyglobby: if the computed common root path cannot be the root of the Windows volume, the root path will be resolved to pwd. [ '/root/apps/a.ts', '/root/apps/b.js',