mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 13:36:36 +00:00
chore: add chrome devtools protocol audit
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@
|
||||
"test": "vitest run",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint .",
|
||||
"refs-cdp-audit": "node scripts/audit-cdp.mjs http://localhost:3000/demo.html http://localhost:5173/preview/demo",
|
||||
"refs-css-audit": "node scripts/audit-css.mjs",
|
||||
"refs-dom-audit": "node scripts/audit-dom.mjs"
|
||||
},
|
||||
@@ -21,6 +22,7 @@
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.63.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||
"diff": "^9.0.0",
|
||||
"eslint": "^10.9.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.23.0",
|
||||
|
||||
Generated
+9
@@ -30,6 +30,9 @@ importers:
|
||||
'@sveltejs/vite-plugin-svelte':
|
||||
specifier: ^7.1.2
|
||||
version: 7.3.0(svelte@5.56.9(@typescript-eslint/types@8.67.0))(vite@8.2.1)
|
||||
diff:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0
|
||||
eslint:
|
||||
specifier: ^10.9.0
|
||||
version: 10.9.0
|
||||
@@ -493,6 +496,10 @@ packages:
|
||||
devalue@5.9.0:
|
||||
resolution: {integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==}
|
||||
|
||||
diff@9.0.0:
|
||||
resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
|
||||
es-module-lexer@2.3.2:
|
||||
resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
|
||||
|
||||
@@ -1507,6 +1514,8 @@ snapshots:
|
||||
|
||||
devalue@5.9.0: {}
|
||||
|
||||
diff@9.0.0: {}
|
||||
|
||||
es-module-lexer@2.3.2: {}
|
||||
|
||||
escape-string-regexp@4.0.0: {}
|
||||
|
||||
@@ -0,0 +1,609 @@
|
||||
// audit-cdp.mjs
|
||||
// CDP/DOMSnapshot-based visual audit over multiple routes.
|
||||
// Replicates the route→ref mapping from audit-css.mjs (the 4 targets that have
|
||||
// a corresponding ref HTML file), starts a dev server (unless --no-serve),
|
||||
// captures each pair, diffs computed styles + geometry, and writes a report to
|
||||
// web/audit/cdp-audit.txt.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/audit-cdp.mjs # serve + audit all 4 targets
|
||||
// node scripts/audit-cdp.mjs --no-serve # assume a server is already running
|
||||
// node scripts/audit-cdp.mjs --ours http://127.0.0.1:5173
|
||||
// node scripts/audit-cdp.mjs --route /preview/demo --ref demo.html
|
||||
import { chromium } from "playwright";
|
||||
import { diffArrays } from "diff";
|
||||
import { spawn } from "node:child_process";
|
||||
import { writeFileSync, mkdirSync, readdirSync } from "node:fs";
|
||||
import { resolve, join } from "node:path";
|
||||
|
||||
const argv = (() => {
|
||||
const a = {};
|
||||
for (let i = 0; i < process.argv.length; i++) {
|
||||
const v = process.argv[i];
|
||||
if (v === "--no-serve") a.noServe = true;
|
||||
else if (v === "--ours") a.ours = process.argv[++i];
|
||||
else if (v === "--ref") a.ref = process.argv[++i];
|
||||
else if (v === "--route") a.route = process.argv[++i];
|
||||
else if (v === "--port") a.port = Number(process.argv[++i]);
|
||||
}
|
||||
return a;
|
||||
})();
|
||||
|
||||
const WEB = process.cwd();
|
||||
const PORT = argv.port || 5179;
|
||||
const REFS_DIR = resolve(WEB, "../refs-html");
|
||||
const PX_TOL = 1;
|
||||
|
||||
const PROPS = [
|
||||
"display",
|
||||
"position",
|
||||
"top",
|
||||
"right",
|
||||
"bottom",
|
||||
"left",
|
||||
"z-index",
|
||||
"float",
|
||||
"flex-direction",
|
||||
"flex-wrap",
|
||||
"justify-content",
|
||||
"align-items",
|
||||
"align-self",
|
||||
"row-gap",
|
||||
"column-gap",
|
||||
"grid-template-columns",
|
||||
"grid-template-rows",
|
||||
"margin-top",
|
||||
"margin-right",
|
||||
"margin-bottom",
|
||||
"margin-left",
|
||||
"padding-top",
|
||||
"padding-right",
|
||||
"padding-bottom",
|
||||
"padding-left",
|
||||
"width",
|
||||
"height",
|
||||
"min-width",
|
||||
"min-height",
|
||||
"max-width",
|
||||
"max-height",
|
||||
"box-sizing",
|
||||
"overflow",
|
||||
"opacity",
|
||||
"visibility",
|
||||
"transform",
|
||||
"box-shadow",
|
||||
"border-top-width",
|
||||
"border-top-style",
|
||||
"border-top-color",
|
||||
"border-radius",
|
||||
"background-color",
|
||||
"background-image",
|
||||
"background-size",
|
||||
"background-position",
|
||||
"color",
|
||||
"font-family",
|
||||
"font-size",
|
||||
"font-weight",
|
||||
"font-style",
|
||||
"line-height",
|
||||
"letter-spacing",
|
||||
"text-align",
|
||||
"text-transform",
|
||||
"white-space",
|
||||
"vertical-align",
|
||||
];
|
||||
|
||||
// --- capture: open a page and snapshot its tree via getComputedStyle ---
|
||||
|
||||
async function capture(browser, target) {
|
||||
const url = target.startsWith("http")
|
||||
? target
|
||||
: "file:///" + target.replace(/\\/g, "/");
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 1440, height: 900 },
|
||||
});
|
||||
await page.goto(url, { waitUntil: "networkidle" });
|
||||
await page.evaluate(async () => {
|
||||
await document.fonts.ready;
|
||||
});
|
||||
// Normalize both sides to light theme so we measure real design diffs,
|
||||
// not a theme inversion.
|
||||
await page.evaluate(() => {
|
||||
document.documentElement.dataset.theme = "light";
|
||||
});
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
const tree = await page.evaluate((PROPS) => {
|
||||
const NOISE_TAGS = new Set([
|
||||
"script",
|
||||
"template",
|
||||
"style",
|
||||
"link",
|
||||
"noscript",
|
||||
]);
|
||||
const snap = (el) => {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
if (
|
||||
NOISE_TAGS.has(tag) ||
|
||||
tag === "vite-error-overlay" ||
|
||||
el.id === "svelte-announcer"
|
||||
)
|
||||
return null;
|
||||
const cs = getComputedStyle(el);
|
||||
const r = el.getBoundingClientRect();
|
||||
return {
|
||||
tag,
|
||||
classes: (el.getAttribute("class") ?? "")
|
||||
.split(/\s+/)
|
||||
.filter((c) => c && !c.startsWith("svelte-")),
|
||||
text: [...el.childNodes]
|
||||
.filter((n) => n.nodeType === 3)
|
||||
.map((n) => n.textContent.trim())
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
box: {
|
||||
x: +r.x.toFixed(1),
|
||||
y: +r.y.toFixed(1),
|
||||
w: +r.width.toFixed(1),
|
||||
h: +r.height.toFixed(1),
|
||||
},
|
||||
styles: Object.fromEntries(
|
||||
PROPS.map((p) => [p, cs.getPropertyValue(p)]),
|
||||
),
|
||||
children:
|
||||
tag === "svg"
|
||||
? []
|
||||
: [...el.children].map(snap).filter(Boolean),
|
||||
};
|
||||
};
|
||||
return snap(document.body);
|
||||
}, PROPS);
|
||||
|
||||
await page.close();
|
||||
return tree;
|
||||
}
|
||||
|
||||
// --- capture via CDP DOMSnapshot (alternative path, kept for reference) ---
|
||||
|
||||
async function captureCdp(browser, target) {
|
||||
const url = target.startsWith("http")
|
||||
? target
|
||||
: "file:///" + target.replace(/\\/g, "/");
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 1440, height: 900 },
|
||||
});
|
||||
await page.goto(url, { waitUntil: "networkidle" });
|
||||
await page.evaluate(async () => {
|
||||
await document.fonts.ready;
|
||||
});
|
||||
const cdp = await page.context().newCDPSession(page);
|
||||
const snap = await cdp.send("DOMSnapshot.captureSnapshot", {
|
||||
computedStyles: PROPS,
|
||||
});
|
||||
await page.close();
|
||||
if (!snap?.documents?.length)
|
||||
throw new Error(
|
||||
"captureSnapshot: no documents, keys: " + Object.keys(snap ?? {}),
|
||||
);
|
||||
return toTree(snap);
|
||||
}
|
||||
|
||||
function toTree(snap) {
|
||||
const S = snap.strings;
|
||||
const doc = snap.documents[0];
|
||||
const nodes = doc.nodes;
|
||||
const layout = doc.layout;
|
||||
const deref = (v) => (typeof v === "number" ? S[v] : v);
|
||||
|
||||
if (layout.nodeIndex.length !== layout.bounds.length)
|
||||
throw new Error("toTree: nodeIndex and bounds are not parallel");
|
||||
|
||||
// styles: flat array of VALUE indices, positional to PROPS.
|
||||
// Property names are not stored in strings — mapping only via whitelist order.
|
||||
const stylesByNode = new Map();
|
||||
for (let k = 0; k < layout.nodeIndex.length; k++) {
|
||||
const raw = layout.styles[k] ?? [];
|
||||
if (raw.length && raw.length !== PROPS.length)
|
||||
console.warn(
|
||||
`toTree: node ${layout.nodeIndex[k]}: ${raw.length} values ` +
|
||||
`for ${PROPS.length} requested — possible shift, node styles unreliable`,
|
||||
);
|
||||
const styles = {};
|
||||
PROPS.forEach((prop, j) => {
|
||||
if (raw[j] != null) styles[prop] = S[raw[j]] ?? "";
|
||||
});
|
||||
stylesByNode.set(layout.nodeIndex[k], styles);
|
||||
}
|
||||
|
||||
const boxByNode = new Map(
|
||||
layout.nodeIndex.map((domIdx, k) => {
|
||||
const b = layout.bounds[k];
|
||||
return [
|
||||
domIdx,
|
||||
{
|
||||
x: +b[0].toFixed(1),
|
||||
y: +b[1].toFixed(1),
|
||||
w: +b[2].toFixed(1),
|
||||
h: +b[3].toFixed(1),
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
const pseudoByNode = new Set(nodes.pseudoType?.index ?? []);
|
||||
|
||||
const kids = new Map();
|
||||
for (let i = 0; i < nodes.nodeType.length; i++) {
|
||||
const p = nodes.parentIndex?.[i];
|
||||
if (p == null || p < 0) continue;
|
||||
if (!kids.has(p)) kids.set(p, []);
|
||||
kids.get(p).push(i);
|
||||
}
|
||||
|
||||
const NOISE_TAGS = new Set([
|
||||
"script",
|
||||
"template",
|
||||
"style",
|
||||
"link",
|
||||
"noscript",
|
||||
]);
|
||||
|
||||
const build = (i) => {
|
||||
if (nodes.nodeType[i] !== 1 || pseudoByNode.has(i)) return null;
|
||||
const a = nodes.attributes?.[i] ?? [];
|
||||
const attrs = {};
|
||||
for (let k = 0; k + 1 < a.length; k += 2)
|
||||
attrs[deref(a[k])] = deref(a[k + 1]);
|
||||
const tag = deref(nodes.nodeName[i]).toLowerCase();
|
||||
if (
|
||||
NOISE_TAGS.has(tag) ||
|
||||
tag === "vite-error-overlay" ||
|
||||
attrs.id === "svelte-announcer"
|
||||
)
|
||||
return null;
|
||||
const text = (kids.get(i) ?? [])
|
||||
.filter((c) => nodes.nodeType[c] === 3)
|
||||
.map((c) => (deref(nodes.nodeValue[c]) ?? "").trim())
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
return {
|
||||
tag: deref(nodes.nodeName[i]).toLowerCase(),
|
||||
classes: (attrs.class ?? "")
|
||||
.split(/\s+/)
|
||||
.filter((c) => c && !c.startsWith("svelte-")),
|
||||
text,
|
||||
box: boxByNode.get(i), // undefined = node not rendered
|
||||
styles: stylesByNode.get(i) ?? {},
|
||||
children:
|
||||
tag === "svg"
|
||||
? []
|
||||
: (kids.get(i) ?? []).map(build).filter(Boolean),
|
||||
};
|
||||
};
|
||||
|
||||
const rootIdx = nodes.nodeType.indexOf(9);
|
||||
const htmlIdx = (kids.get(rootIdx) ?? []).find(
|
||||
(i) => nodes.nodeType[i] === 1,
|
||||
);
|
||||
const bodyIdx = (kids.get(htmlIdx) ?? []).find(
|
||||
(i) => nodes.nodeType[i] === 1 && deref(nodes.nodeName[i]) === "BODY",
|
||||
);
|
||||
const tree = build(bodyIdx);
|
||||
if (!tree) throw new Error("toTree: body not found");
|
||||
// At least one rendered node must have the full set of values.
|
||||
if (
|
||||
![...stylesByNode.values()].some(
|
||||
(s) => Object.keys(s).length === PROPS.length,
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
"toTree: no node has a full set of styles — whitelist/indexing broken",
|
||||
);
|
||||
return tree;
|
||||
}
|
||||
|
||||
// node signature for matching: tag + classes (no svelte suffixes), or text
|
||||
const sig = (n) =>
|
||||
n.tag +
|
||||
(n.classes.length
|
||||
? "." + [...n.classes].sort().join(".")
|
||||
: n.text
|
||||
? ":" + n.text.slice(0, 30)
|
||||
: "");
|
||||
|
||||
const sameNode = (x, y) => {
|
||||
if (x.tag !== y.tag) return false;
|
||||
if (x.tag === "svg") return true; // icons: design vs library classes differ
|
||||
const cx = [...x.classes].sort().join(" ");
|
||||
const cy = [...y.classes].sort().join(" ");
|
||||
return cx === cy || !x.classes.length || !y.classes.length;
|
||||
};
|
||||
|
||||
// CSS selector segment: nth-of-type only when several such tags under parent
|
||||
const seg = (node, i, siblings) => {
|
||||
if (siblings.filter((c) => c.tag === node.tag).length < 2) return node.tag;
|
||||
const nth = siblings.slice(0, i + 1).filter((c) => c.tag === node.tag).length;
|
||||
return `${node.tag}:nth-of-type(${nth})`;
|
||||
};
|
||||
|
||||
// human-readable annotation: classes + text, to make the node easy to find
|
||||
const annotate = (node) => {
|
||||
if (!node) return "";
|
||||
const cls = node.classes.length
|
||||
? " [" + [...node.classes].sort().join(" ") + "]"
|
||||
: "";
|
||||
const txt = node.text ? ` "${node.text.slice(0, 40)}"` : "";
|
||||
return cls + txt;
|
||||
};
|
||||
|
||||
function diffStyles(a, b) {
|
||||
const out = [];
|
||||
if (!a?.styles || !b?.styles)
|
||||
return [
|
||||
`⚠ node without styles (a.tag=${a?.tag}, b.tag=${b?.tag}) — matching bug, inspect manually`,
|
||||
];
|
||||
|
||||
if (!!a.box !== !!b.box)
|
||||
out.push(
|
||||
`render: node is rendered only on one side ` +
|
||||
`(no layout box — usually display:none on one side)`,
|
||||
);
|
||||
if (a.box && b.box)
|
||||
for (const k of ["x", "y", "w", "h"]) {
|
||||
const d = +(b.box[k] - a.box[k]).toFixed(1);
|
||||
if (Math.abs(d) > PX_TOL)
|
||||
out.push(
|
||||
`${k}: ${a.box[k]} → ${b.box[k]} (${d > 0 ? "+" : ""}${d}px)`,
|
||||
);
|
||||
}
|
||||
for (const k of new Set([
|
||||
...Object.keys(a.styles),
|
||||
...Object.keys(b.styles),
|
||||
])) {
|
||||
const va = a.styles[k] ?? "",
|
||||
vb = b.styles[k] ?? "";
|
||||
if (va !== vb) out.push(`${k}: ${va || "—"} → ${vb || "—"}`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function compare(a, b, path, report) {
|
||||
const sel = path.join(" > ");
|
||||
const changes = diffStyles(a, b);
|
||||
if (changes.length)
|
||||
report.push(
|
||||
sel +
|
||||
annotate(b ?? a) +
|
||||
"\n" +
|
||||
changes.map((c) => " " + c).join("\n"),
|
||||
);
|
||||
|
||||
const removed = [],
|
||||
added = [];
|
||||
let ai = 0,
|
||||
bi = 0;
|
||||
for (const g of diffArrays(a.children, b.children, {
|
||||
comparator: sameNode,
|
||||
})) {
|
||||
if (g.removed)
|
||||
for (let k = 0; k < g.count; k++, ai++)
|
||||
removed.push([a.children[ai], ai]);
|
||||
else if (g.added)
|
||||
for (let k = 0; k < g.count; k++, bi++) added.push([b.children[bi], bi]);
|
||||
else
|
||||
for (let k = 0; k < g.count; k++, ai++, bi++)
|
||||
compare(a.children[ai], b.children[bi], [
|
||||
...path,
|
||||
seg(a.children[ai], ai, a.children),
|
||||
], report);
|
||||
}
|
||||
|
||||
// rescue: pair leftovers by tag instead of reporting ✗/+
|
||||
const rest = [];
|
||||
for (const [node, i] of removed) {
|
||||
const j = added.findIndex(([n]) => n.tag === node.tag);
|
||||
if (j < 0) {
|
||||
rest.push([node, i]);
|
||||
continue;
|
||||
}
|
||||
const [[bNode]] = added.splice(j, 1);
|
||||
compare(node, bNode, [...path, seg(node, i, a.children)], report);
|
||||
}
|
||||
for (const [node, i] of rest)
|
||||
report.push(
|
||||
`${[...path, seg(node, i, a.children)].join(" >")}${annotate(node)}\n ✗ present in reference, absent in project`,
|
||||
);
|
||||
for (const [node, i] of added)
|
||||
report.push(
|
||||
`${[...path, seg(node, i, b.children)].join(" >")}${annotate(node)}\n + present in project, absent in reference`,
|
||||
);
|
||||
}
|
||||
|
||||
// --- target discovery (mirrors audit-css.mjs) ---
|
||||
|
||||
// Dynamic segments: explicit id values + id→ref-file mapping.
|
||||
const DYNAMIC = {
|
||||
"tools/[id]": {
|
||||
ids: ["linear-gradient-png", "remove-background-png"],
|
||||
refFor: (id) =>
|
||||
id === "linear-gradient-png"
|
||||
? "gradient.html"
|
||||
: id === "remove-background-png"
|
||||
? "background-remover.html"
|
||||
: null,
|
||||
},
|
||||
};
|
||||
|
||||
const EXCLUDE = new Set(["/preview"]);
|
||||
const refNameFor = (route) => {
|
||||
if (route === "/preview") return "index.html";
|
||||
const last = route.split("/").filter(Boolean).pop();
|
||||
return `${last}.html`;
|
||||
};
|
||||
|
||||
function discoverRoutes() {
|
||||
const base = resolve(WEB, "src/routes/preview");
|
||||
const out = [];
|
||||
const walk = (dir) => {
|
||||
let ents;
|
||||
try {
|
||||
ents = readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const e of ents) {
|
||||
const p = join(dir, e.name);
|
||||
if (e.isDirectory()) walk(p);
|
||||
else if (e.name === "+page.svelte") {
|
||||
const rel = p.slice(base.length).replace(/\\/g, "/");
|
||||
const parts = rel.split("/").filter(Boolean);
|
||||
parts.pop();
|
||||
out.push(parts.length ? `/preview/${parts.join("/")}` : "/preview");
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(base);
|
||||
return out;
|
||||
}
|
||||
|
||||
function refsList() {
|
||||
try {
|
||||
return readdirSync(REFS_DIR).filter((f) => f.endsWith(".html"));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const refExists = (name) => refsList().includes(name);
|
||||
|
||||
function buildTargets() {
|
||||
if (argv.route) {
|
||||
const ref = argv.ref || refNameFor(argv.route);
|
||||
return [{ route: argv.route, ref }];
|
||||
}
|
||||
const targets = [];
|
||||
const seen = new Set();
|
||||
const add = (route, ref) => {
|
||||
if (!ref || seen.has(route)) return;
|
||||
if (!refExists(ref)) {
|
||||
console.warn(`[cdp-audit] ref not found, skipped: ${route} -> ${ref}`);
|
||||
return;
|
||||
}
|
||||
seen.add(route);
|
||||
targets.push({ route, ref });
|
||||
};
|
||||
for (const route of discoverRoutes()) {
|
||||
if (route.includes("[")) continue;
|
||||
if (EXCLUDE.has(route)) {
|
||||
console.log(`[cdp-audit] excluded (no relevant ref): ${route}`);
|
||||
continue;
|
||||
}
|
||||
add(route, refNameFor(route));
|
||||
}
|
||||
for (const [seg, cfg] of Object.entries(DYNAMIC)) {
|
||||
for (const id of cfg.ids) {
|
||||
add(`/preview/${seg.replace("[id]", id)}`, cfg.refFor(id));
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
const waitFor = async (url, ms = 60000) => {
|
||||
const t = Date.now();
|
||||
while (Date.now() - t < ms) {
|
||||
try {
|
||||
const r = await fetch(url);
|
||||
if (r.ok) return;
|
||||
if (r.status >= 400) {
|
||||
throw new Error(
|
||||
`route ${url} returned HTTP ${r.status} (server is up, page failed to render)`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message.includes("returned HTTP")) throw e;
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
throw new Error("dev server not up: " + url);
|
||||
};
|
||||
|
||||
async function auditOne(browser, target) {
|
||||
const oursUrl = argv.ours || `http://127.0.0.1:${PORT}${target.route}`;
|
||||
const refUrl = resolve(REFS_DIR, target.ref);
|
||||
|
||||
const ours = await capture(browser, oursUrl);
|
||||
const ref = await capture(browser, refUrl);
|
||||
|
||||
const report = [];
|
||||
compare(ref, ours, ["body"], report);
|
||||
return { route: target.route, ref: target.ref, lines: report };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const targets = buildTargets();
|
||||
if (!targets.length) {
|
||||
console.error("no audit targets (refs-html/*.html missing?)");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`[cdp-audit] targets: ${targets.map((t) => t.route).join(", ")}`);
|
||||
|
||||
let server;
|
||||
if (!argv.noServe) {
|
||||
const bin = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
||||
server = spawn(bin, ["dev", "--port", String(PORT), "--strictPort"], {
|
||||
cwd: WEB,
|
||||
stdio: "ignore",
|
||||
shell: true,
|
||||
detached: process.platform !== "win32",
|
||||
});
|
||||
await waitFor(argv.ours || `http://127.0.0.1:${PORT}${targets[0].route}`);
|
||||
}
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const reports = [];
|
||||
try {
|
||||
for (const target of targets) {
|
||||
try {
|
||||
const rep = await auditOne(browser, target);
|
||||
reports.push(rep);
|
||||
console.log(
|
||||
`[cdp-audit] ${rep.route} (vs ${rep.ref}): ${rep.lines.length} differences`,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(`[cdp-audit] ${target.route} failed: ${e.message}`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
if (server) {
|
||||
try {
|
||||
if (process.platform === "win32")
|
||||
spawn("taskkill", ["/pid", String(server.pid), "/f", "/t"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
else server.kill("SIGTERM");
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
const header =
|
||||
`CDP visual audit\n${new Date().toISOString()}\n` +
|
||||
`viewport 1440x900, tolerance ${PX_TOL}px, theme light\n\n`;
|
||||
const body = reports
|
||||
.map((r) => {
|
||||
const title = `=== ${r.route} (vs ${r.ref}) — ${r.lines.length} differences ===`;
|
||||
if (!r.lines.length) return title + "\n ✅ no differences found";
|
||||
return title + "\n\n" + r.lines.join("\n\n");
|
||||
})
|
||||
.join("\n\n\n");
|
||||
|
||||
const outDir = resolve(WEB, "audit");
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const outPath = resolve(outDir, "cdp-audit.txt");
|
||||
writeFileSync(outPath, header + body + "\n", "utf8");
|
||||
console.log(`report: web/audit/cdp-audit.txt`);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user