mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
test(control-ui): guard the hand-mirrored theme --bg copies (#130444)
Each theme's --bg is copied by hand into two places outside base.css, because both have to paint before the app stylesheet is parsed: index.html's pre-paint block, which fills the page during first paint, and the Appearance preview chips, which are deliberately theme-invariant and so cannot read the live token. Nothing tied those copies to the palette. Darkening Absolutely moved --bg and left both behind; only a pre-paint assertion that happened to hardcode the old value caught one of them. It is the same shape as the two defects review found in the theme work — a hand-maintained list extended for new themes but not exhaustively — and the third time is enough. Derives the expected values from base.css and asserts both mirrors for every palette (12 themed pre-paint rules plus the default), so a palette edit cannot leave either copy stale. Verified by mutating each mirror: tide: pre-paint #123456 != --bg #10151b beacon: chip #abcdef != --bg #000000 Test-only; no production change.
This commit is contained in:
committed by
GitHub
parent
b10beeee7c
commit
d4c8aaa536
@@ -91,3 +91,130 @@ describe("Control UI base theme tokens", () => {
|
||||
expect(localOverrides).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* --bg is mirrored by hand in two places outside base.css, because both must
|
||||
* paint before the app stylesheet is parsed or the picker would advertise a
|
||||
* colour the theme no longer uses:
|
||||
* - index.html's pre-paint block, which fills the page during first paint
|
||||
* - the Appearance preview chips in config.css, which are theme-invariant and
|
||||
* so cannot read the live token
|
||||
* Nothing tied those copies to the palette, and every review pass on the theme
|
||||
* work turned up another hand-maintained list that had drifted. Darkening
|
||||
* Absolutely moved --bg and silently left both copies behind until a pre-paint
|
||||
* assertion caught one of them. This derives the expected values from base.css
|
||||
* so a palette edit cannot leave either copy stale again.
|
||||
*/
|
||||
describe("Control UI theme --bg mirrors", () => {
|
||||
const RESOLVED_THEME_BG_SELECTOR = new Map<string, string>([
|
||||
["dark", ":root"],
|
||||
["light", ':root[data-theme-mode="light"]'],
|
||||
]);
|
||||
|
||||
function readBlockToken(css: string, selector: string, token: string): string | undefined {
|
||||
const body = css.split(new RegExp(`${escapeSelector(selector)}\\s*\\{`, "u"))[1]?.split("}")[0];
|
||||
return body?.match(new RegExp(`${token}:\\s*([^;]+);`, "u"))?.[1]?.trim();
|
||||
}
|
||||
|
||||
function escapeSelector(selector: string): string {
|
||||
return selector.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
|
||||
}
|
||||
|
||||
/** Every theme's canonical --bg, keyed by the resolved data-theme value. */
|
||||
function readCanonicalBackgrounds(): Map<string, string> {
|
||||
const baseCss = fs.readFileSync(path.join(stylesDir, "base.css"), "utf8");
|
||||
const backgrounds = new Map<string, string>();
|
||||
for (const [resolved, selector] of RESOLVED_THEME_BG_SELECTOR) {
|
||||
const value = readBlockToken(baseCss, selector, "--bg");
|
||||
if (value) {
|
||||
backgrounds.set(resolved, value);
|
||||
}
|
||||
}
|
||||
for (const match of baseCss.matchAll(/:root\[data-theme="([^"]+)"\]\s*\{/gu)) {
|
||||
const resolved = match[1] ?? "";
|
||||
const value = readBlockToken(baseCss, `:root[data-theme="${resolved}"]`, "--bg");
|
||||
if (value) {
|
||||
backgrounds.set(resolved, value);
|
||||
}
|
||||
}
|
||||
return backgrounds;
|
||||
}
|
||||
|
||||
it("keeps the index.html pre-paint background on every theme's --bg", () => {
|
||||
const indexHtml = fs.readFileSync(path.join(uiSrcDir, "..", "index.html"), "utf8");
|
||||
const canonical = readCanonicalBackgrounds();
|
||||
const mismatches: string[] = [];
|
||||
let checked = 0;
|
||||
|
||||
const prePaint = new Map<string, string>();
|
||||
for (const match of indexHtml.matchAll(
|
||||
/html(?:\[data-theme(?:-mode)?="([^"]+)"\])?\s*\{\s*background:\s*([^;]+);/gu,
|
||||
)) {
|
||||
const attribute = match[1];
|
||||
const value = (match[2] ?? "").trim();
|
||||
// Bare `html` is the default dark paint; the -mode selector is light.
|
||||
prePaint.set(attribute ?? "dark", value);
|
||||
}
|
||||
|
||||
for (const [resolved, background] of canonical) {
|
||||
const painted = prePaint.get(resolved);
|
||||
if (painted === undefined) {
|
||||
mismatches.push(`${resolved}: no pre-paint background in index.html`);
|
||||
continue;
|
||||
}
|
||||
checked += 1;
|
||||
if (painted.toLowerCase() !== background.toLowerCase()) {
|
||||
mismatches.push(`${resolved}: pre-paint ${painted} != --bg ${background}`);
|
||||
}
|
||||
}
|
||||
|
||||
expect(mismatches).toEqual([]);
|
||||
expect(checked).toBeGreaterThanOrEqual(RESOLVED_THEME_BG_SELECTOR.size);
|
||||
});
|
||||
|
||||
it("keeps the Appearance preview chips on every theme's --bg", () => {
|
||||
const configCss = fs.readFileSync(path.join(stylesDir, "config.css"), "utf8");
|
||||
const canonical = readCanonicalBackgrounds();
|
||||
const mismatches: string[] = [];
|
||||
let checked = 0;
|
||||
|
||||
for (const match of configCss.matchAll(/\.settings-theme-card--([\w-]+)[^{]*\{([^}]*)\}/gu)) {
|
||||
const family = match[1] ?? "";
|
||||
const body = match[2] ?? "";
|
||||
const chip = body.match(/--theme-chip-bg:\s*([^;]+);/u)?.[1]?.trim();
|
||||
if (!chip || chip.startsWith("var(")) {
|
||||
continue;
|
||||
}
|
||||
// Light chip rules are nested under the light-mode selector, so the
|
||||
// preceding text decides which resolved palette this chip mirrors.
|
||||
const isLight = configCss
|
||||
.slice(0, match.index ?? 0)
|
||||
.split(".settings-theme-card--")
|
||||
.at(-1)
|
||||
?.includes('data-theme-mode="light"');
|
||||
const resolved = resolveFamily(family, isLight === true);
|
||||
const background = canonical.get(resolved);
|
||||
if (!background) {
|
||||
continue;
|
||||
}
|
||||
checked += 1;
|
||||
if (chip.toLowerCase() !== background.toLowerCase()) {
|
||||
mismatches.push(`${resolved}: chip ${chip} != --bg ${background}`);
|
||||
}
|
||||
}
|
||||
|
||||
expect(mismatches).toEqual([]);
|
||||
expect(checked).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
/** Appearance uses theme family names; base.css keys off resolved values. */
|
||||
function resolveFamily(family: string, light: boolean): string {
|
||||
if (family === "claw") {
|
||||
return light ? "light" : "dark";
|
||||
}
|
||||
if (family === "knot") {
|
||||
return light ? "openknot-light" : "openknot";
|
||||
}
|
||||
return light ? `${family}-light` : family;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user