fix(desktop): preserve explicit Linux theme
This commit is contained in:
parent
9889ce5a44
commit
96c56d9e4d
|
|
@ -0,0 +1,110 @@
|
|||
// @vitest-environment happy-dom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
let mediaQueryChangeListener: ((event: MediaQueryListEvent) => void) | undefined;
|
||||
let mediaQueryMatches = false;
|
||||
const requestAnimationFrameMock = vi.fn((callback: FrameRequestCallback) => {
|
||||
callback(0);
|
||||
return 0;
|
||||
});
|
||||
const setTheme = vi.fn(async () => {});
|
||||
|
||||
function installLocalStorageStub() {
|
||||
const store = new Map<string, string>();
|
||||
const storage = {
|
||||
get length() {
|
||||
return store.size;
|
||||
},
|
||||
clear: vi.fn(() => store.clear()),
|
||||
getItem: vi.fn((key: string) => store.get(key) ?? null),
|
||||
key: vi.fn((index: number) => [...store.keys()][index] ?? null),
|
||||
removeItem: vi.fn((key: string) => store.delete(key)),
|
||||
setItem: vi.fn((key: string, value: string) => store.set(key, String(value))),
|
||||
} as Storage;
|
||||
Object.defineProperty(globalThis, "localStorage", { configurable: true, value: storage });
|
||||
Object.defineProperty(window, "localStorage", { configurable: true, value: storage });
|
||||
}
|
||||
|
||||
function installBrowserStubs() {
|
||||
const mediaQuery = {
|
||||
get matches() {
|
||||
return mediaQueryMatches;
|
||||
},
|
||||
addEventListener: vi.fn((event: string, listener: (event: MediaQueryListEvent) => void) => {
|
||||
if (event === "change") mediaQueryChangeListener = listener;
|
||||
}),
|
||||
removeEventListener: vi.fn(),
|
||||
};
|
||||
vi.stubGlobal("navigator", { userAgent: "Mozilla/5.0 (X11; Linux x86_64)" });
|
||||
vi.stubGlobal("requestAnimationFrame", requestAnimationFrameMock);
|
||||
Object.defineProperty(window, "matchMedia", { configurable: true, value: vi.fn(() => mediaQuery) });
|
||||
}
|
||||
|
||||
async function loadTheme(mode: "light" | "dark" | "system" = "system") {
|
||||
window.localStorage.setItem("dbx-theme", mode);
|
||||
const { useTheme } = await import("@/composables/useTheme");
|
||||
return useTheme();
|
||||
}
|
||||
|
||||
async function flushDynamicImport() {
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
describe("useTheme on Linux", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
installLocalStorageStub();
|
||||
window.localStorage.clear();
|
||||
document.documentElement.className = "";
|
||||
document.documentElement.style.colorScheme = "";
|
||||
mediaQueryChangeListener = undefined;
|
||||
mediaQueryMatches = false;
|
||||
installBrowserStubs();
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => true }));
|
||||
vi.doMock("@tauri-apps/api/window", () => ({ getCurrentWindow: () => ({ setTheme }) }));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.doUnmock("@/lib/backend/tauriRuntime");
|
||||
vi.doUnmock("@tauri-apps/api/window");
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("uses the Linux system preference after a fresh startup without overwriting GTK", async () => {
|
||||
mediaQueryMatches = true;
|
||||
const theme = await loadTheme();
|
||||
|
||||
theme.applyTheme();
|
||||
|
||||
expect(theme.isDark.value).toBe(true);
|
||||
expect(document.documentElement.classList.contains("dark")).toBe(true);
|
||||
expect(setTheme).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("continues to apply system preference changes without writing the native theme", async () => {
|
||||
const theme = await loadTheme();
|
||||
theme.applyTheme();
|
||||
|
||||
mediaQueryChangeListener?.({ matches: true } as MediaQueryListEvent);
|
||||
|
||||
expect(theme.isDark.value).toBe(true);
|
||||
expect(document.documentElement.classList.contains("dark")).toBe(true);
|
||||
expect(setTheme).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still writes explicit dark and light choices to the native Linux window theme", async () => {
|
||||
const theme = await loadTheme("dark");
|
||||
expect(theme.themeMode.value).toBe("dark");
|
||||
|
||||
theme.applyTheme();
|
||||
await flushDynamicImport();
|
||||
expect(setTheme).toHaveBeenLastCalledWith("dark");
|
||||
|
||||
theme.setThemeMode("light");
|
||||
await flushDynamicImport();
|
||||
expect(setTheme).toHaveBeenLastCalledWith("light");
|
||||
});
|
||||
});
|
||||
|
|
@ -18,6 +18,10 @@ import {
|
|||
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
|
||||
function isLinuxTauriRuntime() {
|
||||
return isTauriRuntime() && typeof navigator !== "undefined" && /linux/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
const savedThemeMode = safeLocalStorageGet(APP_THEME_STORAGE_KEY);
|
||||
const themeMode = ref<AppThemeMode>(normalizeAppThemeMode(savedThemeMode));
|
||||
const savedThemePalette = safeLocalStorageGet(APP_THEME_PALETTE_STORAGE_KEY);
|
||||
|
|
@ -67,19 +71,24 @@ function applyTheme() {
|
|||
// force reflow so the class toggle takes effect before re-enabling transitions
|
||||
doc.offsetHeight; // eslint-disable-line @typescript-eslint/no-unused-expressions
|
||||
requestAnimationFrame(() => doc.classList.remove("disable-transitions"));
|
||||
|
||||
if (!isTauriRuntime()) return;
|
||||
|
||||
const tauriTheme = getTauriThemeForMode(themeMode.value);
|
||||
// Tauri owns the Linux GTK preference. Calling setTheme(null) here forces it
|
||||
// to light, so only skip the native system-theme write and keep explicit modes.
|
||||
if (isLinuxTauriRuntime() && tauriTheme == null) return;
|
||||
|
||||
if (cachedTauriWindow) {
|
||||
cachedTauriWindow
|
||||
.getCurrentWindow()
|
||||
.setTheme(getTauriThemeForMode(themeMode.value))
|
||||
.setTheme(tauriTheme)
|
||||
.catch(() => {});
|
||||
} else {
|
||||
import("@tauri-apps/api/window").then((mod) => {
|
||||
cachedTauriWindow = mod;
|
||||
mod
|
||||
.getCurrentWindow()
|
||||
.setTheme(getTauriThemeForMode(themeMode.value))
|
||||
.setTheme(tauriTheme)
|
||||
.catch(() => {});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue