From 6192bd20eb32f5e3824380815911c8e265269bc2 Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 2 Dec 2025 16:14:12 +1000 Subject: [PATCH] modified: .gitignore modified: .vscode/launch.json new file: e2e/playwright-collect-coverage.spec.ts modified: package-lock.json modified: package.json modified: playwright.config.ts new file: scripts/merge-lcov.cjs new file: scripts/normalize-lcov.cjs new file: scripts/playwright-collect-coverage.cjs new file: scripts/remap-and-report.cjs modified: vite.config.ts new file: vitest.config.ts --- .gitignore | 2 +- .vscode/launch.json | 13 +- e2e/playwright-collect-coverage.spec.ts | 57 ++ package-lock.json | 757 ++++++++++++++++++++++++ package.json | 13 +- playwright.config.ts | 6 +- scripts/merge-lcov.cjs | 64 ++ scripts/normalize-lcov.cjs | 134 +++++ scripts/playwright-collect-coverage.cjs | 23 + scripts/remap-and-report.cjs | 35 ++ vite.config.ts | 16 +- vitest.config.ts | 24 + 12 files changed, 1134 insertions(+), 10 deletions(-) create mode 100644 e2e/playwright-collect-coverage.spec.ts create mode 100755 scripts/merge-lcov.cjs create mode 100755 scripts/normalize-lcov.cjs create mode 100755 scripts/playwright-collect-coverage.cjs create mode 100644 scripts/remap-and-report.cjs create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index 60776a6..9ef2236 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ playwright-report/ coverage/ build/ testrun.log -e2e-coverage-report/ \ No newline at end of file +coverage-reports/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index f953a31..052126e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,9 +21,9 @@ "request": "launch", "command": "npm run test:e2e", "cwd": "${workspaceFolder}", - "env": { - "CHROME_PATH": "" - } + // "env": { + // "CHROME_PATH": "" + // } }, { "name": "Unit Test Coverage", @@ -38,6 +38,13 @@ "request": "launch", "command": "npx tsc -noEmit && npm run build", "cwd": "${workspaceFolder}" + }, + { + "name": "Full CI Coverage test", + "type": "node-terminal", + "request": "launch", + "command": "npm run coverage:ci", + "cwd": "${workspaceFolder}" } ] } diff --git a/e2e/playwright-collect-coverage.spec.ts b/e2e/playwright-collect-coverage.spec.ts new file mode 100644 index 0000000..4ed5e3f --- /dev/null +++ b/e2e/playwright-collect-coverage.spec.ts @@ -0,0 +1,57 @@ +// e2e/playwright-collect-coverage.spec.ts +import fs from 'fs'; +import path from 'path'; +import { test } from '@playwright/test'; + +const EVAL_TIMEOUT_MS = 5000; +const WRITE_TIMEOUT_MS = 15000; + +function withTimeout(p: Promise, ms: number, onTimeout?: () => void) { + let timer: NodeJS.Timeout | undefined; + const timeout = new Promise((res) => { + timer = setTimeout(() => { + onTimeout?.(); + // @ts-ignore resolve null on timeout + res(null); + }, ms); + }); + return Promise.race([p, timeout]).then((r) => { + if (timer) clearTimeout(timer); + return r; + }); +} + +test.afterEach(async ({ page }, testInfo) => { + try { + console.log(`Collecting coverage for: ${testInfo.title}`); + + // Evaluate coverage in page, but don't hang forever + const evalPromise = page.evaluate(() => (globalThis as any).__coverage__ || null); + const coverage = await withTimeout(evalPromise, EVAL_TIMEOUT_MS, () => { + console.warn('page.evaluate for coverage timed out'); + }); + + if (!coverage || Object.keys(coverage).length === 0) { + console.log('No coverage collected (empty or timed out).'); + return; + } + + const dir = path.join(process.cwd(), '.nyc_output'); + await fs.promises.mkdir(dir, { recursive: true }); + + const safeTitle = (testInfo.title || 'test').replace(/[^a-zA-Z0-9-_]/g, '_').slice(0, 120); + const worker = typeof testInfo.workerIndex === 'number' ? testInfo.workerIndex : process.pid; + const filename = path.join(dir, `coverage-e2e-w${worker}-${Date.now()}-${safeTitle}.json`); + + // write asynchronously, bounded by timeout + const writePromise = fs.promises.writeFile(filename, JSON.stringify(coverage)); + await withTimeout(writePromise, WRITE_TIMEOUT_MS, () => { + console.warn('Writing coverage file timed out'); + }); + + console.log('Wrote coverage:', filename); + } catch (error: unknown) { + const msg = error instanceof Error ? error.message : String(error); + console.warn('Failed to collect coverage from page:', msg); + } +}); diff --git a/package-lock.json b/package-lock.json index 5a11416..96e35e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,15 +39,22 @@ "@vitejs/plugin-react": "^5.1.1", "@vitest/coverage-v8": "^4.0.14", "@vitest/ui": "^4.0.14", + "cross-env": "^10.1.0", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.2.0", "jsdom": "^27.2.0", + "lcov-result-merger": "^5.0.1", "msw": "^2.12.3", "rollup-plugin-visualizer": "^6.0.5", "typescript": "^5.9.3", "typescript-eslint": "^8.48.0", "vite": "^7.2.6", + "vite-plugin-istanbul": "^7.2.1", "vitest": "^4.0.14" } }, @@ -754,6 +761,13 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1525,6 +1539,226 @@ } } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1832,6 +2066,44 @@ } } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@open-draft/deferred-promise": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", @@ -1857,6 +2129,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@playwright/test": { "version": "1.57.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", @@ -3151,6 +3434,16 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001757", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", @@ -3341,6 +3634,24 @@ "node": ">= 6" } }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3507,6 +3818,13 @@ "csstype": "^3.0.2" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.262", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz", @@ -3812,6 +4130,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -3885,6 +4217,36 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3899,6 +4261,16 @@ "dev": true, "license": "MIT" }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -4018,6 +4390,23 @@ "dev": true, "license": "ISC" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -4075,6 +4464,37 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4428,6 +4848,23 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -4472,6 +4909,22 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4605,6 +5058,82 @@ "json-buffer": "3.0.1" } }, + "node_modules/lcov-result-merger": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/lcov-result-merger/-/lcov-result-merger-5.0.1.tgz", + "integrity": "sha512-i53RjTYfqbHgerqGtuJjDfARDU340zNxXrJudQZU3o8ak9rrx8FDQUKf38Cjm6MtbqonqiDFmoKuUe++uZbvOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.11", + "yargs": "^16.2.0" + }, + "bin": { + "lcov-result-merger": "bin/lcov-result-merger.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/lcov-result-merger/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/lcov-result-merger/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lcov-result-merger/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lcov-result-merger/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4735,6 +5264,16 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -4788,6 +5327,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -5041,6 +5590,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -5122,6 +5688,30 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -5306,6 +5896,27 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", @@ -5482,6 +6093,17 @@ "dev": true, "license": "MIT" }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.53.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", @@ -5565,6 +6187,30 @@ "node": ">= 12" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -5718,6 +6364,13 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -5773,6 +6426,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5786,6 +6455,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -5881,6 +6564,21 @@ "node": ">=10" } }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -6255,6 +6953,46 @@ } } }, + "node_modules/vite-plugin-istanbul": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/vite-plugin-istanbul/-/vite-plugin-istanbul-7.2.1.tgz", + "integrity": "sha512-DSPi4ulvYsjnP44sTI5oriNosbM0E6m3uoCxjdxboTtVzxSkFwcDy3/JnSYKebjr+ZToJwVLTms+2CM0rmbbzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.28.0", + "@istanbuljs/load-nyc-config": "^1.1.0", + "@types/babel__generator": "7.6.8", + "espree": "^10.3.0", + "istanbul-lib-instrument": "^6.0.3", + "picocolors": "^1.1.1", + "source-map": "^0.7.4", + "test-exclude": "^7.0.1" + }, + "peerDependencies": { + "vite": ">=4 <=7" + } + }, + "node_modules/vite-plugin-istanbul/node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/vite-plugin-istanbul/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/vite/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -6476,6 +7214,25 @@ "node": ">=8" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", diff --git a/package.json b/package.json index bc97db4..dba95e5 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,11 @@ "lint": "eslint .", "test": "vitest", "test:coverage": "vitest run --coverage", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "coverage:unit": "npm run test:coverage", + "coverage:e2e": "npm run test:e2e", + "coverage:merge": "node scripts/merge-lcov.cjs 'coverage-reports/unit/lcov.info' 'coverage-reports/e2e/coverage.lcov' > coverage-reports/merged/merged.lcov && node scripts/normalize-lcov.cjs coverage-reports/merged/merged.lcov && genhtml coverage-reports/merged/merged.normalized.lcov -o coverage-reports/merged", + "coverage:ci": "npm run coverage:unit && npm run coverage:e2e && npm run coverage:merge" }, "dependencies": { "@emotion/react": "^11.14.0", @@ -44,15 +48,22 @@ "@vitejs/plugin-react": "^5.1.1", "@vitest/coverage-v8": "^4.0.14", "@vitest/ui": "^4.0.14", + "cross-env": "^10.1.0", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.2.0", "jsdom": "^27.2.0", + "lcov-result-merger": "^5.0.1", "msw": "^2.12.3", "rollup-plugin-visualizer": "^6.0.5", "typescript": "^5.9.3", "typescript-eslint": "^8.48.0", "vite": "^7.2.6", + "vite-plugin-istanbul": "^7.2.1", "vitest": "^4.0.14" } } diff --git a/playwright.config.ts b/playwright.config.ts index 7688291..d5dd01c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ defineCoverageReporterConfig({ sourceRoot: process.cwd(), // root used to resolve source files exclude: ['node_modules/**', 'e2e/**'], // exclude test sources and deps - resultDir: path.join(process.cwd(), 'e2e-coverage-report'), + resultDir: path.join(process.cwd(), 'coverage-reports/e2e'), reports: [ ['html'], // interactive HTML at results/e2e-coverage/index.html ['lcovonly', { file: 'coverage.lcov' }], // LCOV for CI @@ -29,9 +29,11 @@ export default defineConfig({ baseURL: 'http://127.0.0.1:4173', headless: true, trace: 'on-first-retry', + navigationTimeout: 60_000, // 60s for page.goto / navigations + actionTimeout: 30_000, // optional: for clicks/typing etc (default 30s) }, webServer: { - command: 'npm run dev -- --host --port 4173', + command: 'E2E_COVERAGE=1 npm run dev -- --host --port 4173', url: 'http://127.0.0.1:4173', timeout: 120 * 1000, reuseExistingServer: !process.env.CI, diff --git a/scripts/merge-lcov.cjs b/scripts/merge-lcov.cjs new file mode 100755 index 0000000..c250432 --- /dev/null +++ b/scripts/merge-lcov.cjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs'); + +const files = process.argv.slice(2); +if (files.length < 2) { + console.error('Usage: node merge-lcov.cjs [ ...]'); + process.exit(2); +} + +function ensureEntry(map, sf) { + if (!map.has(sf)) map.set(sf, { other: new Set(), da: new Map() }); + return map.get(sf); +} + +const combined = new Map(); + +for (const f of files) { + let content; + try { + content = fs.readFileSync(f, 'utf8'); + } catch (err) { + console.error('Could not read', f, err.message); + process.exit(3); + } + const lines = content.split(/\r?\n/); + let currentSF = null; + for (let raw of lines) { + const line = raw.trim(); + if (!line) continue; + if (line.startsWith('SF:')) { + currentSF = line.slice(3); + ensureEntry(combined, currentSF); + } else if (!currentSF) { + // ignore lines before first SF + } else if (line.startsWith('DA:')) { + const parts = line.slice(3).split(','); + const lnNum = parseInt(parts[0], 10); + const hitsNum = parseInt(parts[1] || '0', 10); + const entry = ensureEntry(combined, currentSF); + entry.da.set(lnNum, (entry.da.get(lnNum) || 0) + hitsNum); + } else if (line.startsWith('end_of_record')) { + // ignore + } else { + const entry = ensureEntry(combined, currentSF); + entry.other.add(line); + } + } +} + +let out = ''; +for (const [sf, data] of combined.entries()) { + out += `SF:${sf}\n`; + const other = Array.from(data.other).sort(); + for (const l of other) out += l + '\n'; + const daEntries = Array.from(data.da.entries()).sort((a, b) => a[0] - b[0]); + for (const [ln, hits] of daEntries) out += `DA:${ln},${hits}\n`; + const lf = daEntries.length; + const lh = daEntries.reduce((acc, [, hits]) => acc + (hits > 0 ? 1 : 0), 0); + out += `LF:${lf}\nLH:${lh}\nend_of_record\n`; +} + +process.stdout.write(out); diff --git a/scripts/normalize-lcov.cjs b/scripts/normalize-lcov.cjs new file mode 100755 index 0000000..fcd8f07 --- /dev/null +++ b/scripts/normalize-lcov.cjs @@ -0,0 +1,134 @@ +#!/usr/bin/env node +// scripts/normalize-lcov.cjs +// Usage: node scripts/normalize-lcov.cjs input.lcov [output.lcov] + +const fs = require('fs'); +const path = require('path'); + +const repoRoot = process.cwd(); +const inFile = process.argv[2] || 'coverage-reports/merged/merged.lcov'; +const outFile = process.argv[3] || inFile.replace(/\.lcov$/, '.normalized.lcov'); + +if (!fs.existsSync(inFile)) { + console.error('Input file not found:', inFile); + process.exit(2); +} + +const content = fs.readFileSync(inFile, 'utf8'); +const lines = content.split(/\r?\n/); + +// split into records separated by "end_of_record" +const records = []; +let cur = []; +for (const L of lines) { + cur.push(L); + if (L.trim() === 'end_of_record') { + records.push(cur.join('\n')); + cur = []; + } +} +// if file didn't end with end_of_record, push remainder +if (cur.length) records.push(cur.join('\n')); + +function exists(p) { + try { return fs.existsSync(p); } catch { return false; } +} + +function findInSrc(basename) { + const srcRoot = path.join(repoRoot, 'src'); + if (!fs.existsSync(srcRoot)) return null; + // recursive search (stop when found) + function walk(dir) { + const items = fs.readdirSync(dir, { withFileTypes: true }); + for (const it of items) { + const full = path.join(dir, it.name); + if (it.isFile() && it.name === basename) return full; + if (it.isDirectory() && it.name !== 'node_modules' && it.name !== '.git') { + const found = walk(full); + if (found) return found; + } + } + return null; + } + return walk(srcRoot); +} + +function normalizeSF(sfLine) { + // sfLine is the part after 'SF:' + let sf = sfLine.trim(); + + // Skip clearly virtual / query-string entries + if (sf.startsWith('@') || sf.includes('?v=') || sf === '@react-refresh') { + return null; + } + + // If it's an absolute path that exists, make it repo-relative. + if (path.isAbsolute(sf)) { + if (exists(sf)) return path.relative(repoRoot, sf); + // Try chopping off any leading workspace prefix and look again + const repoName = path.basename(repoRoot); + const idx = sf.indexOf(`/${repoName}/`); + if (idx !== -1) { + const candidate = sf.slice(idx + repoName.length + 2); // after /repoName/ + if (exists(path.join(repoRoot, candidate))) return candidate; + if (exists(path.join(repoRoot, 'src', candidate))) return path.posix.join('src', candidate); + } + } + + // If it's already relative, check relative to repo root + if (!path.isAbsolute(sf)) { + if (exists(path.join(repoRoot, sf))) return sf; + // If it looks like "stockfill/..." or "/..." try to strip the repo prefix + const repoName = path.basename(repoRoot); + if (sf.startsWith(`${repoName}/`) || sf.startsWith('stockfill/')) { + const candidate = sf.replace(new RegExp(`^(${repoName}|stockfill)/`), ''); + if (exists(path.join(repoRoot, candidate))) return candidate; + if (exists(path.join(repoRoot, 'src', candidate))) return path.posix.join('src', candidate); + } + // If it is a bare filename "DBProvider.tsx" try src/ + if (!sf.includes('/') && exists(path.join(repoRoot, 'src', sf))) { + return path.posix.join('src', sf); + } + } + + // If it contains '/src/' somewhere, try to extract the src/... suffix + const srcIdx = sf.indexOf('/src/'); + if (srcIdx !== -1) { + const candidate = sf.slice(srcIdx + 1); // remove leading slash + if (exists(path.join(repoRoot, candidate))) return candidate; + } + + // Finally, try finding by basename under src + const base = path.basename(sf); + const found = findInSrc(base); + if (found) return path.relative(repoRoot, found); + + // give up + return null; +} + +let kept = 0, dropped = 0; + +const outRecords = records.map(rec => { + // find SF line + const m = rec.match(/^SF:(.*)$/m); + if (!m) { + // no SF line? keep it (unlikely) + kept++; + return rec; + } + const sfRaw = m[1].trim(); + const normalized = normalizeSF(sfRaw); + if (!normalized) { + dropped++; + return null; // drop entire record + } + // replace SF:... with normalized relative path using posix separators + const normalizedPosix = normalized.split(path.sep).join(path.posix.sep); + const newRec = rec.replace(/^SF:.*$/m, `SF:${normalizedPosix}`); + kept++; + return newRec; +}).filter(Boolean); + +fs.writeFileSync(outFile, outRecords.join('\n') + '\n'); +console.log(`Wrote ${outFile} (kept ${kept}, dropped ${dropped} records)`); diff --git a/scripts/playwright-collect-coverage.cjs b/scripts/playwright-collect-coverage.cjs new file mode 100755 index 0000000..4229e64 --- /dev/null +++ b/scripts/playwright-collect-coverage.cjs @@ -0,0 +1,23 @@ +// tests/playwright-collect-coverage.js +const fs = require('fs'); +const path = require('path'); +const { test } = require('@playwright/test'); + +test.afterEach(async ({ page }, testInfo) => { + try { + // evaluate coverage from the page + const coverage = await page.evaluate(() => (globalThis.__coverage__ || {})); + if (coverage && Object.keys(coverage).length) { + const dir = path.join(process.cwd(), '.nyc_output'); + fs.mkdirSync(dir, { recursive: true }); + + // unique file per worker/test + const filename = path.join(dir, `coverage-e2e-${process.pid}-${Date.now()}.json`); + fs.writeFileSync(filename, JSON.stringify(coverage)); + console.log('Wrote coverage:', filename); + } + } catch (err) { + // don't fail tests if coverage collection fails + console.warn('Failed to collect coverage from page:', err.message || err); + } +}); diff --git a/scripts/remap-and-report.cjs b/scripts/remap-and-report.cjs new file mode 100644 index 0000000..04507fb --- /dev/null +++ b/scripts/remap-and-report.cjs @@ -0,0 +1,35 @@ +// scripts/remap-and-report.cjs +const fs = require('fs'); +const path = require('path'); +const libCoverage = require('istanbul-lib-coverage'); +const libSourceMaps = require('istanbul-lib-source-maps'); +const libReport = require('istanbul-lib-report'); +const reports = require('istanbul-reports'); + +const out = libCoverage.createCoverageMap({}); +const dir = path.join(process.cwd(), '.nyc_output'); +if (!fs.existsSync(dir)) { + console.error('.nyc_output not found'); + process.exit(2); +} +const files = fs.readdirSync(dir).filter(f => f.endsWith('.json')); +for (const f of files) { + const data = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')); + out.merge(data); +} + +// Remap using source maps +const sourceMapStore = libSourceMaps.createSourceMapStore(); +const transformed = sourceMapStore.transformCoverage(out); +const remapped = transformed.map; + +// Write reports +const outDir = path.join(process.cwd(), 'coverage-reports', 'e2e'); +fs.mkdirSync(outDir, { recursive: true }); +const context = libReport.createContext({ dir: outDir, coverageMap: remapped }); + +// lcov + html +reports.create('lcovonly', {}).execute(context); +reports.create('html', {}).execute(context); + +console.log('Wrote remapped e2e coverage to', outDir); diff --git a/vite.config.ts b/vite.config.ts index 7e45e50..6dbb932 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,12 +3,22 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { visualizer } from 'rollup-plugin-visualizer'; import path from 'path'; +import istanbul from 'vite-plugin-istanbul'; + +const isE2E = Boolean(process.env.E2E_COVERAGE) export default defineConfig({ plugins: [ react(), - visualizer({ filename: 'dist/stats.html', open: false }) - ], + visualizer({ filename: 'dist/stats.html', open: false }), + isE2E && + istanbul({ + include: 'src/**/*', + extension: ['.js', '.ts', '.jsx', '.tsx'], + requireEnv: false, // allow instrumenting unconditionally for CI e2e builds + cypress: true, // enables window.__coverage__ instrumentation style + }), + ].filter(Boolean), resolve: { alias: { // force single copies of react/react-dom @@ -35,7 +45,7 @@ export default defineConfig({ port: 5173 }, build: { - sourcemap: false, + sourcemap: true, outDir: 'dist', // esbuild is lighter. If you want terser for final prod, run it on a beefy CI runner. minify: 'esbuild', diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..5647075 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,24 @@ +import { mergeConfig, defineConfig } from 'vitest/config' +import viteConfig from './vite.config' + +export default mergeConfig ( + viteConfig, + defineConfig({ + test: { + environment: 'jsdom', // or 'jsdom' if you use DOM env + coverage: { + provider: 'v8', // or 'c8' — these support lcov & html + reporter: [ + 'text', + ['lcov', { projectRoot: './' }], // emit SF relative to repo root + 'html' + ], + reportsDirectory: 'coverage-reports/unit', // ← your new unit folder + // optional: + // all: true, + // include: ['src/**/*.ts'], + // exclude: ['test/**'] + }, + }, + }), +)