Files
fusion/packages/dashboard/app/__tests__/dashboard-css-token-validity.css.test.ts
gsxdsm 87e87a8f1a FN-6693: enforce dashboard CSS token references
Extend the dashboard CSS token guard and replace stale custom-property references with defined design tokens.

- Add a dashboard-wide CSS token validity test that scans component/app CSS for undefined custom-property references.
- Include dashboard CSS tests in the dashboard Vitest config and document the guard in testing guidance.
- Remap undefined CSS variables across dashboard components and command-center styles to existing semantic or local tokens.

Files changed:
 docs/testing.md                                    |   3 +
 .../dashboard-css-token-validity.css.test.ts       | 165 +++++++++++++++++++++
 .../dashboard/app/components/ActiveAgentsPanel.css |  10 +-
 .../dashboard/app/components/AgentLogViewer.css    |   2 +-
 .../app/components/AgentReflectionsTab.css         |  10 +-
 packages/dashboard/app/components/AgentsView.css   |   6 +-
 .../dashboard/app/components/BranchGroupCard.css   |   6 +-
 packages/dashboard/app/components/ChatView.css     |  12 +-
 .../app/components/CliBinaryInstallBanner.css      |   2 +-
 .../dashboard/app/components/CliBinaryPanel.css    |   2 +-
 .../app/components/ConversationHistory.css         |  12 +-
 .../dashboard/app/components/DashboardLoader.css   |   2 +-
 .../dashboard/app/components/DesktopLaunchGate.css |   4 +-
 .../dashboard/app/components/DirectoryPicker.css   |   2 +-
 .../dashboard/app/components/FileMentionPopup.css  |   4 +-
 .../dashboard/app/components/GitHubImportModal.css |  10 +-
 packages/dashboard/app/components/GoalsView.css    |   2 +-
 .../dashboard/app/components/GroupTaskModal.css    |  18 +--
 packages/dashboard/app/components/InsightsView.css |  10 +-
 packages/dashboard/app/components/Lane.css         |   8 +-
 .../dashboard/app/components/LanguageSelector.css  |   8 +-
 packages/dashboard/app/components/MailboxModal.css |  62 ++++----
 .../app/components/MergeAdvanceNotice.css          |   2 +-
 .../dashboard/app/components/MissionManager.css    |  10 +-
 .../app/components/MobileWorkflowGraphView.css     |   2 +-
 .../app/components/ModelOnboardingModal.css        |  12 +-
 .../app/components/OnboardingDisclosure.css        |   4 +-
 .../dashboard/app/components/PlanningModeModal.css |   2 +-
 .../dashboard/app/components/PluginManager.css     |  58 ++++----
 .../components/PostOnboardingRecommendations.css   |   4 +-
 .../dashboard/app/components/PullRequestView.css   |  32 ++--
 packages/dashboard/app/components/QuickChatFAB.css |   4 +-
 .../dashboard/app/components/QuickEntryBox.css     |   6 +-
 .../dashboard/app/components/ReliabilityView.css   |   6 +-
 packages/dashboard/app/components/ScriptsModal.css |  34 ++---
 .../dashboard/app/components/SessionTerminal.css   |  28 ++--
 .../dashboard/app/components/SettingsModal.css     |   6 +-
 .../dashboard/app/components/SkillMultiselect.css  |   2 +-
 packages/dashboard/app/components/TaskCard.css     |  24 +--
 .../dashboard/app/components/TaskDocumentsTab.css  |   8 +-
 .../dashboard/app/components/TaskFieldsSection.css |  20 +--
 .../dashboard/app/components/TerminalModal.css     |  22 +--
 .../app/components/WorkflowFieldsPanel.css         |   6 +-
 .../app/components/WorkflowNodeEditor.css          |  30 ++--
 .../dashboard/app/components/WorkspaceSelector.css |   2 +-
 .../components/command-center/CommandCenter.css    | 106 ++++++-------
 .../components/command-center/DateRangePicker.css  |  18 +--
 .../command-center/MissionControlPanel.css         |  14 +-
 .../app/components/command-center/SdlcFunnel.css   |   2 +-
 .../command-center/areas/SystemStatsArea.css       |  24 +--
 .../app/components/command-center/areas/areas.css  |  62 ++++----
 .../components/command-center/charts/charts.css    | 104 ++++++-------
 packages/dashboard/vitest.config.ts                |   1 +
 53 files changed, 592 insertions(+), 423 deletions(-)

Fusion-Task-Id: FN-6693

Fusion-Task-Lineage: e9ccf3e6-fee9-47e0-9474-7b5580adbf31
2026-06-19 04:27:01 -07:00

166 lines
5.7 KiB
TypeScript

