Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions js/packages/ui/src/components/lineage/__tests__/cllPaletteSync.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import {
cllAdditiveAccent,
cllAdditiveBadgeBg,
cllAdditiveBadgeFg,
cllChangedAccent,
cllChangedBadgeBg,
cllChangedBadgeFg,
cllChangeStatusBackgroundsDark,
cllChangeStatusBackgroundsLight,
cllChangeStatusColors,
cllImpactedAccent,
cllImpactedBadgeBg,
cllImpactedBadgeFg,
} from "../styles";

/**
* Single-source-of-truth guard for the CLL palette.
*
* The CLL colors are declared twice: as TS consts in `lineage/styles.tsx`
* (consumed by the React lineage canvas) and as `--schema-*` CSS custom
* properties in `schema/style.css` (consumed by the ag-grid schema view).
* Both files carry "keep in sync by hand — no build-time check" comments.
*
* This test IS that build-time check: it parses `schema/style.css` and asserts
* every TS const matches its CSS counterpart, so drift on either side fails CI.
* The TS consts are treated as canonical; the CSS values are verified against
* them. (A fuller consolidation — deriving the CSS vars from TS at runtime — is
* deferred; see DRC-3525.)
*/

const cssPath = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../../schema/style.css",
);
const css = readFileSync(cssPath, "utf8");

/** Extract `--name: value` declarations from the first block matching a header. */
function block(header: RegExp): Record<string, string> {
const m = header.exec(css);
if (!m) throw new Error(`CSS block not found for header: ${header}`);
const open = css.indexOf("{", m.index);
const close = css.indexOf("}", open);
const body = css.slice(open + 1, close);
const vars: Record<string, string> = {};
for (const decl of body.split(";")) {
const i = decl.indexOf(":");
if (i === -1) continue;
const name = decl.slice(0, i).trim();
if (!name.startsWith("--")) continue;
vars[name] = decl.slice(i + 1).trim();
}
return vars;
}

