import { DEFAULT_CLIENT_SETTINGS, type ClientSettingsPatch, type DesktopSnapShotState, } from "@t3tools/contracts"; import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test"; import { reactHookHarness as hooks } from "../../test/reactHookHarness"; import { visitElements } from "../../test/reactElementTree"; const effects = vi.hoisted(() => [] as (() => void)[]); vi.mock("react", async (original) => { const actual = await original(); const { reactHookHarness } = await import("../../test/reactHookHarness"); return { ...actual, useCallback: reactHookHarness.useCallback, useMemo: reactHookHarness.useMemo, useRef: reactHookHarness.useRef, useState: reactHookHarness.useState, useEffect: (effect: () => void) => effects.push(effect), }; }); vi.mock("react/compiler-runtime", async () => { const { reactHookHarness } = await import("../../test/reactHookHarness"); return { c: reactHookHarness.useMemoCache }; }); vi.mock("@effect/atom-react", () => ({ useAtomValue: () => [] })); vi.mock("../../state/server", () => ({ primaryServerKeybindingsAtom: {} })); const bridge = vi.hoisted(() => ({ getSnapShotState: vi.fn<() => Promise>(), setSnapShotShortcutSuppressed: vi.fn(), checkSnapShotShortcut: vi.fn(), previewSnapShotConfig: vi.fn(), applySnapShotConfig: vi.fn(), setupSnapShot: vi.fn(), requestSnapShotPermissions: vi.fn().mockResolvedValue(undefined), onMenuAction: vi.fn(), onSnapShotEvent: vi.fn(() => () => undefined), })); const toastManager = vi.hoisted(() => ({ add: vi.fn() })); vi.mock("../ui/toast", () => ({ toastManager })); vi.mock("../../lib/desktopSnapShot", () => ({ getDesktopSnapShotBridge: () => bridge })); const settingsStore = vi.hoisted(() => ({ current: {} as typeof DEFAULT_CLIENT_SETTINGS, update: vi.fn<(patch: ClientSettingsPatch) => Promise>(), })); vi.mock("../../hooks/useSettings", () => ({ useClientSettings: () => settingsStore.current, useUpdateClientSettings: () => settingsStore.update, })); import { SnapShotSettings } from "./SnapShotSettings"; import { SnapShotSetupDialog } from "./SnapShotSetupDialog"; import { CaptureShortcutConfig } from "./CaptureShortcutConfig"; import { SnapShotShortcutKeys } from "../desktop/SnapShotShortcutKeys"; let state: DesktopSnapShotState; function render() { hooks.beginRender(); return SnapShotSettings(); } function renderWithEffects() { effects.length = 0; const tree = render(); for (const effect of effects.splice(0)) effect(); return tree; } function wizard(tree: ReturnType) { return visitElements(tree, (element) => element.type === SnapShotSetupDialog); } function button(tree: ReturnType, label: string) { const node = visitElements( tree, (element) => element.props.children === label && typeof element.props.onClick === "function", ); if (!node) throw new Error(`Missing button: ${label}`); return node.props as { onClick: () => void }; } async function finish(promise: Promise) { await promise; await Promise.resolve(); } async function mount() { render(); for (const effect of effects.splice(0)) effect(); await finish(bridge.getSnapShotState.mock.results[0]!.value); return render(); } beforeEach(() => { hooks.reset(); effects.length = 0; vi.clearAllMocks(); vi.stubGlobal("navigator", { platform: "Linux" }); vi.stubGlobal("window", { addEventListener: vi.fn(), removeEventListener: vi.fn() }); settingsStore.current = { ...DEFAULT_CLIENT_SETTINGS, snapShotEnabled: true }; state = { mode: "portal", linuxBackend: "hyprland", shortcut: DEFAULT_CLIENT_SETTINGS.snapShotShortcut, shortcutRegistered: false, shortcutActionRegistered: true, shortcutMessage: null, message: null, hyprlandHelper: { status: "ready", message: "Ready" }, }; bridge.getSnapShotState.mockImplementation(async () => state); bridge.setSnapShotShortcutSuppressed.mockResolvedValue(undefined); bridge.checkSnapShotShortcut.mockResolvedValue({ available: true, message: null }); bridge.setupSnapShot.mockReset().mockResolvedValue(undefined); settingsStore.update.mockImplementation(async (patch) => { settingsStore.current = { ...settingsStore.current, ...patch }; state = { ...state, shortcut: settingsStore.current.snapShotShortcut }; }); }); afterEach(() => vi.unstubAllGlobals()); it.each(["niri", "hyprland"] as const)( "reopens %s setup at Shortcut without reading config or expanding Settings", async (desktop) => { state = { ...state, linuxBackend: desktop }; const tree = await mount(); expect(wizard(tree)).toBeNull(); button(tree, "Change shortcut").onClick(); await finish(bridge.getSnapShotState.mock.results[1]!.value); const opened = render(); expect(visitElements(opened, (element) => element.type === CaptureShortcutConfig)).toBeNull(); const dialog = wizard(opened); expect(dialog?.props.initialStep).toBe("shortcut"); expect(bridge.previewSnapShotConfig).not.toHaveBeenCalled(); expect(bridge.applySnapShotConfig).not.toHaveBeenCalled(); await (dialog!.props.onClose as (completed: boolean) => Promise)(false); expect(wizard(render())).toBeNull(); expect(settingsStore.update).not.toHaveBeenCalled(); expect(settingsStore.current.snapShotEnabled).toBe(true); }, ); it("returns to Access if the Hyprland helper needs attention before changing keys", async () => { const tree = await mount(); state = { ...state, hyprlandHelper: { status: "not-installed", message: "Install helper" } }; button(tree, "Change shortcut").onClick(); await finish(bridge.getSnapShotState.mock.results[1]!.value); expect(wizard(render())?.props.initialStep).toBe("access"); expect(bridge.previewSnapShotConfig).not.toHaveBeenCalled(); }); it.each(["direct", "gnome-extension", "kde"] as const)( "keeps %s shortcut recording and saving inline", async (backend) => { state = { ...state, mode: backend === "direct" ? "direct" : "portal", linuxBackend: backend === "direct" ? undefined : backend, shortcutRegistered: true, shortcutLabel: backend === "direct" ? undefined : "Press 2", shortcutMessage: backend === "direct" ? null : "Desktop shortcut: Press 2", }; const tree = await mount(); const recorder = (node: ReturnType) => { const control = visitElements(node, (element) => "data-keybinding-capture" in element.props); if (!control) throw new Error("Missing inline shortcut recorder"); return control.props; }; if (backend !== "direct") { expect(recorder(tree)["aria-label"]).toBe("Record snapshot shortcut, currently Ctrl+Shift+2"); const keycaps = visitElements(tree, (element) => element.type === SnapShotShortcutKeys); expect(keycaps?.props.shortcut).toMatchObject({ key: "2", ctrlKey: true, shiftKey: true }); expect( visitElements(tree, (element) => element.props.id === "snap-shot-shortcut")?.props.status, ).toBeNull(); } (recorder(tree).onClick as () => void)(); await finish(bridge.setSnapShotShortcutSuppressed.mock.results.at(-1)!.value); (recorder(render()).onKeyDown as (event: object) => void)({ key: "y", code: "KeyY", ctrlKey: true, altKey: true, shiftKey: false, metaKey: false, repeat: false, preventDefault: vi.fn(), stopPropagation: vi.fn(), }); await finish(bridge.checkSnapShotShortcut.mock.results[0]!.value); expect(recorder(render()).size).toBe("xs"); expect(recorder(render())["aria-label"]).toBe("Record snapshot shortcut, currently Ctrl+Alt+Y"); button(render(), "Save").onClick(); await finish(settingsStore.update.mock.results[0]!.value); expect(settingsStore.update).toHaveBeenCalledWith({ snapShotShortcut: expect.objectContaining({ key: "y", modKey: true, altKey: true }), }); expect(wizard(render())).toBeNull(); expect(bridge.previewSnapShotConfig).not.toHaveBeenCalled(); }, ); it("turns capture on directly on Windows without opening setup", async () => { settingsStore.current = { ...settingsStore.current, snapShotEnabled: false }; state = { ...state, mode: "direct", linuxBackend: undefined, windows: true }; const tree = await mount(); const toggle = visitElements( tree, (element) => element.props["aria-label"] === "Enable snapshots", ); if (!toggle) throw new Error("Missing capture toggle"); (toggle.props.onCheckedChange as (checked: boolean) => void)(true); await finish(settingsStore.update.mock.results[0]!.value); expect(settingsStore.update).toHaveBeenCalledWith({ snapShotEnabled: true }); expect(wizard(renderWithEffects())).toBeNull(); expect( visitElements(renderWithEffects(), (element) => element.props.children === "Manage capture"), ).toBeNull(); }); it("keeps the approved desktop shortcut when recording is cancelled in setup", async () => { state = { ...state, linuxBackend: "gnome-extension", gnomeExtension: { status: "enabled", message: "Ready" }, shortcutRegistered: true, shortcutLabel: "Press 8", }; const tree = await mount(); button(tree, "Manage capture").onClick(); await finish(bridge.getSnapShotState.mock.results[1]!.value); const shortcut = () => { const input = visitElements( wizard(render()), (element) => "data-keybinding-capture" in element.props, ); if (!input) throw new Error("Missing setup shortcut recorder"); return input.props; }; expect(shortcut()["aria-label"]).toBe("Record snapshot shortcut, currently Ctrl+Alt+8"); (shortcut().onClick as () => void)(); await finish(bridge.setSnapShotShortcutSuppressed.mock.results.at(-1)!.value); expect(shortcut().children).toBe("Press shortcut…"); (shortcut().onKeyDown as (event: object) => void)({ key: "Escape", preventDefault: vi.fn(), stopPropagation: vi.fn(), }); expect(shortcut()["aria-label"]).toBe("Record snapshot shortcut, currently Ctrl+Alt+8"); expect(settingsStore.update).not.toHaveBeenCalled(); expect(bridge.checkSnapShotShortcut).not.toHaveBeenCalled(); }); function usePortalShortcut(shortcutCanRetry: boolean) { const shortcut = { key: "2", modKey: false, ctrlKey: true, altKey: false, shiftKey: true, metaKey: false, }; settingsStore.current = { ...settingsStore.current, snapShotShortcut: shortcut }; state = { ...state, linuxBackend: "gnome-extension", gnomeExtension: { status: "enabled", message: "Ready" }, shortcut, linuxFeedbackAvailable: true, shortcutRegistered: true, shortcutCanRetry, }; } it("keeps older portal shortcuts editable without offering unsupported permissions", async () => { usePortalShortcut(false); const tree = await mount(); expect(() => button(tree, "Shortcut permissions")).toThrow("Missing button"); expect(bridge.setupSnapShot).not.toHaveBeenCalled(); const recorder = visitElements(tree, (element) => "data-keybinding-capture" in element.props); (recorder!.props.onClick as () => void)(); await finish(bridge.setSnapShotShortcutSuppressed.mock.results.at(-1)!.value); expect( visitElements(render(), (element) => "data-keybinding-capture" in element.props)?.props .children, ).toBe("Press shortcut…"); }); it("shows permission errors once in a toast and allows retrying", async () => { usePortalShortcut(true); bridge.setupSnapShot.mockRejectedValueOnce(new Error("The desktop service disconnected.")); button(await mount(), "Shortcut permissions").onClick(); await finish(bridge.setupSnapShot.mock.results[0]!.value.catch(() => undefined)); const tree = renderWithEffects(); expect(toastManager.add).toHaveBeenCalledExactlyOnceWith({ type: "error", title: "Couldn't open shortcut permissions", description: "The desktop service disconnected.", }); expect(visitElements(tree, (element) => element.props.role === "alert")).toBeNull(); expect(state.shortcutRegistered).toBe(true); button(renderWithEffects(), "Shortcut permissions").onClick(); await finish(bridge.setupSnapShot.mock.results[1]!.value); renderWithEffects(); expect(bridge.setupSnapShot).toHaveBeenCalledTimes(2); expect(toastManager.add).toHaveBeenCalledTimes(1); }); it("keeps a failed preference unchanged and reports the save error in a toast", async () => { usePortalShortcut(true); const flash = (tree: ReturnType) => { const control = visitElements( tree, (element) => element.props["aria-label"] === "Flash captured window", ); if (!control) throw new Error("Missing flash control"); return control.props as { onCheckedChange: (checked: boolean) => void; checked: boolean }; }; settingsStore.update.mockRejectedValueOnce(new Error("The settings file is read-only.")); flash(await mount()).onCheckedChange(false); await finish(settingsStore.update.mock.results[0]!.value.catch(() => undefined)); expect(flash(renderWithEffects()).checked).toBe(true); expect(toastManager.add).toHaveBeenCalledExactlyOnceWith({ type: "error", title: "Couldn't save capture settings", description: "The settings file is read-only.", }); flash(renderWithEffects()).onCheckedChange(false); await finish(settingsStore.update.mock.results[1]!.value); expect(flash(renderWithEffects()).checked).toBe(false); expect(toastManager.add).toHaveBeenCalledTimes(1); }); it("keeps setup errors in the wizard and does not toast them after closing it", async () => { usePortalShortcut(true); button(await mount(), "Manage capture").onClick(); await finish(bridge.getSnapShotState.mock.results[1]!.value); bridge.setupSnapShot.mockRejectedValueOnce(new Error("The desktop service disconnected.")); const action = wizard(render())!.props.onAction as (action: "retry-shortcut") => Promise; await action("retry-shortcut"); const dialog = wizard(renderWithEffects())!; expect(dialog.props.error).toBe("The desktop service disconnected."); expect(toastManager.add).not.toHaveBeenCalled(); await (dialog.props.onClose as (completed: boolean) => Promise)(false); expect(wizard(renderWithEffects())).toBeNull(); expect(toastManager.add).not.toHaveBeenCalled(); }); it.each([false, true])( "resumes macOS permission setup after restart and clears it on close, completed=%s", async (completed) => { const { readSnapShotSetupResume } = await import("../../lib/snapShotSetupResume"); const values = new Map(); Object.assign(window, { localStorage: { getItem: (key: string) => values.get(key) ?? null, setItem: (key: string, value: string) => values.set(key, value), removeItem: (key: string) => values.delete(key), }, }); settingsStore.current = { ...settingsStore.current, snapShotEnabled: false }; state = { mode: "direct", shortcut: DEFAULT_CLIENT_SETTINGS.snapShotShortcut, shortcutRegistered: false, shortcutMessage: null, message: null, macPermissions: { screenRecording: false, accessibility: false }, }; const tree = await mount(); const toggle = visitElements( tree, (element) => element.props["aria-label"] === "Enable snapshots", ); (toggle!.props.onCheckedChange as (checked: boolean) => void)(true); await finish(bridge.getSnapShotState.mock.results[1]!.value); bridge.setupSnapShot.mockImplementationOnce(async () => { expect(readSnapShotSetupResume()).toEqual({ wasEnabled: false }); }); await (wizard(render())!.props.onAction as (action: string) => Promise)( "allow-screen-recording", ); hooks.reset(); effects.length = 0; bridge.getSnapShotState.mockClear(); state = { ...state, macPermissions: { screenRecording: true, accessibility: true } }; const resumed = wizard(await mount()); expect(resumed).not.toBeNull(); expect(resumed!.props.initialStep).toBe("access"); expect(resumed!.props.wasEnabled).toBe(false); await (resumed!.props.onClose as (completed: boolean) => Promise)(completed); expect(wizard(render())).toBeNull(); expect(readSnapShotSetupResume()).toBeNull(); }, ); it("requires a successful macOS test capture before enabling and allows retry", async () => { settingsStore.current = { ...DEFAULT_CLIENT_SETTINGS, snapShotEnabled: false }; state = { mode: "direct", shortcut: DEFAULT_CLIENT_SETTINGS.snapShotShortcut, shortcutRegistered: false, shortcutMessage: null, message: null, macPermissions: { screenRecording: true, accessibility: true }, }; const tree = await mount(); const toggle = visitElements( tree, (element) => element.props["aria-label"] === "Enable snapshots", ); (toggle!.props.onCheckedChange as (checked: boolean) => void)(true); await finish(bridge.getSnapShotState.mock.results[1]!.value); bridge.setupSnapShot.mockRejectedValueOnce(new Error("Capture was denied")); const enable = () => (wizard(render())!.props.onEnable as () => Promise)(); expect(await enable()).toBe(false); expect(wizard(render())!.props.error).toBe("Capture was denied"); expect(settingsStore.current.snapShotEnabled).toBe(false); expect(await enable()).toBe(true); expect(bridge.setupSnapShot.mock.calls).toEqual([["test-mac-capture"], ["test-mac-capture"]]); expect(settingsStore.current.snapShotEnabled).toBe(true); });