diff --git a/packages/coverage-istanbul/src/provider.ts b/packages/coverage-istanbul/src/provider.ts index a579bf874755..50c25c4c45e6 100644 --- a/packages/coverage-istanbul/src/provider.ts +++ b/packages/coverage-istanbul/src/provider.ts @@ -114,6 +114,9 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co const sourceMap = pluginCtx.getCombinedSourcemap() sourceMap.sources = sourceMap.sources.map(removeQueryParameters) + // Exclude SWC's decorators that are left in source maps + sourceCode = sourceCode.replaceAll('_ts_decorate', '/* istanbul ignore next */_ts_decorate') + const code = this.instrumenter.instrumentSync(sourceCode, id, sourceMap as any) const map = this.instrumenter.lastSourceMap() as any diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts index 14ebe527e7fa..f01e99bbb8ae 100644 --- a/packages/coverage-v8/src/provider.ts +++ b/packages/coverage-v8/src/provider.ts @@ -51,6 +51,7 @@ 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 DECORATOR_METADATA_PATTERN = /_ts_metadata\("design:paramtypes"(\s|.)+?]\),/g const DEFAULT_PROJECT = Symbol.for('default-project') const debug = createDebug('vitest:coverage') @@ -312,7 +313,7 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage originalSource: sourcesContent, source: code || sourcesContent, sourceMap: { - sourcemap: removeViteHelpersFromSourceMaps(code, { + sourcemap: excludeGeneratedCode(code, { ...map, version: 3, sources: [url], @@ -363,21 +364,24 @@ async function transformCoverage(coverageMap: CoverageMap) { /** * Remove generated code from the source maps: * - Vite's export helpers: e.g. `Object.defineProperty(__vite_ssr_exports__, "sum", { enumerable: true, configurable: true, get(){ return sum }});` + * - SWC's decorator metadata: e.g. `_ts_metadata("design:paramtypes", [\ntypeof Request === "undefined" ? Object : Request\n]),` */ -function removeViteHelpersFromSourceMaps(source: string | undefined, map: EncodedSourceMap) { - if (!source || !source.match(VITE_EXPORTS_LINE_PATTERN)) +function excludeGeneratedCode(source: string | undefined, map: EncodedSourceMap) { + if (!source) return map - const sourceWithoutHelpers = new MagicString(source) - sourceWithoutHelpers.replaceAll(VITE_EXPORTS_LINE_PATTERN, '\n') + if (!source.match(VITE_EXPORTS_LINE_PATTERN) && !source.match(DECORATOR_METADATA_PATTERN)) + return map + + const trimmed = new MagicString(source) + trimmed.replaceAll(VITE_EXPORTS_LINE_PATTERN, '\n') + trimmed.replaceAll(DECORATOR_METADATA_PATTERN, match => '\n'.repeat(match.split('\n').length - 1)) - const mapWithoutHelpers = sourceWithoutHelpers.generateMap({ - hires: 'boundary', - }) + const trimmedMap = trimmed.generateMap({ hires: 'boundary' }) - // A merged source map where the first one excludes helpers + // A merged source map where the first one excludes generated parts const combinedMap = remapping( - [{ ...mapWithoutHelpers, version: 3 }, map], + [{ ...trimmedMap, version: 3 }, map], () => null, ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f876d312b3f..a8eae3f79f5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1675,6 +1675,9 @@ importers: magicast: specifier: ^0.3.3 version: 0.3.3 + unplugin-swc: + specifier: ^1.4.4 + version: 1.4.4(@swc/core@1.4.1)(rollup@4.9.6) vite: specifier: ^5.0.12 version: 5.0.12(@types/node@20.11.5)(less@4.1.3) @@ -7073,6 +7076,21 @@ packages: picomatch: 2.3.1 rollup: 4.9.6 + /@rollup/pluginutils@5.1.0(rollup@4.9.6): + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^4.9.6 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.5 + estree-walker: 2.0.2 + picomatch: 2.3.1 + rollup: 4.9.6 + dev: true + /@rollup/rollup-android-arm-eabi@4.9.6: resolution: {integrity: sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==} cpu: [arm] @@ -8839,6 +8857,129 @@ packages: - supports-color dev: true + /@swc/core-darwin-arm64@1.4.1: + resolution: {integrity: sha512-ePyfx0348UbR4DOAW24TedeJbafnzha8liXFGuQ4bdXtEVXhLfPngprrxKrAddCuv42F9aTxydlF6+adD3FBhA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@swc/core-darwin-x64@1.4.1: + resolution: {integrity: sha512-eLf4JSe6VkCMdDowjM8XNC5rO+BrgfbluEzAVtKR8L2HacNYukieumN7EzpYCi0uF1BYwu1ku6tLyG2r0VcGxA==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm-gnueabihf@1.4.1: + resolution: {integrity: sha512-K8VtTLWMw+rkN/jDC9o/Q9SMmzdiHwYo2CfgkwVT29NsGccwmNhCQx6XoYiPKyKGIFKt4tdQnJHKUFzxUqQVtQ==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm64-gnu@1.4.1: + resolution: {integrity: sha512-0e8p4g0Bfkt8lkiWgcdiENH3RzkcqKtpRXIVNGOmVc0OBkvc2tpm2WTx/eoCnes2HpTT4CTtR3Zljj4knQ4Fvw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm64-musl@1.4.1: + resolution: {integrity: sha512-b/vWGQo2n7lZVUnSQ7NBq3Qrj85GrAPPiRbpqaIGwOytiFSk8VULFihbEUwDe0rXgY4LDm8z8wkgADZcLnmdUA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-x64-gnu@1.4.1: + resolution: {integrity: sha512-AFMQlvkKEdNi1Vk2GFTxxJzbICttBsOQaXa98kFTeWTnFFIyiIj2w7Sk8XRTEJ/AjF8ia8JPKb1zddBWr9+bEQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-x64-musl@1.4.1: + resolution: {integrity: sha512-QX2MxIECX1gfvUVZY+jk528/oFkS9MAl76e3ZRvG2KC/aKlCQL0KSzcTSm13mOxkDKS30EaGRDRQWNukGpMeRg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-arm64-msvc@1.4.1: + resolution: {integrity: sha512-OklkJYXXI/tntD2zaY8i3iZldpyDw5q+NAP3k9OlQ7wXXf37djRsHLV0NW4+ZNHBjE9xp2RsXJ0jlOJhfgGoFA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-ia32-msvc@1.4.1: + resolution: {integrity: sha512-MBuc3/QfKX9FnLOU7iGN+6yHRTQaPQ9WskiC8s8JFiKQ+7I2p25tay2RplR9dIEEGgVAu6L7auv96LbNTh+FaA==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-x64-msvc@1.4.1: + resolution: {integrity: sha512-lu4h4wFBb/bOK6N2MuZwg7TrEpwYXgpQf5R7ObNSXL65BwZ9BG8XRzD+dLJmALu8l5N08rP/TrpoKRoGT4WSxw==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core@1.4.1: + resolution: {integrity: sha512-3y+Y8js+e7BbM16iND+6Rcs3jdiL28q3iVtYsCviYSSpP2uUVKkp5sJnCY4pg8AaVvyN7CGQHO7gLEZQ5ByozQ==} + engines: {node: '>=10'} + requiresBuild: true + peerDependencies: + '@swc/helpers': ^0.5.0 + peerDependenciesMeta: + '@swc/helpers': + optional: true + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.5 + optionalDependencies: + '@swc/core-darwin-arm64': 1.4.1 + '@swc/core-darwin-x64': 1.4.1 + '@swc/core-linux-arm-gnueabihf': 1.4.1 + '@swc/core-linux-arm64-gnu': 1.4.1 + '@swc/core-linux-arm64-musl': 1.4.1 + '@swc/core-linux-x64-gnu': 1.4.1 + '@swc/core-linux-x64-musl': 1.4.1 + '@swc/core-win32-arm64-msvc': 1.4.1 + '@swc/core-win32-ia32-msvc': 1.4.1 + '@swc/core-win32-x64-msvc': 1.4.1 + dev: true + + /@swc/counter@0.1.3: + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + dev: true + + /@swc/types@0.1.5: + resolution: {integrity: sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==} + dev: true + /@szmarczak/http-timer@5.0.1: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} @@ -19847,6 +19988,11 @@ packages: dev: true optional: true + /load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /loader-runner@2.4.0: resolution: {integrity: sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==} engines: {node: '>=4.3.0 <5.0.0 || >=5.10'} @@ -25831,6 +25977,19 @@ packages: - rollup dev: true + /unplugin-swc@1.4.4(@swc/core@1.4.1)(rollup@4.9.6): + resolution: {integrity: sha512-S2mgLIQVNR1+UGIk379/wD3tmkTJfm9QJFyZgXutMDNsSJrcPNJUdSXUNGE/+1Zde9i/I0r0BvDqxGgTkg+eJQ==} + peerDependencies: + '@swc/core': ^1.2.108 + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.9.6) + '@swc/core': 1.4.1 + load-tsconfig: 0.2.5 + unplugin: 1.7.1 + transitivePeerDependencies: + - rollup + dev: true + /unplugin-vue-components@0.25.2(rollup@4.9.6)(vue@3.3.8): resolution: {integrity: sha512-OVmLFqILH6w+eM8fyt/d/eoJT9A6WO51NZLf1vC5c1FZ4rmq2bbGxTy8WP2Jm7xwFdukaIdv819+UI7RClPyCA==} engines: {node: '>=14'} @@ -25869,6 +26028,15 @@ packages: webpack-virtual-modules: 0.5.0 dev: true + /unplugin@1.7.1: + resolution: {integrity: sha512-JqzORDAPxxs8ErLV4x+LL7bk5pk3YlcWqpSNsIkAZj972KzFZLClc/ekppahKkOczGkwIG6ElFgdOgOlK4tXZw==} + dependencies: + acorn: 8.11.3 + chokidar: 3.5.3 + webpack-sources: 3.2.3 + webpack-virtual-modules: 0.6.1 + dev: true + /unset-value@1.0.0: resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} engines: {node: '>=0.10.0'} @@ -26853,6 +27021,10 @@ packages: resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} dev: true + /webpack-virtual-modules@0.6.1: + resolution: {integrity: sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==} + dev: true + /webpack@4.46.0: resolution: {integrity: sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==} engines: {node: '>=6.11.5'} 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 f81ccd924c84..ccb552fe1c92 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 @@ -20,6 +20,7 @@ exports[`custom json report 1`] = ` "/src/Defined.vue", "/src/Hello.vue", "/src/another-setup.ts", + "/src/decorators.ts", "/src/dynamic-file-esm.ignore.js", "/src/dynamic-files.ts", "/src/function-count.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 8d0773c323e3..30ae7c39868a 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 @@ -530,6 +530,215 @@ exports[`istanbul json report 1`] = ` }, }, }, + "/src/decorators.ts": { + "b": { + "0": [ + 1, + 0, + ], + "1": [ + 0, + 1, + ], + }, + "branchMap": { + "0": { + "loc": { + "end": { + "column": null, + "line": 8, + }, + "start": { + "column": 4, + "line": 5, + }, + }, + "locations": [ + { + "end": { + "column": null, + "line": 8, + }, + "start": { + "column": 4, + "line": 5, + }, + }, + { + "end": { + "column": null, + "line": 8, + }, + "start": { + "column": 4, + "line": 5, + }, + }, + ], + "type": "if", + }, + "1": { + "loc": { + "end": { + "column": null, + "line": 13, + }, + "start": { + "column": 4, + "line": 10, + }, + }, + "locations": [ + { + "end": { + "column": null, + "line": 13, + }, + "start": { + "column": 4, + "line": 10, + }, + }, + { + "end": { + "column": null, + "line": 13, + }, + "start": { + "column": 4, + "line": 10, + }, + }, + ], + "type": "if", + }, + }, + "f": { + "0": 1, + "1": 1, + "2": 1, + }, + "fnMap": { + "0": { + "decl": { + "end": { + "column": 9, + "line": 4, + }, + "start": { + "column": 2, + "line": 4, + }, + }, + "loc": { + "end": { + "column": null, + "line": 14, + }, + "start": { + "column": 46, + "line": 4, + }, + }, + "name": "(anonymous_4)", + }, + "1": { + "decl": { + "end": { + "column": null, + "line": 17, + }, + "start": { + "column": 9, + "line": 17, + }, + }, + "loc": { + "end": { + "column": null, + "line": 21, + }, + "start": { + "column": 25, + "line": 20, + }, + }, + "name": "SomeDecorator", + }, + "2": { + "decl": { + "end": { + "column": 17, + "line": 25, + }, + "start": { + "column": 9, + "line": 25, + }, + }, + "loc": { + "end": { + "column": null, + "line": 27, + }, + "start": { + "column": 33, + "line": 25, + }, + }, + "name": "noop", + }, + }, + "path": "/src/decorators.ts", + "s": { + "0": 1, + "1": 1, + "2": 1, + "3": 0, + }, + "statementMap": { + "0": { + "end": { + "column": null, + "line": 8, + }, + "start": { + "column": 4, + "line": 5, + }, + }, + "1": { + "end": { + "column": null, + "line": 7, + }, + "start": { + "column": 6, + "line": 7, + }, + }, + "2": { + "end": { + "column": null, + "line": 13, + }, + "start": { + "column": 4, + "line": 10, + }, + }, + "3": { + "end": { + "column": null, + "line": 12, + }, + "start": { + "column": 6, + "line": 12, + }, + }, + }, + }, "/src/dynamic-files.ts": { "b": { "0": [ 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 863ebec77577..4ee530489568 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 @@ -1231,6 +1231,510 @@ exports[`v8 json report 1`] = ` }, }, }, + "/src/decorators.ts": { + "all": false, + "b": { + "0": [ + 1, + ], + "1": [ + 0, + ], + "2": [ + 1, + ], + "3": [ + 1, + ], + }, + "branchMap": { + "0": { + "line": 4, + "loc": { + "end": { + "column": 3, + "line": 14, + }, + "start": { + "column": 2, + "line": 4, + }, + }, + "locations": [ + { + "end": { + "column": 3, + "line": 14, + }, + "start": { + "column": 2, + "line": 4, + }, + }, + ], + "type": "branch", + }, + "1": { + "line": 10, + "loc": { + "end": { + "column": 5, + "line": 13, + }, + "start": { + "column": 35, + "line": 10, + }, + }, + "locations": [ + { + "end": { + "column": 5, + "line": 13, + }, + "start": { + "column": 35, + "line": 10, + }, + }, + ], + "type": "branch", + }, + "2": { + "line": 17, + "loc": { + "end": { + "column": 4, + "line": 21, + }, + "start": { + "column": 0, + "line": 17, + }, + }, + "locations": [ + { + "end": { + "column": 4, + "line": 21, + }, + "start": { + "column": 0, + "line": 17, + }, + }, + ], + "type": "branch", + }, + "3": { + "line": 25, + "loc": { + "end": { + "column": 1, + "line": 27, + }, + "start": { + "column": 0, + "line": 25, + }, + }, + "locations": [ + { + "end": { + "column": 1, + "line": 27, + }, + "start": { + "column": 0, + "line": 25, + }, + }, + ], + "type": "branch", + }, + }, + "f": { + "0": 1, + "1": 1, + "2": 1, + }, + "fnMap": { + "0": { + "decl": { + "end": { + "column": 3, + "line": 14, + }, + "start": { + "column": 2, + "line": 4, + }, + }, + "line": 4, + "loc": { + "end": { + "column": 3, + "line": 14, + }, + "start": { + "column": 2, + "line": 4, + }, + }, + "name": "method", + }, + "1": { + "decl": { + "end": { + "column": 4, + "line": 21, + }, + "start": { + "column": 0, + "line": 17, + }, + }, + "line": 17, + "loc": { + "end": { + "column": 4, + "line": 21, + }, + "start": { + "column": 0, + "line": 17, + }, + }, + "name": "SomeDecorator", + }, + "2": { + "decl": { + "end": { + "column": 1, + "line": 27, + }, + "start": { + "column": 0, + "line": 25, + }, + }, + "line": 25, + "loc": { + "end": { + "column": 1, + "line": 27, + }, + "start": { + "column": 0, + "line": 25, + }, + }, + "name": "noop", + }, + }, + "path": "/src/decorators.ts", + "s": { + "0": 1, + "1": 1, + "10": 0, + "11": 0, + "12": 0, + "13": 1, + "14": 1, + "15": 1, + "16": 1, + "17": 1, + "18": 1, + "19": 1, + "2": 1, + "20": 1, + "21": 1, + "22": 1, + "23": 1, + "24": 1, + "25": 1, + "26": 1, + "3": 1, + "4": 1, + "5": 1, + "6": 1, + "7": 1, + "8": 1, + "9": 1, + }, + "statementMap": { + "0": { + "end": { + "column": 85, + "line": 1, + }, + "start": { + "column": 0, + "line": 1, + }, + }, + "1": { + "end": { + "column": 14, + "line": 2, + }, + "start": { + "column": 0, + "line": 2, + }, + }, + "10": { + "end": { + "column": 23, + "line": 11, + }, + "start": { + "column": 0, + "line": 11, + }, + }, + "11": { + "end": { + "column": 21, + "line": 12, + }, + "start": { + "column": 0, + "line": 12, + }, + }, + "12": { + "end": { + "column": 5, + "line": 13, + }, + "start": { + "column": 0, + "line": 13, + }, + }, + "13": { + "end": { + "column": 3, + "line": 14, + }, + "start": { + "column": 0, + "line": 14, + }, + }, + "14": { + "end": { + "column": 1, + "line": 15, + }, + "start": { + "column": 0, + "line": 15, + }, + }, + "15": { + "end": { + "column": 0, + "line": 16, + }, + "start": { + "column": 0, + "line": 16, + }, + }, + "16": { + "end": { + "column": 23, + "line": 17, + }, + "start": { + "column": 0, + "line": 17, + }, + }, + "17": { + "end": { + "column": 18, + "line": 18, + }, + "start": { + "column": 0, + "line": 18, + }, + }, + "18": { + "end": { + "column": 32, + "line": 19, + }, + "start": { + "column": 0, + "line": 19, + }, + }, + "19": { + "end": { + "column": 26, + "line": 20, + }, + "start": { + "column": 0, + "line": 20, + }, + }, + "2": { + "end": { + "column": 31, + "line": 3, + }, + "start": { + "column": 0, + "line": 3, + }, + }, + "20": { + "end": { + "column": 4, + "line": 21, + }, + "start": { + "column": 0, + "line": 21, + }, + }, + "21": { + "end": { + "column": 0, + "line": 22, + }, + "start": { + "column": 0, + "line": 22, + }, + }, + "22": { + "end": { + "column": 24, + "line": 23, + }, + "start": { + "column": 0, + "line": 23, + }, + }, + "23": { + "end": { + "column": 0, + "line": 24, + }, + "start": { + "column": 0, + "line": 24, + }, + }, + "24": { + "end": { + "column": 36, + "line": 25, + }, + "start": { + "column": 0, + "line": 25, + }, + }, + "25": { + "end": { + "column": 0, + "line": 26, + }, + "start": { + "column": 0, + "line": 26, + }, + }, + "26": { + "end": { + "column": 1, + "line": 27, + }, + "start": { + "column": 0, + "line": 27, + }, + }, + "3": { + "end": { + "column": 47, + "line": 4, + }, + "start": { + "column": 0, + "line": 4, + }, + }, + "4": { + "end": { + "column": 20, + "line": 5, + }, + "start": { + "column": 0, + "line": 5, + }, + }, + "5": { + "end": { + "column": 21, + "line": 6, + }, + "start": { + "column": 0, + "line": 6, + }, + }, + "6": { + "end": { + "column": 21, + "line": 7, + }, + "start": { + "column": 0, + "line": 7, + }, + }, + "7": { + "end": { + "column": 5, + "line": 8, + }, + "start": { + "column": 0, + "line": 8, + }, + }, + "8": { + "end": { + "column": 0, + "line": 9, + }, + "start": { + "column": 0, + "line": 9, + }, + }, + "9": { + "end": { + "column": 36, + "line": 10, + }, + "start": { + "column": 0, + "line": 10, + }, + }, + }, + }, "/src/dynamic-file-cjs.ignore.cjs": { "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 42147822a541..e9ba58f5cc34 100644 --- a/test/coverage-test/coverage-report-tests/generic.report.test.ts +++ b/test/coverage-test/coverage-report-tests/generic.report.test.ts @@ -185,6 +185,30 @@ test('multi environment coverage is merged correctly', async () => { expect(lineCoverage[30]).toBe(2) }) +test('decorators generated metadata is ignored', async () => { + const coverageJson = await readCoverageJson() + const coverageMap = libCoverage.createCoverageMap(coverageJson as any) + const fileCoverage = coverageMap.fileCoverageFor('/src/decorators.ts') + const lineCoverage = fileCoverage.getLineCoverage() + const branchCoverage = fileCoverage.getBranchCoverageByLine() + + // Decorator should not be uncovered - on V8 this is marked as covered, on Istanbul it's excluded from report + if (process.env.COVERAGE_PROVIDER === 'v8') { + expect(lineCoverage['4']).toBe(1) + expect(branchCoverage['4'].coverage).toBe(100) + } + else { + expect(lineCoverage['4']).toBeUndefined() + expect(branchCoverage['4']).toBeUndefined() + } + + // Covered branch should be marked correctly + expect(lineCoverage['7']).toBe(1) + + // Uncovered branch should be marked correctly + expect(lineCoverage['12']).toBe(0) +}) + test('temporary files are removed after test', async () => { const coveragePath = resolve('./coverage') const files = fs.readdirSync(coveragePath) diff --git a/test/coverage-test/package.json b/test/coverage-test/package.json index 1393431bb3c6..64a4f906360d 100644 --- a/test/coverage-test/package.json +++ b/test/coverage-test/package.json @@ -24,6 +24,7 @@ "istanbul-lib-coverage": "^3.2.0", "istanbul-lib-report": "^3.0.1", "magicast": "^0.3.3", + "unplugin-swc": "^1.4.4", "vite": "latest", "vitest": "workspace:*", "vue": "latest", diff --git a/test/coverage-test/src/decorators.ts b/test/coverage-test/src/decorators.ts new file mode 100644 index 000000000000..798ade00ae2c --- /dev/null +++ b/test/coverage-test/src/decorators.ts @@ -0,0 +1,27 @@ +// eslint-disable-next-line ts/ban-ts-comment -- so that typecheck doesn't include it +// @ts-nocheck +export class DecoratorsTester { + method(@SomeDecorator parameter: Something) { + if (parameter) { + // Covered line + noop(parameter) + } + + if (parameter === 'uncovered') { + // Uncovered line + noop(parameter) + } + } +} + +function SomeDecorator( + _target: Object, + _propertyKey: string | symbol, + _parameterIndex: number, +) {} + +type Something = unknown + +function noop(..._args: unknown[]) { + +} diff --git a/test/coverage-test/test/coverage.test.ts b/test/coverage-test/test/coverage.test.ts index 42ae5fe6ea51..366743ab4e8f 100644 --- a/test/coverage-test/test/coverage.test.ts +++ b/test/coverage-test/test/coverage.test.ts @@ -7,6 +7,7 @@ import { implicitElse } from '../src/implicitElse' import { useImportEnv } from '../src/importEnv' import { second } from '../src/function-count' import MultiSuite from '../src/multi-suite' +import { DecoratorsTester } from '../src/decorators' // @ts-expect-error -- untyped virtual file provided by custom plugin import virtualFile2 from '\0vitest-custom-virtual-file-2' @@ -71,3 +72,7 @@ test('virtual file imports', () => { expect(virtualFile1).toBe('This file should be excluded from coverage report #1') expect(virtualFile2).toBe('This file should be excluded from coverage report #2') }) + +test('decorators', () => { + new DecoratorsTester().method('cover line') +}) diff --git a/test/coverage-test/testing.mjs b/test/coverage-test/testing.mjs index 2a2c5cd3515f..702f92bfd744 100644 --- a/test/coverage-test/testing.mjs +++ b/test/coverage-test/testing.mjs @@ -8,8 +8,8 @@ const isBrowser = process.argv.includes('--browser') const isCI = process.env.GITHUB_ACTIONS process.env.COVERAGE_PROVIDER = provider -// TODO: Fix flakiness and enable on CI -- browser picks test files that don't exist and fails, some tests fail because of the multi environment mismatch -if (isCI) +// TODO: Fix flakiness and enable on CI -- browser picks test files that don't exist and fails, issue #5165 +if (isCI && isBrowser) process.exit(0) const poolConfigs = [ diff --git a/test/coverage-test/tsconfig.json b/test/coverage-test/tsconfig.json new file mode 100644 index 000000000000..0742e50cba97 --- /dev/null +++ b/test/coverage-test/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "experimentalDecorators": true + } +} diff --git a/test/coverage-test/vitest.config.ts b/test/coverage-test/vitest.config.ts index 01cf624633c4..f493f0ca5e8c 100644 --- a/test/coverage-test/vitest.config.ts +++ b/test/coverage-test/vitest.config.ts @@ -1,67 +1,19 @@ import { resolve } from 'pathe' +import swc from 'unplugin-swc' import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' import MagicString from 'magic-string' import remapping from '@ampproject/remapping' +import type { Plugin } from 'vite' const provider = process.argv[1 + process.argv.indexOf('--provider')] -export default defineConfig(() => ({ +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', - resolveId(id) { - if (id === 'virtual:vitest-custom-virtual-file-1') - return 'src/virtual:vitest-custom-virtual-file-1.ts' - - if (id === '\0vitest-custom-virtual-file-2') - return 'src/\0vitest-custom-virtual-file-2.ts' - }, - load(id) { - if (id === 'src/virtual:vitest-custom-virtual-file-1.ts') { - return ` - const virtualFile = "This file should be excluded from coverage report #1" - export default virtualFile; - ` - } - - // Vitest browser resolves this as "\x00", Node as "__x00__" - if (id === 'src/__x00__vitest-custom-virtual-file-2.ts' || id === 'src/\x00vitest-custom-virtual-file-2.ts') { - return ` - const virtualFile = "This file should be excluded from coverage report #2" - export default virtualFile; - ` - } - }, - }, + MultiTransformPlugin(), + VirtualFilesPlugin(), + DecoratorsPlugin(), ], define: { MY_CONSTANT: '"my constant"', @@ -105,3 +57,88 @@ export default defineConfig(() => ({ ], }, })) + +/* + * 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. + */ +function MultiTransformPlugin(): Plugin { + return { + 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 +function VirtualFilesPlugin(): Plugin { + return { + name: 'vitest-custom-virtual-files', + resolveId(id) { + if (id === 'virtual:vitest-custom-virtual-file-1') + return 'src/virtual:vitest-custom-virtual-file-1.ts' + + if (id === '\0vitest-custom-virtual-file-2') + return 'src/\0vitest-custom-virtual-file-2.ts' + }, + load(id) { + if (id === 'src/virtual:vitest-custom-virtual-file-1.ts') { + return ` + const virtualFile = "This file should be excluded from coverage report #1" + export default virtualFile; + ` + } + + // Vitest browser resolves this as "\x00", Node as "__x00__" + if (id === 'src/__x00__vitest-custom-virtual-file-2.ts' || id === 'src/\x00vitest-custom-virtual-file-2.ts') { + return ` + const virtualFile = "This file should be excluded from coverage report #2" + export default virtualFile; + ` + } + }, + } +} + +function DecoratorsPlugin(): Plugin { + const plugin = swc.vite({ + jsc: { + target: 'esnext', + parser: { + syntax: 'typescript', + decorators: true, + }, + transform: { + legacyDecorator: true, + decoratorMetadata: true, + }, + }, + }) + + return { + name: 'custom-swc-decorator', + enforce: 'pre', + transform(code, id, options) { + if (id.endsWith('decorators.ts')) + // @ts-expect-error -- Ignore complex type + return plugin.transform(code, id, options) + }, + } +}