194 lines
8.9 KiB
TypeScript
194 lines
8.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
import { DEFAULT_CANVAS_COLOR_THEME } from "../src/lib/canvas-theme";
|
|
|
|
import {
|
|
DEFAULT_CANVAS_BACKGROUND_MODE,
|
|
canvasAppearanceForTheme,
|
|
customCanvasAppearanceFromTheme,
|
|
enterCustomCanvasAppearance,
|
|
normalizeCanvasAppearance,
|
|
normalizeHexColor,
|
|
readCanvasAppearanceDefault,
|
|
resolveCanvasAppearance,
|
|
resolveCanvasGridColor,
|
|
writeCanvasAppearanceDefault,
|
|
} from "../src/lib/canvas/canvas-appearance";
|
|
|
|
const values = new Map<string, string>();
|
|
let originalWindow: PropertyDescriptor | undefined;
|
|
|
|
beforeEach(() => {
|
|
originalWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
|
|
values.clear();
|
|
Object.defineProperty(globalThis, "window", {
|
|
configurable: true,
|
|
value: {
|
|
localStorage: {
|
|
getItem: (key: string) => values.get(key) || null,
|
|
setItem: (key: string, value: string) => values.set(key, value),
|
|
removeItem: (key: string) => values.delete(key),
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (originalWindow) Object.defineProperty(globalThis, "window", originalWindow);
|
|
else Reflect.deleteProperty(globalThis, "window");
|
|
});
|
|
|
|
describe("canvas custom appearance", () => {
|
|
test("uses point grid as the default for new canvases", () => {
|
|
expect(DEFAULT_CANVAS_BACKGROUND_MODE).toBe("dots");
|
|
expect(DEFAULT_CANVAS_COLOR_THEME).toBe("dark");
|
|
});
|
|
|
|
test("resolves the black preset to a black background and fully opaque black grid", () => {
|
|
const appearance = canvasAppearanceForTheme(DEFAULT_CANVAS_COLOR_THEME);
|
|
expect(resolveCanvasAppearance(appearance, "light")).toEqual({
|
|
baseTheme: "dark",
|
|
background: "#000000",
|
|
grid: "#000000",
|
|
});
|
|
expect(resolveCanvasGridColor(appearance, "light", "dots")).toBe("#000000");
|
|
expect(resolveCanvasAppearance(customCanvasAppearanceFromTheme("dark"), "light")).toEqual({
|
|
baseTheme: "dark",
|
|
background: "#000000",
|
|
grid: "rgba(0,0,0,1)",
|
|
});
|
|
});
|
|
|
|
test("selecting either fixed theme resets the spatial grid to dots", async () => {
|
|
const source = await Bun.file(new URL("../src/components/canvas/canvas-appearance-controls.tsx", import.meta.url)).text();
|
|
const fixedThemeSource = source.slice(source.indexOf("const selectFixedTheme"), source.indexOf("const selectCustomTheme"));
|
|
expect(fixedThemeSource).toContain("onBackgroundModeChange(DEFAULT_CANVAS_BACKGROUND_MODE)");
|
|
expect(resolveCanvasGridColor(canvasAppearanceForTheme("light"), "dark", "dots")).toBe("rgba(0,0,0,.80)");
|
|
});
|
|
|
|
test("inherits the active fixed theme the first time custom mode is selected", () => {
|
|
const light = enterCustomCanvasAppearance(canvasAppearanceForTheme("light"), "light");
|
|
expect(light).toEqual({
|
|
mode: "custom",
|
|
custom: {
|
|
baseTheme: "light",
|
|
backgroundColor: "#F0F0F0",
|
|
backgroundBrightness: 0,
|
|
gridColor: "#000000",
|
|
gridOpacity: 80,
|
|
},
|
|
});
|
|
|
|
const dark = enterCustomCanvasAppearance(canvasAppearanceForTheme("dark"), "dark");
|
|
expect(dark.custom).toMatchObject({ baseTheme: "dark", backgroundColor: "#000000", backgroundBrightness: 0, gridColor: "#000000", gridOpacity: 100 });
|
|
});
|
|
|
|
test("restores a previous custom profile only under the same base theme", () => {
|
|
const previous = customCanvasAppearanceFromTheme("light");
|
|
previous.custom = { ...previous.custom!, backgroundColor: "#F3DCE5" };
|
|
const fixedLight = canvasAppearanceForTheme("light", previous);
|
|
expect(enterCustomCanvasAppearance(fixedLight, "light").custom).toEqual(previous.custom);
|
|
|
|
const fixedDark = canvasAppearanceForTheme("dark", previous);
|
|
expect(fixedDark.custom).toBeUndefined();
|
|
expect(enterCustomCanvasAppearance(fixedDark, "dark").custom).toMatchObject({
|
|
baseTheme: "dark",
|
|
backgroundColor: "#000000",
|
|
});
|
|
});
|
|
|
|
test("keeps color-picker popups inside the appearance interaction boundary", async () => {
|
|
const toolbarSource = await Bun.file(new URL("../src/components/canvas/canvas-toolbar.tsx", import.meta.url)).text();
|
|
const controlsSource = await Bun.file(new URL("../src/components/canvas/canvas-appearance-controls.tsx", import.meta.url)).text();
|
|
expect(toolbarSource).toContain('closest(".ant-color-picker,.ant-popover")');
|
|
expect(controlsSource).toContain("if (normalized) onChange(normalized)");
|
|
});
|
|
|
|
test("commits custom colors immediately instead of discarding them when the panel closes", async () => {
|
|
const controlsSource = await Bun.file(new URL("../src/components/canvas/canvas-appearance-controls.tsx", import.meta.url)).text();
|
|
const updateCustomSource = controlsSource.slice(
|
|
controlsSource.indexOf("const updateCustom"),
|
|
controlsSource.indexOf("const resetCustom"),
|
|
);
|
|
expect(updateCustomSource).toContain("onAppearanceChange(next)");
|
|
expect(updateCustomSource).not.toContain("onAppearancePreviewChange(next)");
|
|
});
|
|
|
|
test("keeps hex editing live across parent updates and common hex formats", async () => {
|
|
expect(normalizeHexColor("F3DCE5")).toBe("#F3DCE5");
|
|
expect(normalizeHexColor("#f3d")).toBe("#FF33DD");
|
|
|
|
const controlsSource = await Bun.file(new URL("../src/components/canvas/canvas-appearance-controls.tsx", import.meta.url)).text();
|
|
expect(controlsSource).toContain("if (!editing) setTextValue(value)");
|
|
expect(controlsSource).toContain("updateCustom({ backgroundColor, backgroundBrightness: 0 })");
|
|
});
|
|
|
|
test("adjusts only the custom canvas substrate and grid", () => {
|
|
const appearance = normalizeCanvasAppearance({
|
|
mode: "custom",
|
|
custom: {
|
|
baseTheme: "light",
|
|
backgroundColor: "#F3DCE5",
|
|
backgroundBrightness: 0,
|
|
backgroundOpacity: 10,
|
|
gridColor: "#9D7182",
|
|
gridOpacity: 22,
|
|
},
|
|
}, "dark");
|
|
|
|
const resolved = resolveCanvasAppearance(appearance, "dark");
|
|
expect(resolved.baseTheme).toBe("light");
|
|
expect(resolved.background).toBe("#F3DCE5");
|
|
expect(resolveCanvasGridColor(appearance, "dark", "lines")).toBe("rgba(157,113,130,0.22)");
|
|
|
|
appearance.custom!.backgroundBrightness = 10;
|
|
const brighter = resolveCanvasAppearance(appearance, "dark").background;
|
|
appearance.custom!.backgroundBrightness = -10;
|
|
const darker = resolveCanvasAppearance(appearance, "dark").background;
|
|
|
|
const brighterParts = brighter.match(/^oklch\(([\d.]+)% ([\d.]+) ([\d.]+)\)$/);
|
|
const darkerParts = darker.match(/^oklch\(([\d.]+)% ([\d.]+) ([\d.]+)\)$/);
|
|
expect(brighterParts).not.toBeNull();
|
|
expect(darkerParts).not.toBeNull();
|
|
expect(Number(brighterParts![1])).toBeGreaterThan(Number(darkerParts![1]));
|
|
expect(brighterParts!.slice(2)).toEqual(darkerParts!.slice(2));
|
|
});
|
|
|
|
test("switches the custom interface style without changing custom canvas colors", () => {
|
|
const appearance = customCanvasAppearanceFromTheme("dark");
|
|
appearance.custom = {
|
|
...appearance.custom!,
|
|
baseTheme: "light",
|
|
backgroundColor: "#F3DCE5",
|
|
gridColor: "#9D7182",
|
|
gridOpacity: 22,
|
|
};
|
|
|
|
expect(resolveCanvasAppearance(appearance, "dark")).toEqual({
|
|
baseTheme: "light",
|
|
background: "#F3DCE5",
|
|
grid: "rgba(157,113,130,0.22)",
|
|
});
|
|
});
|
|
|
|
test("does not expose background opacity after legacy values are ignored", async () => {
|
|
const controlsSource = await Bun.file(new URL("../src/components/canvas/canvas-appearance-controls.tsx", import.meta.url)).text();
|
|
expect(controlsSource).toContain('const LIGHT_PRESETS = ["#F0F0F0"');
|
|
expect(controlsSource).toContain('const DARK_PRESETS = ["#000000"');
|
|
expect(controlsSource).not.toContain('label="背景透明度"');
|
|
expect(controlsSource).toContain('aria-label="界面样式"');
|
|
expect(controlsSource).toContain('label="网格强度"');
|
|
});
|
|
|
|
test("stores defaults locally with the active account scope", () => {
|
|
window.localStorage.setItem("infinite-canvas:active-user-scope", "account-A");
|
|
const value = { appearance: customCanvasAppearanceFromTheme("dark"), backgroundMode: "lines" as const };
|
|
writeCanvasAppearanceDefault(value);
|
|
|
|
expect(values.has("infinite-canvas:canvas-appearance-default:user:account-A")).toBe(true);
|
|
expect(readCanvasAppearanceDefault()).toEqual(value);
|
|
|
|
window.localStorage.setItem("infinite-canvas:active-user-scope", "account-B");
|
|
expect(readCanvasAppearanceDefault()).toBeNull();
|
|
});
|
|
});
|