import { readdirSync, readFileSync, statSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
const APP_ROOT = path.resolve(__dirname, "..");
const COMPONENTS_ROOT = path.join(APP_ROOT, "components");
const JS_SET_PROPERTY_ALLOWLIST = new Set([
"--cc-radial-value",
"--mobile-wf-depth",
"--icb-bottom-offset",
"--icb-right-offset",
"--quick-chat-fab-lift",
"--quick-chat-fab-shadow",
"--quick-chat-fab-shadow-hover",
"--selection-comment-panel-width",
"--task-chat-composer-max-height",
"--layout-content-max-width",
"--opacity-disabled",
"--provider-icon-color",
]);
/**
* FNXC:DashboardStyling 2026-06-19-00:00:
* FN-6690/FN-6693 proved jsdom style assertions miss undefined CSS custom-property references because jsdom does not resolve `var()` at computed-value time.
* Scan raw dashboard CSS instead: a bare `var(--missing-token)` can silently invalidate a declaration or depend on a stale fallback, so every referenced custom property must be defined by CSS, assigned by React inline style, or documented as a runtime-local allowlist entry.
*/
function stripCssComments(source: string): string {
return source.replace(/\/\*[\s\S]*?\*\//g, "");
}
function collectFiles(dir: string, predicate: (fileName: string) => boolean): string[] {
const out: string[] = [];
for (const entry of readdirSync(dir)) {
if (entry === "node_modules" || entry === "dist" || entry === "public" || entry.startsWith(".")) continue;
const fullPath = path.join(dir, entry);
const info = statSync(fullPath);
if (info.isDirectory()) {
out.push(...collectFiles(fullPath, predicate));
continue;
}
if (info.isFile() && predicate(entry)) out.push(fullPath);
}
return out.sort((left, right) => formatAppPath(left).localeCompare(formatAppPath(right)));
}
function collectCssFilesToScan(): string[] {
const componentCss = collectFiles(COMPONENTS_ROOT, (fileName) => fileName.endsWith(".css"));
const appLevelCss = readdirSync(APP_ROOT)
.filter((entry) => entry.endsWith(".css") && entry !== "styles.css")
.map((entry) => path.join(APP_ROOT, entry));
return [...componentCss, ...appLevelCss].sort((left, right) => formatAppPath(left).localeCompare(formatAppPath(right)));
}
function collectAllCssFiles(): string[] {
return collectFiles(APP_ROOT, (fileName) => fileName.endsWith(".css"));
}
function collectSourceFiles(): string[] {
return collectFiles(APP_ROOT, (fileName) => /\.(tsx?|jsx?)$/.test(fileName));
}
function collectDefinedProperties(cssFiles: string[]): Set<string> {
const properties = new Set<string>();
for (const filePath of cssFiles) {
const source = stripCssComments(readFileSync(filePath, "utf8"));
for (const match of source.matchAll(/(^|[\s{;])(--[A-Za-z0-9_-]+)\s*:/g)) {
properties.add(match[2]);
}
}
return properties;
}
function collectInlineSetProperties(sourceFiles: string[]): Set<string> {
const properties = new Set<string>();
for (const filePath of sourceFiles) {
const source = readFileSync(filePath, "utf8");
for (const match of source.matchAll(/\[\s*["'`](--[A-Za-z0-9_-]+)["'`]\s*(?:as\s+string)?\s*\]\s*:/g)) {
properties.add(match[1]);
}
for (const match of source.matchAll(/["'`](--[A-Za-z0-9_-]+)["'`]\s*:/g)) {
properties.add(match[1]);
}
}
return properties;
}
function collectReferencedProperties(source: string): Set<string> {
const references = new Set<string>();
const uncommented = stripCssComments(source);
for (const match of uncommented.matchAll(/var\(\s*(--[A-Za-z0-9_-]+)/g)) {
references.add(match[1]);
}
return references;
}
function formatAppPath(filePath: string): string {
return path.relative(APP_ROOT, filePath).split(path.sep).join("/");
}
function findUndefinedReferences(args: {
cssFilesToScan: string[];
definedProperties: Set<string>;
inlineSetProperties: Set<string>;
allowlist?: Set<string>;
sourceByFile?: Map<string, string>;
}): string[] {
const {
cssFilesToScan,
definedProperties,
inlineSetProperties,
allowlist = JS_SET_PROPERTY_ALLOWLIST,
sourceByFile = new Map(),
} = args;
const violations: string[] = [];
for (const filePath of cssFilesToScan) {
const source = sourceByFile.get(filePath) ?? readFileSync(filePath, "utf8");
for (const property of collectReferencedProperties(source)) {
if (definedProperties.has(property) || inlineSetProperties.has(property) || allowlist.has(property)) continue;
violations.push(`${formatAppPath(filePath)} references ${property}`);
}
}
return violations.sort();
}
describe("dashboard CSS token validity", () => {
it("flags a synthetic undefined custom-property reference", () => {
const fixturePath = path.join(APP_ROOT, "fixture.css");
const fixtureSource = "/* var(--commented-out) */ .x { color: var(--does-not-exist); }";
const violations = findUndefinedReferences({
cssFilesToScan: [fixturePath],
definedProperties: new Set(["--defined-token"]),
inlineSetProperties: new Set(),
allowlist: new Set(),
sourceByFile: new Map([[fixturePath, fixtureSource]]),
});
expect(collectReferencedProperties(fixtureSource)).toEqual(new Set(["--does-not-exist"]));
expect(violations).toEqual(["fixture.css references --does-not-exist"]);
});
it("keeps component and app-level CSS references backed by defined or runtime-set properties", () => {
const violations = findUndefinedReferences({
cssFilesToScan: collectCssFilesToScan(),
definedProperties: collectDefinedProperties(collectAllCssFiles()),
inlineSetProperties: collectInlineSetProperties(collectSourceFiles()),
});
expect(violations, [`Undefined CSS custom-property references found:`, ...violations].join("\n")).toEqual([]);
});
});