mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-03 12:42:01 +00:00
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.
243 lines
8.4 KiB
Diff
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;
|
|
|