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",
|
"test": "vitest run",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "eslint .",
|
"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-css-audit": "node scripts/audit-css.mjs",
|
||||||
"refs-dom-audit": "node scripts/audit-dom.mjs"
|
"refs-dom-audit": "node scripts/audit-dom.mjs"
|
||||||
},
|
},
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.63.0",
|
"@sveltejs/kit": "^2.63.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||||
|
"diff": "^9.0.0",
|
||||||
"eslint": "^10.9.0",
|
"eslint": "^10.9.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-svelte": "^3.23.0",
|
"eslint-plugin-svelte": "^3.23.0",
|
||||||
|
|||||||
Generated
+9
@@ -30,6 +30,9 @@ importers:
|
|||||||
'@sveltejs/vite-plugin-svelte':
|
'@sveltejs/vite-plugin-svelte':
|
||||||
specifier: ^7.1.2
|
specifier: ^7.1.2
|
||||||
version: 7.3.0(svelte@5.56.9(@typescript-eslint/types@8.67.0))(vite@8.2.1)
|
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:
|
eslint:
|
||||||
specifier: ^10.9.0
|
specifier: ^10.9.0
|
||||||
version: 10.9.0
|
version: 10.9.0
|
||||||
@@ -493,6 +496,10 @@ packages:
|
|||||||
devalue@5.9.0:
|
devalue@5.9.0:
|
||||||
resolution: {integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==}
|
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:
|
es-module-lexer@2.3.2:
|
||||||
resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
|
resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
|
||||||
|
|
||||||
@@ -1507,6 +1514,8 @@ snapshots:
|
|||||||
|
|
||||||
devalue@5.9.0: {}
|
devalue@5.9.0: {}
|
||||||
|
|
||||||
|
diff@9.0.0: {}
|
||||||
|
|
||||||
es-module-lexer@2.3.2: {}
|
es-module-lexer@2.3.2: {}
|
||||||
|
|
||||||
escape-string-regexp@4.0.0: {}
|
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