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-width",
|
||||||
"border-top-style",
|
"border-top-style",
|
||||||
"border-top-color",
|
"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",
|
"border-radius",
|
||||||
"background-color",
|
"background-color",
|
||||||
"background-image",
|
"background-image",
|
||||||
@@ -207,6 +216,7 @@ async function capture(browser, target, viewport) {
|
|||||||
PROPS,
|
PROPS,
|
||||||
NOISE_TAGS: [...NOISE_TAGS],
|
NOISE_TAGS: [...NOISE_TAGS],
|
||||||
INHERITED: [...INHERITED],
|
INHERITED: [...INHERITED],
|
||||||
|
BOX_GROUPS,
|
||||||
});
|
});
|
||||||
await page.close();
|
await page.close();
|
||||||
return tree;
|
return tree;
|
||||||
@@ -217,7 +227,7 @@ async function capture(browser, target, viewport) {
|
|||||||
// declares after cascade + inheritance + var() resolution — alongside the
|
// declares after cascade + inheritance + var() resolution — alongside the
|
||||||
// computed value from getComputedStyle. Runs inside the page via
|
// computed value from getComputedStyle. Runs inside the page via
|
||||||
// page.evaluate, so it must stay self-contained (browser globals + the arg).
|
// 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.
|
// page.evaluate hand-serialized the Sets to plain arrays — restore them.
|
||||||
NOISE_TAGS = new Set(NOISE_TAGS);
|
NOISE_TAGS = new Set(NOISE_TAGS);
|
||||||
INHERITED = new Set(INHERITED);
|
INHERITED = new Set(INHERITED);
|
||||||
@@ -546,11 +556,12 @@ function snapshotDocument({ PROPS, NOISE_TAGS, INHERITED }) {
|
|||||||
return [...map.values()];
|
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 = [];
|
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) {
|
for (const { rule, order, effective } of rules) {
|
||||||
const sel = effective ?? rule.selectorText;
|
const sel = effective ?? rule.selectorText;
|
||||||
if (!sel) continue;
|
if (!sel) continue;
|
||||||
@@ -575,8 +586,10 @@ function snapshotDocument({ PROPS, NOISE_TAGS, INHERITED }) {
|
|||||||
const inline = el.getAttribute("style");
|
const inline = el.getAttribute("style");
|
||||||
if (inline) {
|
if (inline) {
|
||||||
const st = el.style;
|
const st = el.style;
|
||||||
|
const names = new Set();
|
||||||
for (let k = 0; k < st.length; k++) {
|
for (let k = 0; k < st.length; k++) {
|
||||||
const name = st.item(k);
|
const name = st.item(k);
|
||||||
|
names.add(name);
|
||||||
out.push({
|
out.push({
|
||||||
name,
|
name,
|
||||||
value: st.getPropertyValue(name).trim(),
|
value: st.getPropertyValue(name).trim(),
|
||||||
@@ -588,6 +601,25 @@ function snapshotDocument({ PROPS, NOISE_TAGS, INHERITED }) {
|
|||||||
order: 1e6,
|
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;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -661,12 +693,29 @@ function snapshotDocument({ PROPS, NOISE_TAGS, INHERITED }) {
|
|||||||
el.id === "svelte-announcer"
|
el.id === "svelte-announcer"
|
||||||
)
|
)
|
||||||
return null;
|
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));
|
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);
|
const cur = specMap.get(name);
|
||||||
if (!cur || betterThan(d, cur) > 0) specMap.set(name, d);
|
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 styles = {};
|
||||||
const raw = {};
|
const raw = {};
|
||||||
const computed = {};
|
const computed = {};
|
||||||
@@ -699,6 +748,7 @@ function snapshotDocument({ PROPS, NOISE_TAGS, INHERITED }) {
|
|||||||
styles,
|
styles,
|
||||||
raw,
|
raw,
|
||||||
computed,
|
computed,
|
||||||
|
groups,
|
||||||
children:
|
children:
|
||||||
tag === "svg"
|
tag === "svg"
|
||||||
? []
|
? []
|
||||||
@@ -943,6 +993,14 @@ const DEFAULTISH = (v) =>
|
|||||||
v === "unset" ||
|
v === "unset" ||
|
||||||
v === "revert";
|
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
|
// Geometry drift along an axis only matters when that dimension is actually
|
||||||
// locked by CSS; otherwise it is just content flow (text lengths, images).
|
// locked by CSS; otherwise it is just content flow (text lengths, images).
|
||||||
function flowDriven(node, k) {
|
function flowDriven(node, k) {
|
||||||
@@ -973,6 +1031,117 @@ function normVal(k, v) {
|
|||||||
return 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) {
|
function diffStyles(a, b) {
|
||||||
const out = [];
|
const out = [];
|
||||||
if (!a?.styles || !b?.styles)
|
if (!a?.styles || !b?.styles)
|
||||||
@@ -993,23 +1162,27 @@ function diffStyles(a, b) {
|
|||||||
...Object.keys(a.styles),
|
...Object.keys(a.styles),
|
||||||
...Object.keys(b.styles),
|
...Object.keys(b.styles),
|
||||||
])) {
|
])) {
|
||||||
const va = normVal(k, a.styles[k] ?? ""),
|
if (BORDER_KEYS.has(k) || GROUP_KEYS.has(k)) continue; // grouped below
|
||||||
vb = normVal(k, b.styles[k] ?? "");
|
const line = styleLine(a, b, k);
|
||||||
if (va !== vb) {
|
if (line) {
|
||||||
// One side relied on a default (UA/inherit) while the other spelled it
|
out.push(line);
|
||||||
// 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) || "—"}`);
|
|
||||||
styleDiffKeys.add(k);
|
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.
|
// 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
|
// 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
|
// duplicate — drop it. Keep geometry only for changes the styles don't
|
||||||
@@ -1034,14 +1207,20 @@ function diffStyles(a, b) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function compare(a, b, path, report) {
|
// Emit one node's style changes (if any) as a report block under `sel`.
|
||||||
const sel = path.join(" > ");
|
function reportStyles(sel, a, b, report) {
|
||||||
const changes = diffStyles(a, b);
|
const changes = diffStyles(a, b);
|
||||||
if (changes.length)
|
if (changes.length)
|
||||||
report.push(
|
report.push(
|
||||||
sel + annotate(b ?? a) + "\n" + changes.map((c) => " " + c).join("\n"),
|
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 = [],
|
const removed = [],
|
||||||
added = [];
|
added = [];
|
||||||
let ai = 0,
|
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) =============================
|
// ==== target discovery (mirrors audit-css.mjs) =============================
|
||||||
|
|
||||||
// Dynamic segments: explicit id values + id→ref-file mapping.
|
// Dynamic segments: explicit id values + id→ref-file mapping.
|
||||||
|
|||||||
Reference in New Issue
Block a user