Files
ComfyUI_frontend/patches/@bgotink__playwright-coverage@0.3.2.patch
bymyself e36a5cd1fc 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-07 13:24:58 -07:00

243 lines
8.4 KiB
Diff

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 {
diff --git a/src/data.ts b/src/data.ts
index 539a70e5bde5c2ab8644b0bfa3ff52625cf4490e..29bc26bf729cbf9496594d9b9e91745a095658c5 100644
--- a/src/data.ts
+++ b/src/data.ts
@@ -12,6 +12,33 @@ export const attachmentName = '@bgotink/playwright-coverage';
const fetch = import('node-fetch');
+/**
+ * Try to read a sourcemap from the local filesystem by mapping a URL path
+ * (e.g. /assets/index-abc.js.map) to a local file (e.g. dist/assets/index-abc.js.map).
+ * Falls back to common build output directories.
+ */
+async function tryReadLocalSourceMap(url: string): Promise<string | null> {
+ try {
+ const parsed = new URL(url);
+ // Try mapping URL pathname to dist/ directory
+ const urlPath = parsed.pathname.replace(/^\//, '');
+ const candidates = [
+ join(process.cwd(), 'dist', urlPath),
+ join(process.cwd(), urlPath),
+ ];
+ for (const candidate of candidates) {
+ try {
+ return await fs.readFile(candidate, 'utf8');
+ } catch {
+ // try next candidate
+ }
+ }
+ } catch {
+ // invalid URL
+ }
+ return null;
+}
+
export async function getSourceMap(
url: string,
source: string,
@@ -44,13 +71,23 @@ export async function getSourceMap(
return dataString;
}
default: {
- const response = await (
- await fetch
- ).default(resolved.href, {
- method: 'GET',
- });
+ // Try HTTP fetch first, fall back to reading from local filesystem
+ try {
+ const response = await (
+ await fetch
+ ).default(resolved.href, {
+ method: 'GET',
+ });
- return await response.text();
+ return await response.text();
+ } catch {
+ // HTTP fetch failed — try reading from local dist/ directory
+ const local = await tryReadLocalSourceMap(resolved.href);
+ if (local != null) {
+ return local;
+ }
+ throw new Error(`Failed to fetch sourcemap: ${resolved.href}`);
+ }
}
}
},
@@ -73,6 +110,15 @@ export async function getSourceMap(
return (await response.json()) as EncodedSourceMap;
} catch {
+ // HTTP fetch failed — try reading from local dist/ directory
+ try {
+ const local = await tryReadLocalSourceMap(`${url}.map`);
+ if (local != null) {
+ return JSON.parse(local) as EncodedSourceMap;
+ }
+ } catch {
+ // ignore
+ }
return undefined;
}
}
@@ -102,12 +148,41 @@ export async function convertToIstanbulCoverage(
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?.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 to get
+ // project-relative paths (e.g. "src/foo.ts").
+ if (sourceMap.sources != null) {
+ const scriptUrl = script.url;
+ try {
+ const origin = new URL(scriptUrl).origin;
+ sourceMap = {
+ ...sourceMap,
+ sources: sourceMap.sources.map(s => {
+ if (s == null) return s;
+ try {
+ const resolved = new 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: string) {
let url;