Files
Jyotisha/frontend/tests/css-contract-test-support.ts
T
Jesse_Chen 2d370f2e9d
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
fix(tests): read every CSS rule for a selector, not the first one
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>
2026-08-17 13:03:57 +08:00

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(" ");
}