mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-14 19:51:05 +00:00
Compare commits
10 Commits
test/cover
...
playwright
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fa1c093e7 | ||
|
|
c32224f807 | ||
|
|
0f811728a2 | ||
|
|
cccd577258 | ||
|
|
7563a0fcf1 | ||
|
|
e15ba39647 | ||
|
|
59f74ed7c3 | ||
|
|
7952a40bfc | ||
|
|
92215a3b13 | ||
|
|
2fb7ce5e0c |
59
.github/workflows/ci-tests-e2e-coverage.yaml
vendored
Normal file
59
.github/workflows/ci-tests-e2e-coverage.yaml
vendored
Normal 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
|
||||
48
.github/workflows/pr-report.yaml
vendored
48
.github/workflows/pr-report.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
1
browser_tests/fixtures/utils/coverageConstants.ts
Normal file
1
browser_tests/fixtures/utils/coverageConstants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const COLLECT_COVERAGE = process.env.COLLECT_COVERAGE === 'true'
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
115
patches/@bgotink__playwright-coverage@0.3.2.patch
Normal file
115
patches/@bgotink__playwright-coverage@0.3.2.patch
Normal 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
93
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
@@ -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
110
scripts/coverage-report.ts
Normal 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')
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user