- Move the Tauri app (React frontend + Rust backend) into desktop/ via git mv — configs unchanged (relative paths: tauri.conf.json, vite.config.ts) - Root package.json becomes a private bun workspace container with orchestration scripts; desktop package renamed gridline-desktop - Scaffold www/ with Astro 7 + Tailwind v4 (hand-wired vite plugin, static output, ready for Dokploy) - Update release.yml for the new layout: projectPath: desktop, desktop/src-tauri resource paths, rust-cache workspace path - Update README + AGENTS.md structure trees and dev commands - Verified: vitest (1074), cargo test (354), desktop build, tauri dev (window launches), tauri build (dmg + app bundles), Astro build
165 lines
6.0 KiB
TypeScript
165 lines
6.0 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { applyTheme, applyFontSize, applyAccentColor, resolveTheme } from "./useAppearance";
|
|
|
|
const windowMocks = vi.hoisted(() => ({
|
|
setTheme: vi.fn().mockResolvedValue(undefined),
|
|
setBackgroundColor: vi.fn().mockResolvedValue(undefined),
|
|
}));
|
|
|
|
vi.mock("@tauri-apps/api/window", () => ({
|
|
getCurrentWindow: () => ({
|
|
setTheme: windowMocks.setTheme,
|
|
setBackgroundColor: windowMocks.setBackgroundColor,
|
|
}),
|
|
}));
|
|
|
|
describe("useAppearance helpers", () => {
|
|
const getRoot = () => document.documentElement;
|
|
|
|
const stubMatchMedia = (
|
|
matches: boolean,
|
|
addEventListener: ReturnType<typeof vi.fn>,
|
|
removeEventListener: ReturnType<typeof vi.fn>,
|
|
) => {
|
|
Object.defineProperty(window, "matchMedia", {
|
|
configurable: true,
|
|
writable: true,
|
|
value: vi.fn().mockReturnValue({
|
|
matches,
|
|
addEventListener,
|
|
removeEventListener,
|
|
}),
|
|
});
|
|
};
|
|
|
|
beforeEach(() => {
|
|
getRoot().classList.remove("light");
|
|
delete getRoot().dataset.fontSize;
|
|
getRoot().style.removeProperty("--color-accent");
|
|
windowMocks.setTheme.mockClear();
|
|
windowMocks.setBackgroundColor.mockClear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
getRoot().classList.remove("light");
|
|
delete getRoot().dataset.fontSize;
|
|
getRoot().style.removeProperty("--color-accent");
|
|
delete (window as unknown as { matchMedia?: unknown }).matchMedia;
|
|
});
|
|
|
|
it('applyTheme("dark") removes the light class', () => {
|
|
getRoot().classList.add("light");
|
|
applyTheme("dark");
|
|
expect(getRoot().classList.contains("light")).toBe(false);
|
|
});
|
|
|
|
it('applyTheme("light") adds the light class', () => {
|
|
applyTheme("light");
|
|
expect(getRoot().classList.contains("light")).toBe(true);
|
|
});
|
|
|
|
it('applyTheme("system") keeps dark when the OS does not prefer light', () => {
|
|
const addEventListener = vi.fn();
|
|
const removeEventListener = vi.fn();
|
|
stubMatchMedia(false, addEventListener, removeEventListener);
|
|
const cleanup = applyTheme("system");
|
|
expect(getRoot().classList.contains("light")).toBe(false);
|
|
expect(addEventListener).toHaveBeenCalled();
|
|
cleanup();
|
|
expect(removeEventListener).toHaveBeenCalled();
|
|
});
|
|
|
|
it('applyTheme("system") adds the light class when the OS prefers light', () => {
|
|
stubMatchMedia(true, vi.fn(), vi.fn());
|
|
applyTheme("system");
|
|
expect(getRoot().classList.contains("light")).toBe(true);
|
|
});
|
|
|
|
describe("resolveTheme", () => {
|
|
it('resolveTheme("light") is "light"', () => {
|
|
expect(resolveTheme("light")).toBe("light");
|
|
});
|
|
|
|
it('resolveTheme("dark") is "dark"', () => {
|
|
expect(resolveTheme("dark")).toBe("dark");
|
|
});
|
|
|
|
it('resolveTheme("system") is "dark" when the OS does not prefer light', () => {
|
|
stubMatchMedia(false, vi.fn(), vi.fn());
|
|
expect(resolveTheme("system")).toBe("dark");
|
|
});
|
|
|
|
it('resolveTheme("system") is "light" when the OS prefers light', () => {
|
|
stubMatchMedia(true, vi.fn(), vi.fn());
|
|
expect(resolveTheme("system")).toBe("light");
|
|
});
|
|
|
|
it('resolveTheme("system") falls back to "dark" without matchMedia', () => {
|
|
delete (window as unknown as { matchMedia?: unknown }).matchMedia;
|
|
expect(resolveTheme("system")).toBe("dark");
|
|
});
|
|
});
|
|
|
|
it('applyFontSize("small") sets the data attribute', () => {
|
|
applyFontSize("small");
|
|
expect(getRoot().dataset.fontSize).toBe("small");
|
|
});
|
|
|
|
it('applyFontSize("large") sets the data attribute', () => {
|
|
applyFontSize("large");
|
|
expect(getRoot().dataset.fontSize).toBe("large");
|
|
});
|
|
|
|
it('applyFontSize("medium") removes the data attribute', () => {
|
|
getRoot().dataset.fontSize = "large";
|
|
applyFontSize("medium");
|
|
expect(getRoot().dataset.fontSize).toBeUndefined();
|
|
});
|
|
|
|
it('applyAccentColor sets the custom property for a valid hex', () => {
|
|
applyAccentColor("#22C55E");
|
|
expect(
|
|
getRoot().style.getPropertyValue("--color-accent").toLowerCase()
|
|
).toBe("#22c55e");
|
|
});
|
|
|
|
it('applyAccentColor accepts lowercase hex', () => {
|
|
applyAccentColor("#2563eb");
|
|
expect(
|
|
getRoot().style.getPropertyValue("--color-accent").toLowerCase()
|
|
).toBe("#2563eb");
|
|
});
|
|
|
|
it('applyAccentColor removes the custom property for an invalid value', () => {
|
|
getRoot().style.setProperty("--color-accent", "#22C55E");
|
|
applyAccentColor("blue");
|
|
expect(getRoot().style.getPropertyValue("--color-accent")).toBe("");
|
|
});
|
|
|
|
it('applyAccentColor rejects malformed hex', () => {
|
|
applyAccentColor("#22C5");
|
|
expect(getRoot().style.getPropertyValue("--color-accent")).toBe("");
|
|
});
|
|
|
|
it("syncs the native window (theme + background) for light", async () => {
|
|
applyTheme("light");
|
|
// Allow the fire-and-forget dynamic import + invoke to settle.
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(windowMocks.setTheme).toHaveBeenCalledWith("light");
|
|
expect(windowMocks.setBackgroundColor).toHaveBeenCalledWith("#FAFAFA");
|
|
});
|
|
|
|
it("syncs the native window (theme + background) for dark", async () => {
|
|
applyTheme("dark");
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(windowMocks.setTheme).toHaveBeenCalledWith("dark");
|
|
expect(windowMocks.setBackgroundColor).toHaveBeenCalledWith("#0A0A0B");
|
|
});
|
|
|
|
it("resets the window to follow the OS when switching to system", async () => {
|
|
stubMatchMedia(false, vi.fn(), vi.fn());
|
|
applyTheme("system");
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
expect(windowMocks.setTheme).toHaveBeenCalledWith(null);
|
|
});
|
|
}); |