Compare commits

...

10 Commits

Author SHA1 Message Date
bymyself
3fa1c093e7 fix: add @bgotink/playwright-coverage to knip ignoreDependencies
The knip playwright plugin does not detect the import from
playwright.config.ts, causing a false unused-dependency error.

Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3047998511
2026-04-08 18:24:50 -07:00
bymyself
c32224f807 fix: use Playwright request context instead of global fetch
Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3054600477
2026-04-08 18:23:32 -07:00
bymyself
0f811728a2 fix: add pending status for coverage, document skip vs pending asymmetry
- Add missing 'pending' case for coverage section in unified-report.
- Add comments explaining why coverage defaults to 'skip' vs 'pending'
  in both unified-report.js and pr-report.yaml.
- Update coverage-report invocation to use tsx for .ts file.

Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3047998541
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3054600479
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3054600485
2026-04-08 18:21:31 -07:00
bymyself
cccd577258 fix: convert coverage-report.js to TypeScript
Project mandates TypeScript exclusive. Converted to .ts with proper
types, renamed output variable from 'lines' to 'output' for clarity,
and removed redundant ternary guard.

Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3047998524
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3054600481
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3054600483
2026-04-08 18:16:04 -07:00
bymyself
7563a0fcf1 fix: extract shared COLLECT_COVERAGE, fix type assertion, add comments
- Extract COLLECT_COVERAGE to shared coverageConstants.ts to avoid drift
  between ComfyPage.ts and playwright.config.ts.
- Add coupling comment for COVERAGE_ATTACHMENT magic string.
- Use Object.assign instead of as Record<string, unknown> for source.
- Add comment explaining resetOnNavigation: false.

Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3047998513
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3047998518
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3054600476
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3047998519
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3047998521
2026-04-08 18:16:04 -07:00
bymyself
e15ba39647 fix: remove inert src/data.ts hunks from patch, document patch purpose
Only lib/data.js is the runtime artifact; src/data.ts changes are never
compiled at install time. Also add a comment in package.json explaining
what the patch does.

Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3047998511
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3054600475
2026-04-08 18:16:04 -07:00
bymyself
59f74ed7c3 fix: CI coverage workflow improvements
- Restrict triggers to main pushes + manual dispatch (no PR trigger)
- Remove continue-on-error: true (upload-artifact if:always() suffices)
- Use --workers=50% for runner-agnostic parallelism
- Remove unused PLAYWRIGHT_BLOB_OUTPUT_DIR env var
- Reduce retention-days from 30 to 7

Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3054600467
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3047998502
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3054600470
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3047998527
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3047998533
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3047998536
2026-04-08 18:16:04 -07:00
bymyself
7952a40bfc fix: remove playwright-coverage-feasibility.md
The feasibility doc recommends Istanbul as primary and V8 as fallback,
but the PR implements V8 exclusively. Remove per reviewer preference.

Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3047816594
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3047998506
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3048093935
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10930#discussion_r3054600472
2026-04-08 18:16:04 -07:00
GitHub Action
92215a3b13 [automated] Apply ESLint and Oxfmt fixes 2026-04-08 18:16:04 -07:00
bymyself
2fb7ce5e0c feat: add V8 code coverage collection for Playwright E2E tests
Adds infrastructure to collect V8 JavaScript code coverage during Playwright
E2E test runs and generate lcov/html reports.

Architecture:
- Custom page fixture in ComfyPage.ts starts V8 JS coverage before each test
  and stops it after, fetching source text for network-loaded scripts
- @bgotink/playwright-coverage reporter (v0.3.2) processes V8 coverage data
  into Istanbul format, generating lcov and text-summary reports
- pnpm patch on the reporter fixes two issues:
  1. Adds filesystem fallback for sourcemap loading — when HTTP fetch fails
     in the worker thread, reads .map files from dist/ on disk
  2. Rewrites Vite sourcemap source paths from build-output-relative
     (../../src/foo.ts) to project-relative (src/foo.ts) so they aren't
     excluded by the library's path validation