const rootVars = block(/:root,\s*\.light\s*\{/);
const darkVars = block(/\n\.dark\s*\{/);
const cllVars = block(/\n\.cll-experience\s*\{/);
const cllDarkVars = block(/\.dark \.cll-experience,/);

/**
* Resolve a CSS var the way the cascade does inside `.cll-experience`:
* the `.cll-experience` override wins, otherwise the base `:root`/`.dark` value.
*/
const lightCll = (name: string) => cllVars[name] ?? rootVars[name];
const darkCll = (name: string) => cllDarkVars[name] ?? darkVars[name];

describe("CLL palette TS ↔ CSS sync (styles.tsx ↔ schema/style.css)", () => {
it("parses the expected CSS blocks", () => {
expect(Object.keys(rootVars).length).toBeGreaterThan(0);
expect(Object.keys(darkVars).length).toBeGreaterThan(0);
expect(Object.keys(cllVars).length).toBeGreaterThan(0);
expect(Object.keys(cllDarkVars).length).toBeGreaterThan(0);
});

it("accent colors match the *-accent vars", () => {
expect(cllChangeStatusColors.modified).toBe(
lightCll("--schema-color-changed-accent"),
);
expect(cllChangeStatusColors.modified).toBe(
darkCll("--schema-color-changed-accent"),
);
expect(cllChangeStatusColors.impacted).toBe(
lightCll("--schema-color-impacted-accent"),
);
expect(cllChangeStatusColors.impacted).toBe(
darkCll("--schema-color-impacted-accent"),
);
expect(cllChangeStatusColors.added).toBe(
lightCll("--schema-color-added-accent"),
);
expect(cllChangeStatusColors.removed).toBe(
lightCll("--schema-color-removed-accent"),
);
});

it("row backgrounds match the --schema-color-{changed,impacted,added,removed} vars", () => {
// light
expect(cllChangeStatusBackgroundsLight.modified).toBe(
lightCll("--schema-color-changed"),
);
expect(cllChangeStatusBackgroundsLight.impacted).toBe(
lightCll("--schema-color-impacted"),
);
expect(cllChangeStatusBackgroundsLight.added).toBe(
lightCll("--schema-color-added"),
);
expect(cllChangeStatusBackgroundsLight.removed).toBe(
lightCll("--schema-color-removed"),
);
// dark
expect(cllChangeStatusBackgroundsDark.modified).toBe(
darkCll("--schema-color-changed"),
);
expect(cllChangeStatusBackgroundsDark.impacted).toBe(
darkCll("--schema-color-impacted"),
);
expect(cllChangeStatusBackgroundsDark.added).toBe(
darkCll("--schema-color-added"),
);
expect(cllChangeStatusBackgroundsDark.removed).toBe(
darkCll("--schema-color-removed"),
);
});

it("impacted badge/accent tokens match the --schema-*impacted* vars", () => {
expect(cllImpactedAccent.light).toBe(
lightCll("--schema-color-impacted-accent"),
);
expect(cllImpactedAccent.dark).toBe(
darkCll("--schema-color-impacted-accent"),
);
expect(cllImpactedBadgeBg.light).toBe(
lightCll("--schema-badge-impacted-bg"),
);
expect(cllImpactedBadgeBg.dark).toBe(darkCll("--schema-badge-impacted-bg"));
expect(cllImpactedBadgeFg.light).toBe(
lightCll("--schema-badge-impacted-fg"),
);
expect(cllImpactedBadgeFg.dark).toBe(darkCll("--schema-badge-impacted-fg"));
});

it("changed badge/accent tokens match the --schema-*changed* vars", () => {
expect(cllChangedAccent).toBe(lightCll("--schema-color-changed-accent"));
expect(cllChangedAccent).toBe(darkCll("--schema-color-changed-accent"));
expect(cllChangedBadgeBg.light).toBe(lightCll("--schema-badge-changed-bg"));
expect(cllChangedBadgeBg.dark).toBe(darkCll("--schema-badge-changed-bg"));
expect(cllChangedBadgeFg.light).toBe(lightCll("--schema-badge-changed-fg"));
expect(cllChangedBadgeFg.dark).toBe(darkCll("--schema-badge-changed-fg"));
});

it("additive badge/accent tokens match the --schema-*added* vars", () => {
expect(cllAdditiveAccent.light).toBe(
lightCll("--schema-color-added-accent"),
);
// Intentional: the additive accent's *dark* value mirrors the added badge
// foreground (a brighter green), not --schema-color-added-accent (which
// stays the mid green in both modes).
expect(cllAdditiveAccent.dark).toBe(darkCll("--schema-badge-added-fg"));
expect(cllAdditiveBadgeBg.light).toBe(lightCll("--schema-badge-added-bg"));
expect(cllAdditiveBadgeBg.dark).toBe(darkCll("--schema-badge-added-bg"));
expect(cllAdditiveBadgeFg.light).toBe(lightCll("--schema-badge-added-fg"));
expect(cllAdditiveBadgeFg.dark).toBe(darkCll("--schema-badge-added-fg"));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,33 @@ describe("computeWholeModelImpact", () => {
expect(result.wholeModelImpactedNodeIds).toEqual(new Set(["a", "b"]));
expect(result.wholeModelChangedNodeIds).toEqual(new Set(["a"]));
});

it("does not bleed impact across disconnected components", () => {
// Component 1: a (changed) -> b
// Component 2: x -> y (no change, no edge to component 1)
// The wave from `a` must stay inside its own component.
const graph = makeGraph([
["a", "b"],
["x", "y"],
]);
const cll = makeCll(["a"], ["a", "b", "x", "y"]);
const result = computeWholeModelImpact(graph as any, cll as any);
expect(result.wholeModelImpactedNodeIds).toEqual(new Set(["a", "b"]));
expect(result.wholeModelChangedNodeIds).toEqual(new Set(["a"]));
expect(result.wholeModelImpactedNodeIds.has("x")).toBe(false);
expect(result.wholeModelImpactedNodeIds.has("y")).toBe(false);
});

it("handles a changed node absent from the lineage graph", () => {
// `ghost` is flagged breaking in the CLL but has no node in the
// lineage graph (CLL/graph can drift). It should still seed both sets,
// and the missing-node guard must skip its BFS expansion without throwing.
const graph = makeGraph([["a", "b"]]);
const cll = makeCll(["ghost"], ["a", "b", "ghost"]);
const result = computeWholeModelImpact(graph as any, cll as any);
expect(result.wholeModelChangedNodeIds).toEqual(new Set(["ghost"]));
expect(result.wholeModelImpactedNodeIds).toEqual(new Set(["ghost"]));
// No graph node for `ghost`, so nothing downstream is reached.
expect(result.wholeModelImpactedNodeIds.has("b")).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { describe, expect, it } from "vitest";
import {
cllAdditiveAccent,
cllAdditiveBadgeBg,
cllAdditiveBadgeFg,
cllChangedAccent,
cllChangedBadgeBg,
cllChangedBadgeFg,
cllImpactedAccent,
cllImpactedBadgeBg,
cllImpactedBadgeFg,
} from "../styles";
import {
pickGraphBadge,
pickTitleChip,
type TreatmentInputs,
} from "../wholeModelTreatment";

/**
* Locks the visual tokens produced by `tokensForKind` (exercised through the
* public `pickTitleChip` / `pickGraphBadge` resolvers) against the shared CLL
* palette consts in `styles.tsx`, for every kind × (light / dark).
*
* These two surfaces are the only callers of `tokensForKind`, so asserting
* their `.tokens` output covers all five kinds and both modes. If the palette
* wiring drifts (wrong const, swapped light/dark), this fails loudly.
*/

const BASE: TreatmentInputs = {
newCllExperience: true,
isWholeModelChanged: false,
isWholeModelImpacted: false,
isImpacted: false,
};

describe("wholeModelTreatment tokens", () => {
describe("title chip — changed", () => {
for (const isDark of [false, true] as const) {
const mode = isDark ? "dark" : "light";
it(`maps the brown/changed palette in ${mode} mode`, () => {
const res = pickTitleChip(
{ ...BASE, isWholeModelChanged: true },
isDark,
);
expect(res?.kind).toBe("changed");
expect(res?.tokens).toEqual({
stripeAccent: "var(--schema-color-changed-accent)",
fg: cllChangedBadgeFg[mode],
badgeBg: cllChangedBadgeBg[mode],
badgeBorder: cllChangedAccent,
});
});
}
});

describe("title chip — impacted", () => {
for (const isDark of [false, true] as const) {
const mode = isDark ? "dark" : "light";
it(`maps the amber/impacted palette in ${mode} mode`, () => {
const res = pickTitleChip(
{ ...BASE, isWholeModelImpacted: true },
isDark,
);
expect(res?.kind).toBe("impacted");
expect(res?.tokens).toEqual({
stripeAccent: "var(--schema-color-impacted-accent)",
fg: cllImpactedBadgeFg[mode],
badgeBg: cllImpactedBadgeBg[mode],
badgeBorder: cllImpactedAccent[mode],
});
});
}
});

describe("graph badge — additive", () => {
for (const isDark of [false, true] as const) {
const mode = isDark ? "dark" : "light";
it(`maps the green/additive palette in ${mode} mode`, () => {
const res = pickGraphBadge(
{ ...BASE, changeCategory: "non_breaking" }, // wire-enum-ok
isDark,
);
expect(res?.kind).toBe("additive");
expect(res?.tokens).toEqual({
stripeAccent: "var(--schema-color-added-accent)",
fg: cllAdditiveBadgeFg[mode],
badgeBg: cllAdditiveBadgeBg[mode],
badgeBorder: cllAdditiveAccent[mode],
});
});
}
});

describe("graph badge — column-changed", () => {
for (const isDark of [false, true] as const) {
const mode = isDark ? "dark" : "light";
it(`reuses the brown/changed palette in ${mode} mode`, () => {
const res = pickGraphBadge(
{ ...BASE, changeCategory: "partial_breaking" }, // wire-enum-ok
isDark,
);
expect(res?.kind).toBe("column-changed");
expect(res?.tokens).toEqual({
stripeAccent: "var(--schema-color-changed-accent)",
fg: cllChangedBadgeFg[mode],
badgeBg: cllChangedBadgeBg[mode],
badgeBorder: cllChangedAccent,
});
});
}
});

describe("graph badge — column-impacted", () => {
for (const isDark of [false, true] as const) {
const mode = isDark ? "dark" : "light";
it(`reuses the amber/impacted palette in ${mode} mode`, () => {
const res = pickGraphBadge({ ...BASE, isImpacted: true }, isDark);
expect(res?.kind).toBe("column-impacted");
expect(res?.tokens).toEqual({
stripeAccent: "var(--schema-color-impacted-accent)",
fg: cllImpactedBadgeFg[mode],
badgeBg: cllImpactedBadgeBg[mode],
badgeBorder: cllImpactedAccent[mode],
});
});
}
});
});
20 changes: 9 additions & 11 deletions js/packages/ui/src/components/lineage/nodes/LineageNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
useRef,
useState,
} from "react";
import { colors } from "../../../theme/colors";
import { DIM_FILTER } from "../config/zoomConstants";
import {
getIconForMaterialization,
Expand Down Expand Up @@ -367,10 +368,16 @@ function LineageNodeComponent({
const borderWidth = "2px";
const borderColor = colorChangeStatus;

// Themed surface + text colors, sourced from the shared `colors` design
// tokens (the codebase's dark-mode mechanism) and selected by the `isDark`
// prop — rather than inline hex literals. The dark card surface uses
// neutral[800] (#262626), matching the sibling LineageColumnNode card.
const paperBg = isDark ? colors.neutral[800] : colors.white;
const primaryText = isDark ? colors.white : colors.black;
const invertedText = isDark ? colors.black : colors.white;

// Node background color logic
const nodeBackgroundColor = (() => {
const paperBg = isDark ? "#1e1e1e" : "#ffffff";

if (showContent) {
if (selectMode === "selecting") {
return isSelected ? colorChangeStatus : paperBg;
Expand All @@ -392,9 +399,6 @@ function LineageNodeComponent({

// Text color logic
const titleColor = (() => {
const primaryText = isDark ? "#ffffff" : "#000000";
const invertedText = isDark ? "#000000" : "#ffffff";

if (selectMode === "selecting") {
return isSelected ? invertedText : primaryText;
}
Expand All @@ -405,9 +409,6 @@ function LineageNodeComponent({
})();

const iconColor = (() => {
const primaryText = isDark ? "#ffffff" : "#000000";
const invertedText = isDark ? "#000000" : "#ffffff";

if (selectMode === "selecting") {
return isSelected ? invertedText : primaryText;
}
Expand All @@ -418,9 +419,6 @@ function LineageNodeComponent({
})();

const changeStatusIconColor = (() => {
const primaryText = isDark ? "#ffffff" : "#000000";
const invertedText = isDark ? "#000000" : "#ffffff";

if (selectMode === "selecting") {
return isSelected ? invertedText : colorChangeStatus;
}
Expand Down
Loading
Loading