FN-6256: enforce recursive raw rgba CSS hygiene

Extend the dashboard component CSS hygiene guard so raw rgb/rgba checks cover nested component styles.

- Recursively discover dashboard component CSS files instead of only scanning the top-level components directory.
- Report stable component-relative paths for violations across nested folders.
- Add focused coverage for raw rgba detection while preserving var() fallback allowance.

Files changed:
 .../__tests__/component-css-no-raw-rgba.test.ts    | 85 +++++++++++++++++-----
 1 file changed, 65 insertions(+), 20 deletions(-)

Fusion-Task-Id: FN-6256

Fusion-Task-Lineage: c8e84ab4-1da9-4cf0-9e04-d8ca4038eafa
This commit is contained in:
gsxdsm
2026-06-11 22:35:54 -07:00
parent 73990e4212
commit b4dcd291eb

View File

@@ -1,5 +1,5 @@
import { readdirSync, readFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { join, relative, resolve, sep } from "node:path";
import { describe, expect, it } from "vitest";
const componentsDir = resolve(__dirname, "..", "components");
@@ -8,27 +8,72 @@ function stripVarFallbackRgba(content: string): string {
return content.replace(/var\([^()]*,\s*rgba?\([^)]*\)\s*\)/g, "");
}
describe("component CSS color token hygiene", () => {
it("contains no raw rgb/rgba calls outside var() fallbacks", () => {
const cssFiles = readdirSync(componentsDir)
.filter((name) => name.endsWith(".css"))
.sort();
function findComponentCssFiles(dir = componentsDir): string[] {
const entries = readdirSync(dir, { withFileTypes: true });
const files = entries.flatMap((entry) => {
const entryPath = join(dir, entry.name);
const violations: string[] = [];
for (const fileName of cssFiles) {
const filePath = join(componentsDir, fileName);
const source = readFileSync(filePath, "utf8");
const withoutFallbacks = stripVarFallbackRgba(source);
const lines = withoutFallbacks.split(/\r?\n/);
for (let index = 0; index < lines.length; index += 1) {
if (/rgba?\(/.test(lines[index])) {
violations.push(`${fileName}:${index + 1}:${lines[index].trim()}`);
}
}
if (entry.isDirectory()) {
return findComponentCssFiles(entryPath);
}
expect(violations).toEqual([]);
return entry.isFile() && entry.name.endsWith(".css") ? [entryPath] : [];
});
return files.sort((left, right) =>
relative(componentsDir, left).localeCompare(relative(componentsDir, right))
);
}
function formatComponentCssPath(filePath: string): string {
return relative(componentsDir, filePath).split(sep).join("/");
}
function findRawRgbViolations(source: string, fileName: string): string[] {
const withoutFallbacks = stripVarFallbackRgba(source);
const lines = withoutFallbacks.split(/\r?\n/);
return lines.flatMap((line, index) =>
/rgba?\(/.test(line) ? [`${fileName}:${index + 1}:${line.trim()}`] : []
);
}
function buildRawRgbFailureMessage(violations: string[]): string {
return [
"Raw rgb/rgba() found in component CSS.",
"Use design tokens or color-mix(in srgb, var(--color-X) N%, transparent) instead:",
...violations,
].join("\n");
}
describe("component CSS color token hygiene", () => {
it("detects raw rgb/rgba calls but permits var() fallback rgb/rgba", () => {
const source = [
".clean { color: var(--color-text); }",
".fallback { color: var(--custom-color, rgba(1, 2, 3, 0.5)); }",
".violation { box-shadow: 0 0 0 1px rgba(1, 2, 3, 0.5); }",
].join("\n");
const violations = findRawRgbViolations(source, "fixture.css");
expect(violations).toEqual([
"fixture.css:3:.violation { box-shadow: 0 0 0 1px rgba(1, 2, 3, 0.5); }",
]);
expect(buildRawRgbFailureMessage(violations)).toContain(
"fixture.css:3:.violation { box-shadow: 0 0 0 1px rgba(1, 2, 3, 0.5); }"
);
expect(buildRawRgbFailureMessage(violations)).toContain(
"color-mix(in srgb, var(--color-X) N%, transparent)"
);
});
it("contains no raw rgb/rgba calls outside var() fallbacks", () => {
const cssFiles = findComponentCssFiles();
const violations = cssFiles.flatMap((filePath) =>
findRawRgbViolations(readFileSync(filePath, "utf8"), formatComponentCssPath(filePath))
);
expect(violations, buildRawRgbFailureMessage(violations)).toEqual([]);
});
});