2d370f2e9d
The sidebar contract read a selector with indexOf, so it returned whichever rule appeared earliest in the file. A responsive override added above the base rule made the gate report a missing min-height that was never removed, which blocked staging on a change that was correct. Share one helper that strips comments, parses rule by rule, accepts a whole group as the query, and returns the declarations of every matching rule. The union also makes a negative assertion mean no rule may declare the property, which is what these contracts intend. The helper was duplicated in two files and had no tests of its own; it now has regressions for each way it read the wrong block. Co-authored-by: Cursor <cursoragent@cursor.com>
34 lines
1.6 KiB
TypeScript
34 lines
1.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
|
|
const globalStylesUrl = new URL("../src/app/globals.css", import.meta.url);
|
|
|
|
/**
|
|
* Declaration text for every rule that targets `selector`.
|
|
*
|
|
* A selector is normally declared once at the base layer and again inside
|
|
* media queries, so reading only the first occurrence reports whichever rule
|
|
* happens to appear earliest in the file. That makes a positive assertion fail
|
|
* when a responsive override is added above the base rule, and lets a negative
|
|
* assertion pass while an override still declares the forbidden property.
|
|
* Joining every matching rule keeps both directions honest.
|
|
*/
|
|
export function cssDeclarations(selector: string, source = readFileSync(globalStylesUrl, "utf8")) {
|
|
// A comment sits between the previous rule and the selector it documents, so
|
|
// it lands inside the captured selector text unless it is removed first.
|
|
const withoutComments = source.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
const normalize = (value: string) => value.trim().replace(/\s+/g, " ");
|
|
// A caller may ask for a whole group, so any component counts as a hit.
|
|
const wanted = selector.split(",").map(normalize).filter(Boolean);
|
|
const bodies: string[] = [];
|
|
for (const [, selectorList, body] of withoutComments.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
const targets = selectorList.split(",").map(normalize);
|
|
const hit = targets.some((target) => wanted.some(
|
|
(value) => target === value || target.endsWith(` ${value}`),
|
|
));
|
|
if (hit) bodies.push(body.trim());
|
|
}
|
|
assert.notEqual(bodies.length, 0, `missing CSS selector: ${selector}`);
|
|
return bodies.join(" ");
|
|
}
|