refactor: convert to bun-workspaces monorepo (desktop/ + www/)
- 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
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user