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:
@@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user