Files
oh-my-pi/packages/coding-agent/test/theme-auto-detection.test.ts
T
can1357 9b78a6fa85 refactor(natives): migrated native bindings to NAPI-RS with auto-generated types
- Migrated native bindings from TypeScript wrappers to NAPI-RS generated modules with auto-generated type definitions and runtime enums.
- Replaced chunk tree API with stateful ChunkState class supporting render, edit, and resolve operations with improved error handling.
- Converted callback signatures to error-first pattern (error, result) for shell, PTY, glob, and grep operations.
- Introduced type-safe enums for MacOSAppearance, GrepOutputMode, KeyEventType, ImageFormat, and AstMatchStrictness replacing string literals.
- Refactored chunk tree implementation with dedicated modules for edit, indent, resolve, and state management with comprehensive validation.
- Moved clipboard utilities from native bindings to coding-agent package with improved OSC 52 and Termux compatibility.
2026-04-07 02:00:59 +02:00

132 lines
4.7 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
import * as themeModule from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import * as nativesModule from "@oh-my-pi/pi-natives";
import { MacOSAppearance } from "@oh-my-pi/pi-natives";
const originalPlatform = process.platform;
const originalColorfgbg = Bun.env.COLORFGBG;
const originalZellij = Bun.env.ZELLIJ;
type ThemeTestGlobals = {
platform?: NodeJS.Platform;
colorfgbg?: string;
zellij?: string;
};
const withThemeTestGlobals = (globals: ThemeTestGlobals = {}) => {
Object.defineProperty(process, "platform", {
value: globals.platform ?? "darwin",
configurable: true,
writable: true,
});
if (globals.colorfgbg === undefined) delete Bun.env.COLORFGBG;
else Bun.env.COLORFGBG = globals.colorfgbg;
if (globals.zellij === undefined) delete Bun.env.ZELLIJ;
else Bun.env.ZELLIJ = globals.zellij;
return {
[Symbol.dispose]() {
themeModule.stopThemeWatcher();
Object.defineProperty(process, "platform", {
value: originalPlatform,
configurable: true,
writable: true,
});
if (originalColorfgbg === undefined) delete Bun.env.COLORFGBG;
else Bun.env.COLORFGBG = originalColorfgbg;
if (originalZellij === undefined) delete Bun.env.ZELLIJ;
else Bun.env.ZELLIJ = originalZellij;
vi.restoreAllMocks();
},
};
};
describe("theme auto-detection", () => {
beforeEach(async () => {
themeModule.stopThemeWatcher();
const darkTheme = await themeModule.getThemeByName("dark");
if (!darkTheme) {
throw new Error("Failed to load dark theme for tests");
}
themeModule.setThemeInstance(darkTheme);
vi.restoreAllMocks();
});
afterEach(() => {
themeModule.stopThemeWatcher();
vi.restoreAllMocks();
});
it("prefers COLORFGBG before macOS fallback inside Zellij", async () => {
using _globals = withThemeTestGlobals({ zellij: "1", colorfgbg: "15;0" });
const detectSpy = vi.spyOn(nativesModule, "detectMacOSAppearance").mockReturnValue(MacOSAppearance.Light);
await themeModule.initTheme(false, undefined, undefined, "dark", "light");
expect(themeModule.getCurrentThemeName()).toBe("dark");
expect(detectSpy).not.toHaveBeenCalled();
});
it("keeps honoring terminal-reported appearance outside fallback mode", async () => {
using _globals = withThemeTestGlobals();
const detectSpy = vi.spyOn(nativesModule, "detectMacOSAppearance").mockReturnValue(MacOSAppearance.Light);
const observerSpy = vi.spyOn(nativesModule.MacAppearanceObserver, "start");
themeModule.onTerminalAppearanceChange("dark");
await themeModule.initTheme(true, undefined, undefined, "dark", "light");
expect(themeModule.getCurrentThemeName()).toBe("dark");
expect(detectSpy).not.toHaveBeenCalled();
expect(observerSpy).not.toHaveBeenCalled();
});
it("updates auto theme from the native fallback observer in Zellij", async () => {
using _globals = withThemeTestGlobals({ zellij: "1" });
const stop = vi.fn();
let onAppearanceChange: ((appearance: "dark" | "light") => void) | undefined;
vi.spyOn(nativesModule, "detectMacOSAppearance").mockReturnValue(MacOSAppearance.Light);
const observerSpy = vi.spyOn(nativesModule.MacAppearanceObserver, "start").mockImplementation(((
callback: (err: null | Error, appearance: "dark" | "light") => void,
) => {
onAppearanceChange = (appearance: "dark" | "light") => callback(null, appearance);
return { stop };
}) as any);
await themeModule.initTheme(true, undefined, undefined, "dark", "light");
expect(observerSpy).toHaveBeenCalledTimes(1);
expect(themeModule.getCurrentThemeName()).toBe("light");
expect(onAppearanceChange).toBeDefined();
onAppearanceChange!("dark");
await Bun.sleep(0);
expect(themeModule.getCurrentThemeName()).toBe("dark");
themeModule.stopThemeWatcher();
expect(stop).toHaveBeenCalledTimes(1);
});
it("Zellij fallback stays macOS-only (Linux + Zellij = honor terminal)", async () => {
using _globals = withThemeTestGlobals({ platform: "linux", zellij: "1" });
const detectSpy = vi.spyOn(nativesModule, "detectMacOSAppearance").mockReturnValue(MacOSAppearance.Light);
themeModule.onTerminalAppearanceChange("dark");
await themeModule.initTheme(false, undefined, undefined, "dark", "light");
expect(themeModule.getCurrentThemeName()).toBe("dark");
expect(detectSpy).not.toHaveBeenCalled();
});
it("terminal-reported appearance wins over conflicting COLORFGBG", async () => {
using _globals = withThemeTestGlobals({ colorfgbg: "15;0" });
const detectSpy = vi.spyOn(nativesModule, "detectMacOSAppearance").mockReturnValue(MacOSAppearance.Light);
themeModule.onTerminalAppearanceChange("light");
await themeModule.initTheme(false, undefined, undefined, "dark", "light");
expect(themeModule.getCurrentThemeName()).toBe("light");
expect(detectSpy).not.toHaveBeenCalled();
});
});