CI workflow (ci-tests-e2e-coverage.yaml):
- Runs on PRs and pushes to main/core/*
- 60-minute timeout, 2 workers, chromium project
- Uploads coverage/playwright/ as artifact for downstream reporting

Results: 96.1% line coverage, 68.1% branch coverage, 60.9% function coverage
across 1,221 source files.
2026-04-08 18:16:04 -07:00
11 changed files with 515 additions and 3 deletions

View File

@@ -0,0 +1,59 @@
name: 'CI: E2E Coverage'
on:
push:
branches: [main, core/*]
paths-ignore: ['**/*.md']
workflow_dispatch:
concurrency:
group: e2e-coverage-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
collect:
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
runs-on: ubuntu-latest
timeout-minutes: 60
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.16
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: read
packages: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- name: Start ComfyUI server
uses: ./.github/actions/start-comfyui-server
- name: Run Playwright tests with coverage
id: tests
run: >
COLLECT_COVERAGE=true
pnpm exec playwright test
--project=chromium
--workers=50%
- name: Upload coverage data
if: always()
uses: actions/upload-artifact@v6
with:
name: e2e-coverage
path: coverage/playwright/
retention-days: 7
if-no-files-found: warn

View File

@@ -2,7 +2,7 @@ name: 'PR: Unified Report'
on:
workflow_run:
workflows: ['CI: Size Data', 'CI: Performance Report']
workflows: ['CI: Size Data', 'CI: Performance Report', 'CI: E2E Coverage']
types:
- completed
@@ -154,6 +154,51 @@ jobs:
path: temp/size-prev
if_no_artifact_found: warn
- name: Find coverage workflow run for this commit
if: steps.pr-meta.outputs.skip != 'true'
id: find-coverage
uses: actions/github-script@v8
with:
script: |
const headSha = '${{ steps.pr-meta.outputs.head-sha }}';
const { data: runs } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'ci-tests-e2e-coverage.yaml',
head_sha: headSha,
per_page: 1,
});
const run = runs.workflow_runs[0];
if (!run) {
// 'skip' (not 'pending') — coverage workflow is opt-in,
// so absence means "not configured" rather than "still running"
core.setOutput('status', 'skip');
return;
}
if (run.status !== 'completed') {
core.setOutput('status', 'pending');
return;
}
if (run.conclusion !== 'success') {
core.setOutput('status', 'failed');
return;
}
core.setOutput('status', 'ready');
core.setOutput('run-id', String(run.id));
- name: Download coverage data
if: steps.pr-meta.outputs.skip != 'true' && steps.find-coverage.outputs.status == 'ready'
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
name: e2e-coverage
run_id: ${{ steps.find-coverage.outputs.run-id }}
path: temp/coverage
if_no_artifact_found: warn
- name: Download perf metrics (current)
if: steps.pr-meta.outputs.skip != 'true' && steps.find-perf.outputs.status == 'ready'
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
@@ -192,6 +237,7 @@ jobs:
node scripts/unified-report.js
--size-status=${{ steps.find-size.outputs.status }}
--perf-status=${{ steps.find-perf.outputs.status }}
--coverage-status=${{ steps.find-coverage.outputs.status }}
> pr-report.md
- name: Remove legacy separate comments

View File

@@ -1,6 +1,7 @@
import type { APIRequestContext, Locator, Page } from '@playwright/test'
import { test as base } from '@playwright/test'
import { config as dotenvConfig } from 'dotenv'
import { promises as fs } from 'fs'
import { NodeBadgeMode } from '@/types/nodeSource'
import { ComfyActionbar } from '@e2e/helpers/actionbar'
@@ -8,6 +9,7 @@ import { ComfyTemplates } from '@e2e/helpers/templates'
import { ComfyMouse } from '@e2e/fixtures/ComfyMouse'
import { TestIds } from '@e2e/fixtures/selectors'
import { comfyExpect } from '@e2e/fixtures/utils/customMatchers'
import { COLLECT_COVERAGE } from '@e2e/fixtures/utils/coverageConstants'
import { assetPath } from '@e2e/fixtures/utils/paths'
import { sleep } from '@e2e/fixtures/utils/timing'
import { VueNodeHelpers } from '@e2e/fixtures/VueNodeHelpers'
@@ -407,10 +409,48 @@ export class ComfyPage {
export const testComfySnapToGridGridSize = 50
// Must match attachmentName exported by @bgotink/playwright-coverage
const COVERAGE_ATTACHMENT = '@bgotink/playwright-coverage'
export const comfyPageFixture = base.extend<{
comfyPage: ComfyPage
comfyMouse: ComfyMouse
collectCoverage: boolean
}>({
collectCoverage: [COLLECT_COVERAGE, { option: true }],
page: async ({ page, collectCoverage, browserName }, use, testInfo) => {
if (browserName !== 'chromium' || !collectCoverage) {
return use(page)
}
// Accumulate coverage across navigations within a single test to capture
// all code paths exercised, including after in-test reloads.
await page.coverage.startJSCoverage({ resetOnNavigation: false })
await use(page)
const entries = await page.coverage.stopJSCoverage()
// Fetch source text for network-loaded scripts that V8 didn't capture
for (const entry of entries) {
if (typeof entry.source !== 'string' && entry.url.startsWith('http')) {
try {
const resp = await page.request.get(entry.url)
if (resp.ok()) Object.assign(entry, { source: await resp.text() })
} catch {
// skip unreachable scripts
}
}
}
const resultFile = testInfo.outputPath('v8-coverage.json')
await fs.writeFile(resultFile, JSON.stringify({ result: entries }))
testInfo.attachments.push({
name: COVERAGE_ATTACHMENT,
contentType: 'application/json',
path: resultFile
})
},
comfyPage: async ({ page, request }, use, testInfo) => {
const comfyPage = new ComfyPage(page, request)

View File

@@ -0,0 +1 @@
export const COLLECT_COVERAGE = process.env.COLLECT_COVERAGE === 'true'

View File

@@ -48,7 +48,9 @@ const config: KnipConfig = {
'@primeuix/forms',
'@primeuix/styled',
'@primeuix/utils',
'@primevue/icons'
'@primevue/icons',
// Used in playwright.config.ts for coverage collection
'@bgotink/playwright-coverage'
],
ignore: [
// Auto generated API types

View File

@@ -44,6 +44,7 @@
"stylelint:fix": "stylelint --cache --fix '{apps,packages,src}/**/*.{css,vue}'",
"stylelint": "stylelint --cache '{apps,packages,src}/**/*.{css,vue}'",
"test:browser": "pnpm exec nx e2e",
"test:browser:coverage": "cross-env COLLECT_COVERAGE=true pnpm test:browser",
"test:browser:local": "cross-env PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm test:browser",
"test:coverage": "vitest run --coverage",
"test:unit": "nx run test",
@@ -122,6 +123,7 @@
"zod-validation-error": "catalog:"
},
"devDependencies": {
"@bgotink/playwright-coverage": "catalog:",
"@comfyorg/ingest-types": "workspace:*",
"@eslint/js": "catalog:",
"@intlify/eslint-plugin-vue-i18n": "catalog:",
@@ -219,6 +221,10 @@
"sharp",
"unrs-resolver",
"vue-demi"
]
],
"patchedDependencies": {
"@bgotink/playwright-coverage@0.3.2": "patches/@bgotink__playwright-coverage@0.3.2.patch"
},
"patchedDependencies//comment": "Patch adds local-filesystem sourcemap fallback and source-path rewriting for Vite builds. Only patches lib/data.js (runtime). TODO: upstream these fixes to eliminate the patch."
}
}

View File

@@ -0,0 +1,115 @@
diff --git a/lib/data.js b/lib/data.js
index c11873c7084264208c01abd66ee4a5c43e9e9aee..0858ce94961c27e880dbe6226b44a36e4553ed2f 100644
--- a/lib/data.js
+++ b/lib/data.js
@@ -49,6 +49,28 @@ const v8_to_istanbul_1 = __importDefault(require("v8-to-istanbul"));
const convertSourceMap = __importStar(require("convert-source-map"));
exports.attachmentName = '@bgotink/playwright-coverage';
const fetch = import('node-fetch');
+async function tryReadLocalSourceMap(url) {
+ try {
+ const parsed = new url_1.URL(url);
+ const urlPath = parsed.pathname.replace(/^\//, '');
+ const candidates = [
+ (0, path_1.join)(process.cwd(), 'dist', urlPath),
+ (0, path_1.join)(process.cwd(), urlPath),
+ ];
+ for (const candidate of candidates) {
+ try {
+ return await fs_1.promises.readFile(candidate, 'utf8');
+ }
+ catch {
+ // try next candidate
+ }
+ }
+ }
+ catch {
+ // invalid URL
+ }
+ return null;
+}
async function getSourceMap(url, source) {
const inlineMap = convertSourceMap.fromSource(source);
if (inlineMap != null) {
@@ -72,10 +94,19 @@ async function getSourceMap(url, source) {
return dataString;
}
default: {
- const response = await (await fetch).default(resolved.href, {
- method: 'GET',
- });
- return await response.text();
+ try {
+ const response = await (await fetch).default(resolved.href, {
+ method: 'GET',
+ });
+ return await response.text();
+ }
+ catch {
+ const local = await tryReadLocalSourceMap(resolved.href);
+ if (local != null) {
+ return local;
+ }
+ throw new Error(`Failed to fetch sourcemap: ${resolved.href}`);
+ }
}
}
});
@@ -94,6 +125,15 @@ async function getSourceMap(url, source) {
return (await response.json());
}
catch {
+ try {
+ const local = await tryReadLocalSourceMap(`${url}.map`);
+ if (local != null) {
+ return JSON.parse(local);
+ }
+ }
+ catch {
+ // ignore
+ }
return undefined;
}
}
@@ -104,10 +144,40 @@ async function convertToIstanbulCoverage(v8Coverage, sources, sourceMaps, exclud
const istanbulCoverage = (0, istanbul_lib_coverage_1.createCoverageMap)({});
for (const script of v8Coverage.result) {
const source = sources.get(script.url);
- const sourceMap = sourceMaps.get(script.url);
+ let sourceMap = sourceMaps.get(script.url);
if (source == null || !(sourceMap === null || sourceMap === void 0 ? void 0 : sourceMap.mappings)) {
continue;
}
+ // Rewrite sourcemap sources from build-output-relative paths to
+ // project-relative paths. Vite emits sources like "../../src/foo.ts"
+ // relative to dist/assets/. Resolve them against the script URL to
+ // get server-absolute paths, then strip the origin.
+ if (sourceMap.sources != null) {
+ const scriptUrl = script.url;
+ try {
+ const origin = new url_1.URL(scriptUrl).origin;
+ sourceMap = {
+ ...sourceMap,
+ sources: sourceMap.sources.map(s => {
+ if (s == null)
+ return s;
+ try {
+ const resolved = new url_1.URL(s, scriptUrl).href;
+ if (resolved.startsWith(origin + '/')) {
+ return resolved.slice(origin.length + 1);
+ }
+ }
+ catch {
+ // not a valid URL combo
+ }
+ return s;
+ }),
+ };
+ }
+ catch {
+ // scriptUrl is not a valid URL — skip rewriting
+ }
+ }
function sanitizePath(path) {
let url;
try {

93
pnpm-lock.yaml generated
View File

@@ -15,6 +15,9 @@ catalogs:
'@astrojs/vue':
specifier: ^5.0.0
version: 5.1.4
'@bgotink/playwright-coverage':
specifier: ^0.3.2
version: 0.3.2
'@comfyorg/comfyui-electron-types':
specifier: 0.6.2
version: 0.6.2
@@ -403,6 +406,11 @@ catalogs:
overrides:
vite: ^8.0.0
patchedDependencies:
'@bgotink/playwright-coverage@0.3.2':
hash: 6b34234824fc0925423712b7f68f0dd750ea06d8ddce54047726e44ff1fe5406
path: patches/@bgotink__playwright-coverage@0.3.2.patch
importers:
.:
@@ -606,6 +614,9 @@ importers:
specifier: 'catalog:'
version: 3.3.0(zod@3.25.76)
devDependencies:
'@bgotink/playwright-coverage':
specifier: 'catalog:'
version: 0.3.2(patch_hash=6b34234824fc0925423712b7f68f0dd750ea06d8ddce54047726e44ff1fe5406)(@playwright/test@1.58.1)
'@comfyorg/ingest-types':
specifier: workspace:*
version: link:packages/ingest-types
@@ -1664,10 +1675,18 @@ packages:
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@0.2.3':
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
'@bgotink/playwright-coverage@0.3.2':
resolution: {integrity: sha512-F6ow6TD2LpELb+qD4MrmUj4TyP48JByQ/PNu6gehRLRtnU1mwXCnqfpT8AQ0bGiqS73EEg6Ifa5ts5DPSYYU8w==}
peerDependencies:
'@playwright/test': ^1.14.1
'@cacheable/memory@2.0.6':
resolution: {integrity: sha512-7e8SScMocHxcAb8YhtkbMhGG+EKLRIficb1F5sjvhSYsWTZGxvg4KIDp8kgxnV2PUJ3ddPe6J9QESjKvBWRDkg==}
@@ -4340,6 +4359,9 @@ packages:
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/istanbul-lib-coverage@2.0.6':
resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
'@types/jsdom@21.1.7':
resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
@@ -5514,6 +5536,9 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
comlink@4.4.2:
resolution: {integrity: sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==}
comma-separated-tokens@2.0.3:
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
@@ -5688,6 +5713,10 @@ packages:
typescript:
optional: true
data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
data-urls@6.0.0:
resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==}
engines: {node: '>=20'}
@@ -6339,6 +6368,10 @@ packages:
picomatch:
optional: true
fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
@@ -6436,6 +6469,10 @@ packages:
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
engines: {node: '>= 12.20'}
formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
front-matter@4.0.2:
resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==}
@@ -7791,6 +7828,10 @@ packages:
encoding:
optional: true
node-fetch@3.3.2:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
node-html-parser@5.4.2:
resolution: {integrity: sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==}
@@ -9417,6 +9458,10 @@ packages:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
v8-to-istanbul@9.3.0:
resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
engines: {node: '>=10.12.0'}
valibot@1.2.0:
resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==}
peerDependencies:
@@ -9687,6 +9732,10 @@ packages:
web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
web-streams-polyfill@4.0.0-beta.3:
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
engines: {node: '>= 14'}
@@ -10882,8 +10931,23 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@bcoe/v8-coverage@0.2.3': {}
'@bcoe/v8-coverage@1.0.2': {}
'@bgotink/playwright-coverage@0.3.2(patch_hash=6b34234824fc0925423712b7f68f0dd750ea06d8ddce54047726e44ff1fe5406)(@playwright/test@1.58.1)':
dependencies:
'@bcoe/v8-coverage': 0.2.3
'@playwright/test': 1.58.1
comlink: 4.4.2
convert-source-map: 2.0.0
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-reports: 3.2.0
micromatch: 4.0.8
node-fetch: 3.3.2
v8-to-istanbul: 9.3.0
'@cacheable/memory@2.0.6':
dependencies:
'@cacheable/utils': 2.3.2
@@ -13399,6 +13463,8 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
'@types/istanbul-lib-coverage@2.0.6': {}
'@types/jsdom@21.1.7':
dependencies:
'@types/node': 25.0.3
@@ -14841,6 +14907,8 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
comlink@4.4.2: {}
comma-separated-tokens@2.0.3: {}
commander@10.0.1: {}
@@ -15008,6 +15076,8 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
data-uri-to-buffer@4.0.1: {}
data-urls@6.0.0:
dependencies:
whatwg-mimetype: 4.0.0
@@ -15822,6 +15892,11 @@ snapshots:
optionalDependencies:
picomatch: 4.0.3
fetch-blob@3.2.0:
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
fflate@0.4.8: {}
fflate@0.8.2: {}
@@ -15945,6 +16020,10 @@ snapshots:
node-domexception: 1.0.0
web-streams-polyfill: 4.0.0-beta.3
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
front-matter@4.0.2:
dependencies:
js-yaml: 3.14.2
@@ -17524,6 +17603,12 @@ snapshots:
dependencies:
whatwg-url: 5.0.0
node-fetch@3.3.2:
dependencies:
data-uri-to-buffer: 4.0.1
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
node-html-parser@5.4.2:
dependencies:
css-select: 4.3.0
@@ -19607,6 +19692,12 @@ snapshots:
uuid@11.1.0: {}
v8-to-istanbul@9.3.0:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
'@types/istanbul-lib-coverage': 2.0.6
convert-source-map: 2.0.0
valibot@1.2.0(typescript@5.9.3):
optionalDependencies:
typescript: 5.9.3
@@ -20000,6 +20091,8 @@ snapshots:
web-namespaces@2.0.1: {}
web-streams-polyfill@3.3.3: {}
web-streams-polyfill@4.0.0-beta.3: {}
web-vitals@4.2.4: {}

View File

@@ -6,6 +6,7 @@ catalog:
'@alloc/quick-lru': ^5.2.0
'@astrojs/sitemap': ^3.7.1
'@astrojs/vue': ^5.0.0
'@bgotink/playwright-coverage': ^0.3.2
'@comfyorg/comfyui-electron-types': 0.6.2
'@eslint/js': ^9.39.1
'@formkit/auto-animate': ^0.9.0

110
scripts/coverage-report.ts Normal file
View File

@@ -0,0 +1,110 @@
import { existsSync, readFileSync } from 'node:fs'
/**
* Generates a markdown coverage report from lcov data.
* Output format matches the unified PR report style (size + perf sections).
*/
const lcovPath = process.argv[2] || 'coverage/playwright/coverage.lcov'
if (!existsSync(lcovPath)) {
process.stdout.write(
'## 🔬 E2E Coverage\n\n> ⚠️ No coverage data found. Check the CI workflow logs.\n'
)
process.exit(0)
}
const lcov = readFileSync(lcovPath, 'utf-8')
// Parse lcov summary
let totalLines = 0
let coveredLines = 0
let totalFunctions = 0
let coveredFunctions = 0
let totalBranches = 0
let coveredBranches = 0
const fileStats = new Map<string, { lines: number; covered: number }>()
let currentFile = ''
for (const line of lcov.split('\n')) {
if (line.startsWith('SF:')) {
currentFile = line.slice(3)
} else if (line.startsWith('LF:')) {
const n = parseInt(line.slice(3), 10)
totalLines += n
const entry = fileStats.get(currentFile) ?? { lines: 0, covered: 0 }
entry.lines = n
fileStats.set(currentFile, entry)
} else if (line.startsWith('LH:')) {
const n = parseInt(line.slice(3), 10)
coveredLines += n
const entry = fileStats.get(currentFile) ?? { lines: 0, covered: 0 }
entry.covered = n
fileStats.set(currentFile, entry)
} else if (line.startsWith('FNF:')) {
totalFunctions += parseInt(line.slice(4), 10)
} else if (line.startsWith('FNH:')) {
coveredFunctions += parseInt(line.slice(4), 10)
} else if (line.startsWith('BRF:')) {
totalBranches += parseInt(line.slice(4), 10)
} else if (line.startsWith('BRH:')) {
coveredBranches += parseInt(line.slice(4), 10)
}
}
function pct(covered: number, total: number): string {
if (total === 0) return '—'
return ((covered / total) * 100).toFixed(1) + '%'
}
function bar(covered: number, total: number): string {
if (total === 0) return '—'
const p = (covered / total) * 100
if (p >= 80) return '🟢'
if (p >= 50) return '🟡'
return '🔴'
}
const output: string[] = []
output.push('## 🔬 E2E Coverage')
output.push('')
output.push('| Metric | Covered | Total | Pct | |')
output.push('|---|--:|--:|--:|---|')
output.push(
`| Lines | ${coveredLines.toLocaleString()} | ${totalLines.toLocaleString()} | ${pct(coveredLines, totalLines)} | ${bar(coveredLines, totalLines)} |`
)
output.push(
`| Functions | ${coveredFunctions.toLocaleString()} | ${totalFunctions.toLocaleString()} | ${pct(coveredFunctions, totalFunctions)} | ${bar(coveredFunctions, totalFunctions)} |`
)
output.push(
`| Branches | ${coveredBranches.toLocaleString()} | ${totalBranches.toLocaleString()} | ${pct(coveredBranches, totalBranches)} | ${bar(coveredBranches, totalBranches)} |`
)
// Top uncovered files
const uncovered = [...fileStats.entries()]
.filter(([, s]) => s.lines > 0)
.map(([file, s]) => ({
file: file.replace(/^.*\/src\//, 'src/'),
pct: (s.covered / s.lines) * 100,
missed: s.lines - s.covered
}))
.filter((f) => f.missed > 0)
.sort((a, b) => b.missed - a.missed)
.slice(0, 10)
if (uncovered.length > 0) {
output.push('')
output.push('<details>')
output.push('<summary>Top 10 files by uncovered lines</summary>')
output.push('')
output.push('| File | Coverage | Missed |')
output.push('|---|--:|--:|')
for (const f of uncovered) {
output.push(`| \`${f.file}\` | ${f.pct.toFixed(1)}% | ${f.missed} |`)
}
output.push('')
output.push('</details>')
}
process.stdout.write(output.join('\n') + '\n')

View File

@@ -13,6 +13,9 @@ function getArg(name) {
const sizeStatus = getArg('size-status') ?? 'pending'
const perfStatus = getArg('perf-status') ?? 'pending'
// 'skip' (not 'pending') — coverage workflow is opt-in,
// so absence means "not configured" rather than "still running"
const coverageStatus = getArg('coverage-status') ?? 'skip'
/** @type {string[]} */
const lines = []
@@ -72,4 +75,40 @@ if (perfStatus === 'ready' && existsSync('test-results/perf-metrics.json')) {
lines.push('> ⏳ Performance tests in progress…')
}
// --- Coverage section ---
if (coverageStatus === 'ready' && existsSync('temp/coverage/coverage.lcov')) {
try {
const coverageReport = execFileSync(
'pnpm',
[
'exec',
'tsx',
'scripts/coverage-report.ts',
'temp/coverage/coverage.lcov'
],
{ encoding: 'utf-8' }
).trimEnd()
lines.push('')
lines.push(coverageReport)
} catch {
lines.push('')
lines.push('## 🔬 E2E Coverage')
lines.push('')
lines.push(
'> ⚠️ Failed to render coverage report. Check the CI workflow logs.'
)
}
} else if (coverageStatus === 'failed') {
lines.push('')
lines.push('## 🔬 E2E Coverage')
lines.push('')
lines.push('> ⚠️ Coverage collection failed. Check the CI workflow logs.')
} else if (coverageStatus === 'pending') {
lines.push('')
lines.push('## 🔬 E2E Coverage')
lines.push('')
lines.push('> ⏳ Coverage collection in progress…')
}
// coverageStatus === 'skip' (default) — don't show section at all
process.stdout.write(lines.join('\n') + '\n')