chore: update audit cdp - group margin\padding\gap
This commit is contained in:
+206
-22
@@ -126,6 +126,15 @@ const PROPS = [
|
||||
"border-top-width",
|
||||
"border-top-style",
|
||||
"border-top-color",
|
||||
"border-right-width",
|
||||
"border-right-style",
|
||||
"border-right-color",
|
||||
"border-bottom-width",
|
||||
"border-bottom-style",
|
||||
"border-bottom-color",
|
||||
"border-left-width",
|
||||
"border-left-style",
|
||||
"border-left-color",
|
||||
"border-radius",
|
||||
"background-color",
|
||||
"background-image",
|
||||
@@ -207,6 +216,7 @@ async function capture(browser, target, viewport) {
|
||||
PROPS,
|
||||
NOISE_TAGS: [...NOISE_TAGS],
|
||||
INHERITED: [...INHERITED],
|
||||
BOX_GROUPS,
|
||||
});
|
||||
await page.close();
|
||||
return tree;
|
||||
@@ -217,7 +227,7 @@ async function capture(browser, target, viewport) {
|
||||
// declares after cascade + inheritance + var() resolution — alongside the
|
||||
// computed value from getComputedStyle. Runs inside the page via
|
||||
// page.evaluate, so it must stay self-contained (browser globals + the arg).
|
||||
function snapshotDocument({ PROPS, NOISE_TAGS, INHERITED }) {
|
||||
function snapshotDocument({ PROPS, NOISE_TAGS, INHERITED, BOX_GROUPS }) {
|
||||
// page.evaluate hand-serialized the Sets to plain arrays — restore them.
|
||||
NOISE_TAGS = new Set(NOISE_TAGS);
|
||||
INHERITED = new Set(INHERITED);
|
||||
@@ -546,11 +556,12 @@ function snapshotDocument({ PROPS, NOISE_TAGS, INHERITED }) {
|
||||
return [...map.values()];
|
||||
}
|
||||
|
||||
function matchedDeclarations(el, rules) {
|
||||
// `comp` is the element's computed style, used to resolve var()s inside a
|
||||
// rule's shorthand values (custom props inherit, so per-element lookup is
|
||||
// correct); the caller computes it once so the tree builder and matcher
|
||||
// share a single getComputedStyle call.
|
||||
function matchedDeclarations(el, rules, comp) {
|
||||
const out = [];
|
||||
// Computed style for resolving var()s inside a rule's shorthand values
|
||||
// (custom props inherit, so per-element lookup is correct).
|
||||
const comp = getComputedStyle(el);
|
||||
for (const { rule, order, effective } of rules) {
|
||||
const sel = effective ?? rule.selectorText;
|
||||
if (!sel) continue;
|
||||
@@ -575,8 +586,10 @@ function snapshotDocument({ PROPS, NOISE_TAGS, INHERITED }) {
|
||||
const inline = el.getAttribute("style");
|
||||
if (inline) {
|
||||
const st = el.style;
|
||||
const names = new Set();
|
||||
for (let k = 0; k < st.length; k++) {
|
||||
const name = st.item(k);
|
||||
names.add(name);
|
||||
out.push({
|
||||
name,
|
||||
value: st.getPropertyValue(name).trim(),
|
||||
@@ -588,6 +601,25 @@ function snapshotDocument({ PROPS, NOISE_TAGS, INHERITED }) {
|
||||
order: 1e6,
|
||||
});
|
||||
}
|
||||
// Enumeration only yields longhands (inline "margin: 0" shows up as
|
||||
// margin-top..left, never as "margin"). Parse the RAW attribute text
|
||||
// (el.style.cssText is canonicalized by the browser, e.g. a 4-value
|
||||
// margin collapses to the shortest form) so box shorthands get a
|
||||
// winning entry carrying the ORIGINAL declared spelling.
|
||||
for (const part of parseDecls(inline)) {
|
||||
const md = /^([^:]+):\s*([\s\S]*?)\s*(!important)?\s*$/.exec(part);
|
||||
if (!md || names.has(md[1].trim())) continue;
|
||||
out.push({
|
||||
name: md[1].trim(),
|
||||
value: md[2].trim(),
|
||||
important: !!md[3],
|
||||
layerRank: Number.MAX_SAFE_INTEGER,
|
||||
a: 1e6,
|
||||
b: 0,
|
||||
c: 0,
|
||||
order: 1e6,
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -661,12 +693,29 @@ function snapshotDocument({ PROPS, NOISE_TAGS, INHERITED }) {
|
||||
el.id === "svelte-announcer"
|
||||
)
|
||||
return null;
|
||||
// Computed style drives var() resolution and the recorded computed values;
|
||||
// computed once here and reused by the cascade matcher below.
|
||||
const cs = getComputedStyle(el);
|
||||
const specMap = new Map(inheritedEntries(inherited));
|
||||
for (const [name, d] of resolve(matchedDeclarations(el, rules))) {
|
||||
for (const [name, d] of resolve(matchedDeclarations(el, rules, cs))) {
|
||||
const cur = specMap.get(name);
|
||||
if (!cur || betterThan(d, cur) > 0) specMap.set(name, d);
|
||||
}
|
||||
const cs = getComputedStyle(el);
|
||||
// Box shorthands (margin/padding/gap): record the ORIGINAL declared value
|
||||
// when one rule cleanly set every side (`consistent` cascade), so the report
|
||||
// can show "20px 20px 20px 20px" vs a single "20px" the way the CSS spells
|
||||
// it, not just the collapsed rendering.
|
||||
const groups = {};
|
||||
for (const g of BOX_GROUPS) {
|
||||
const declared = specMap.get(g.name)?.value;
|
||||
if (!declared) continue;
|
||||
const expanded = expandShorthand(g.name, declared, cs);
|
||||
const consistent = g.keys.every(
|
||||
(k) => (expanded[k] ?? "") === (specMap.get(k)?.value ?? ""),
|
||||
);
|
||||
groups[g.name] = { declared, consistent };
|
||||
}
|
||||
|
||||
const styles = {};
|
||||
const raw = {};
|
||||
const computed = {};
|
||||
@@ -699,6 +748,7 @@ function snapshotDocument({ PROPS, NOISE_TAGS, INHERITED }) {
|
||||
styles,
|
||||
raw,
|
||||
computed,
|
||||
groups,
|
||||
children:
|
||||
tag === "svg"
|
||||
? []
|
||||
@@ -943,6 +993,14 @@ const DEFAULTISH = (v) =>
|
||||
v === "unset" ||
|
||||
v === "revert";
|
||||
|
||||
// Border longhands are reported as one grouped "border" line per side-group
|
||||
// rather than 12 separate lines, so a full-border change is visible at a glance.
|
||||
const BORDER_SIDES = ["top", "right", "bottom", "left"];
|
||||
const BORDER_SUBS = ["width", "style", "color"];
|
||||
const BORDER_KEYS = new Set(
|
||||
BORDER_SIDES.flatMap((s) => BORDER_SUBS.map((t) => `border-${s}-${t}`)),
|
||||
);
|
||||
|
||||
// Geometry drift along an axis only matters when that dimension is actually
|
||||
// locked by CSS; otherwise it is just content flow (text lengths, images).
|
||||
function flowDriven(node, k) {
|
||||
@@ -973,6 +1031,117 @@ function normVal(k, v) {
|
||||
return v;
|
||||
}
|
||||
|
||||
// Is property k genuinely different between two interpreted nodes? Skips
|
||||
// spelling-vs-default noise (one side spells out a UA/inherit default the
|
||||
// other relies on) when they render identically (computed matches), unless the
|
||||
// prop is layout-sensitive where the specified value is what matters.
|
||||
function isRealDiff(a, b, k) {
|
||||
const va = normVal(k, a.styles[k] ?? ""),
|
||||
vb = normVal(k, b.styles[k] ?? "");
|
||||
if (va === vb) return false;
|
||||
return !(
|
||||
!LAYOUT.has(k) &&
|
||||
(DEFAULTISH(va) || DEFAULTISH(vb)) &&
|
||||
a.computed?.[k] === b.computed?.[k]
|
||||
);
|
||||
}
|
||||
|
||||
// One "prop: ref → ours" line for a non-border property, or null if identical.
|
||||
function styleLine(a, b, k) {
|
||||
if (!isRealDiff(a, b, k)) return null;
|
||||
return `${k}: ${fmtSide(a, k) || "—"} → ${fmtSide(b, k) || "—"}`;
|
||||
}
|
||||
|
||||
// Compact, one-line rendering of one node's border. When every side shares the
|
||||
// same width/style/color it reads "1px solid red"; otherwise the sides that
|
||||
// actually differ are listed explicitly (e.g. "top(color:#00f)").
|
||||
function borderOf(a, b, n) {
|
||||
const sideVals = (sub) => {
|
||||
const vals = BORDER_SIDES.map((s) => fmtSide(n, `border-${s}-${sub}`));
|
||||
return new Set(vals.map((v) => v || "—")).size === 1 ? vals[0] : null;
|
||||
};
|
||||
const w = sideVals("width"),
|
||||
st = sideVals("style"),
|
||||
c = sideVals("color");
|
||||
if (w !== null && st !== null && c !== null)
|
||||
return `${w} ${st} ${c}`.replace(/\s+/g, " ").trim();
|
||||
const parts = BORDER_SIDES.map((s) => {
|
||||
const subs = BORDER_SUBS.filter((t) =>
|
||||
isRealDiff(a, b, `border-${s}-${t}`),
|
||||
);
|
||||
if (!subs.length) return null;
|
||||
return `${s}(${subs
|
||||
.map((t) => `${t}:${fmtSide(n, `border-${s}-${t}`) || "—"}`)
|
||||
.join(" ")})`;
|
||||
}).filter(Boolean);
|
||||
return parts.join(" ") || "—";
|
||||
}
|
||||
|
||||
// The grouped "border" line covering all four sides, or null if there is no
|
||||
// real border difference. Returns the differing longhand keys too so the caller
|
||||
// can mark them as already-reported (suppressing geometry duplicates).
|
||||
function borderLine(a, b) {
|
||||
const keys = [...BORDER_KEYS].filter((k) => isRealDiff(a, b, k));
|
||||
if (!keys.length) return null;
|
||||
return { line: `border: ${borderOf(a, b, a)} → ${borderOf(a, b, b)}`, keys };
|
||||
}
|
||||
|
||||
// CSS box shorthands reported as one line instead of one line per longhand
|
||||
// (4 for margin/padding, 2 for gap). The same table is passed into the browser
|
||||
// so the snapshot can also record the ORIGINAL declared shorthand (see snap()).
|
||||
const BOX_GROUPS = [
|
||||
{
|
||||
name: "margin",
|
||||
keys: ["margin-top", "margin-right", "margin-bottom", "margin-left"],
|
||||
},
|
||||
{
|
||||
name: "padding",
|
||||
keys: ["padding-top", "padding-right", "padding-bottom", "padding-left"],
|
||||
},
|
||||
{ name: "gap", keys: ["row-gap", "column-gap"] },
|
||||
];
|
||||
const GROUP_KEYS = new Set(BOX_GROUPS.flatMap((g) => g.keys));
|
||||
|
||||
// Collapse per-side specified values into the shortest CSS shorthand that
|
||||
// expresses them (1/2/3/4 values for a box, 1/2 for gap) — exactly how the
|
||||
// browser serializes e.g. padding. Distinct sides can't collapse, so a
|
||||
// "padding: 15px 20px 25px 30px" vs "padding: 20px" difference stays visible.
|
||||
function boxShorthand(vals) {
|
||||
if (vals.length === 2) {
|
||||
const [row, col] = vals;
|
||||
return row === col ? row : `${row} ${col}`;
|
||||
}
|
||||
const [t, r, b, l] = vals;
|
||||
if (t === r && t === b && t === l) return t;
|
||||
if (t === b && r === l) return `${t} ${r}`;
|
||||
if (r === l) return `${t} ${r} ${b}`;
|
||||
return `${t} ${r} ${b} ${l}`;
|
||||
}
|
||||
|
||||
// The browser records how a box shorthand was actually DECLARED in CSS (e.g.
|
||||
// "20px 20px 20px 20px") when one rule cleanly set every side of the group.
|
||||
// Surface it when it differs from the collapsed rendering, so source-level
|
||||
// syntax differences (explicit longhand lists vs a single value) are visible.
|
||||
function declaredHint(node, group, collapsed) {
|
||||
const h = node?.groups?.[group.name];
|
||||
if (!h?.consistent || !h.declared) return "";
|
||||
if (h.declared === collapsed) return "";
|
||||
return ` [declared: ${h.declared}]`;
|
||||
}
|
||||
|
||||
// One grouped "margin/padding/gap: ref → ours" line, or null when none of the
|
||||
// group's longhands actually differ. Returns the differing keys too.
|
||||
function boxGroupLine(a, b, group) {
|
||||
const keys = group.keys;
|
||||
const diff = keys.filter((k) => isRealDiff(a, b, k));
|
||||
if (!diff.length) return null;
|
||||
const packed = (n) => boxShorthand(keys.map((k) => fmtSide(n, k) || "—"));
|
||||
const line =
|
||||
`${group.name}: ${packed(a)}${declaredHint(a, group, packed(a))}` +
|
||||
` → ${packed(b)}${declaredHint(b, group, packed(b))}`;
|
||||
return { line, keys: diff };
|
||||
}
|
||||
|
||||
function diffStyles(a, b) {
|
||||
const out = [];
|
||||
if (!a?.styles || !b?.styles)
|
||||
@@ -993,23 +1162,27 @@ function diffStyles(a, b) {
|
||||
...Object.keys(a.styles),
|
||||
...Object.keys(b.styles),
|
||||
])) {
|
||||
const va = normVal(k, a.styles[k] ?? ""),
|
||||
vb = normVal(k, b.styles[k] ?? "");
|
||||
if (va !== vb) {
|
||||
// One side relied on a default (UA/inherit) while the other spelled it
|
||||
// out, but they render identically — not a real difference. Skip for
|
||||
// content-independent props by comparing the computed values.
|
||||
if (
|
||||
!LAYOUT.has(k) &&
|
||||
(DEFAULTISH(va) || DEFAULTISH(vb)) &&
|
||||
a.computed?.[k] === b.computed?.[k]
|
||||
)
|
||||
continue;
|
||||
out.push(`${k}: ${fmtSide(a, k) || "—"} → ${fmtSide(b, k) || "—"}`);
|
||||
if (BORDER_KEYS.has(k) || GROUP_KEYS.has(k)) continue; // grouped below
|
||||
const line = styleLine(a, b, k);
|
||||
if (line) {
|
||||
out.push(line);
|
||||
styleDiffKeys.add(k);
|
||||
}
|
||||
}
|
||||
|
||||
// Box shorthands (margin/padding/gap) and the border collapse their
|
||||
// individual longhand lines into one "shorthand: ref → ours" line each.
|
||||
const lineOf = (g) => {
|
||||
if (g.name === "border") return borderLine(a, b);
|
||||
return boxGroupLine(a, b, g);
|
||||
};
|
||||
for (const g of [...BOX_GROUPS, { name: "border" }]) {
|
||||
const gl = lineOf(g);
|
||||
if (!gl) continue;
|
||||
out.push(gl.line);
|
||||
for (const k of gl.keys) styleDiffKeys.add(k);
|
||||
}
|
||||
|
||||
// Box axes map to style props: w→width, h→height, x→left/right, y→top/bottom.
|
||||
// If the corresponding style diff is already above, the geometry line is a
|
||||
// duplicate — drop it. Keep geometry only for changes the styles don't
|
||||
@@ -1034,14 +1207,20 @@ function diffStyles(a, b) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function compare(a, b, path, report) {
|
||||
const sel = path.join(" > ");
|
||||
// Emit one node's style changes (if any) as a report block under `sel`.
|
||||
function reportStyles(sel, a, b, report) {
|
||||
const changes = diffStyles(a, b);
|
||||
if (changes.length)
|
||||
report.push(
|
||||
sel + annotate(b ?? a) + "\n" + changes.map((c) => " " + c).join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
// Walk a matched pair's children, diffing them and emitting the results into
|
||||
// `report`. `removed`/`added` are leftover children after the LCS pairing —
|
||||
// they're first repairable by tag, and anything still unpaired is a real
|
||||
// add/remove entry.
|
||||
function diffChildren(a, b, path, report) {
|
||||
const removed = [],
|
||||
added = [];
|
||||
let ai = 0,
|
||||
@@ -1085,6 +1264,11 @@ function compare(a, b, path, report) {
|
||||
);
|
||||
}
|
||||
|
||||
function compare(a, b, path, report) {
|
||||
reportStyles(path.join(" > "), a, b, report);
|
||||
diffChildren(a, b, path, report);
|
||||
}
|
||||
|
||||
// ==== target discovery (mirrors audit-css.mjs) =============================
|
||||
|
||||
// Dynamic segments: explicit id values + id→ref-file mapping.
|
||||
|
||||
Reference in New Issue
Block a user