BlueSky & more on desktop
lazurite.stormlightlabs.org/
tauri
rust
typescript
bluesky
appview
atproto
solid
1import { ThemeController } from "$/components/theme/ThemeController";
2import type { AppSettings } from "$/lib/types";
3import { AppTestProviders } from "$/test/providers";
4import { render, waitFor } from "@solidjs/testing-library";
5import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
7type ThemeChangeHandler = (event: { payload: "light" | "dark" }) => void;
8const noopUnlisten = () => {};
9
10const tauriThemeMock = vi.hoisted(() => vi.fn(async () => null as "light" | "dark" | null));
11const onThemeChangedMock = vi.hoisted(() => vi.fn(async (_handler: ThemeChangeHandler) => noopUnlisten));
12
13vi.mock(
14 "@tauri-apps/api/window",
15 () => ({ getCurrentWindow: () => ({ label: "main", onThemeChanged: onThemeChangedMock, theme: tauriThemeMock }) }),
16);
17
18function installMatchMedia(initialDark: boolean) {
19 const listeners = new Set<(event: MediaQueryListEvent) => void>();
20 const media = {
21 matches: initialDark,
22 media: "(prefers-color-scheme: dark)",
23 onchange: null,
24 addEventListener: (_type: string, listener: (event: MediaQueryListEvent) => void) => {
25 listeners.add(listener);
26 },
27 removeEventListener: (_type: string, listener: (event: MediaQueryListEvent) => void) => {
28 listeners.delete(listener);
29 },
30 addListener: (listener: (event: MediaQueryListEvent) => void) => {
31 listeners.add(listener);
32 },
33 removeListener: (listener: (event: MediaQueryListEvent) => void) => {
34 listeners.delete(listener);
35 },
36 dispatch(nextDark: boolean) {
37 media.matches = nextDark;
38 const event = { matches: nextDark } as MediaQueryListEvent;
39 for (const listener of listeners) {
40 listener(event);
41 }
42 },
43 };
44
45 Object.defineProperty(globalThis, "matchMedia", { configurable: true, writable: true, value: vi.fn(() => media) });
46
47 return media;
48}
49
50describe("ThemeController", () => {
51 const baseSettings: AppSettings = {
52 theme: "auto",
53 timelineRefreshSecs: 60,
54 notificationsDesktop: true,
55 notificationsBadge: true,
56 notificationsSound: false,
57 embeddingsEnabled: false,
58 constellationUrl: "https://constellation.microcosm.blue",
59 spacedustUrl: "https://spacedust.microcosm.blue",
60 spacedustInstant: false,
61 spacedustEnabled: false,
62 globalShortcut: "Ctrl+Shift+N",
63 downloadDirectory: "/Users/test/Downloads",
64 };
65
66 beforeEach(() => {
67 tauriThemeMock.mockReset();
68 tauriThemeMock.mockResolvedValue(null);
69 onThemeChangedMock.mockReset();
70 onThemeChangedMock.mockResolvedValue(noopUnlisten);
71 });
72
73 afterEach(() => {
74 delete document.documentElement.dataset.theme;
75 document.documentElement.style.colorScheme = "";
76 });
77
78 it("applies explicit light theme", async () => {
79 installMatchMedia(true);
80
81 render(() => (
82 <AppTestProviders preferences={{ settings: { ...baseSettings, theme: "light" } }}>
83 <ThemeController />
84 </AppTestProviders>
85 ));
86
87 await waitFor(() => expect(document.documentElement.dataset.theme).toBe("light"));
88 expect(document.documentElement.style.colorScheme).toBe("light");
89 });
90
91 it("follows system theme changes when setting is auto", async () => {
92 const media = installMatchMedia(true);
93
94 render(() => (
95 <AppTestProviders preferences={{ settings: { ...baseSettings, theme: "auto" } }}>
96 <ThemeController />
97 </AppTestProviders>
98 ));
99
100 await waitFor(() => expect(document.documentElement.dataset.theme).toBe("dark"));
101
102 media.dispatch(false);
103 await waitFor(() => expect(document.documentElement.dataset.theme).toBe("light"));
104 });
105});