Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions apps/desktop/src/snapShot/DesktopSnapShot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3839,6 +3839,35 @@ it.effect("registers macOS modifier pairs through the flags poller", () => {
).pipe(Effect.provide(testLayer("darwin")));
});

it.effect("releases the macOS both-Shift poller when snapshots are turned off", () => {
spawnedPollers.length = 0;
accessibilityTrustedMock.mockReturnValue(true);
mediaAccessStatusMock.mockReturnValue("granted");
const settings = {
...DEFAULT_CLIENT_SETTINGS,
snapShotEnabled: true,
snapShotShortcut: { kind: "both-shift-keys" },
} satisfies ClientSettings;

return Effect.scoped(
Effect.gen(function* () {
const service = yield* DesktopSnapShot.make;
yield* service.configure(settings);
assert.lengthOf(spawnedPollers, 1);
const poller = spawnedPollers[0]!;
assert.deepEqual(poller.args.slice(-2), ["2", "4"]);
assert.lengthOf(poller.kill.mock.calls, 0);
assert.isTrue((yield* service.state).shortcutRegistered);

yield* service.configure({ ...settings, snapShotEnabled: false });
assert.lengthOf(poller.kill.mock.calls, 1);
assert.isFalse((yield* service.state).shortcutRegistered);
const failure = yield* Effect.flip(service.capture);
assert.equal(failure.operation, "disabled");
}),
).pipe(Effect.provide(testLayer("darwin")));
});

it.effect("waits to apply settings while permissions are pending", () => {
accessibilityTrustedMock.mockReturnValue(true);
mediaAccessStatusMock.mockReturnValueOnce("not-determined").mockReturnValue("granted");
Expand Down
216 changes: 216 additions & 0 deletions apps/web/src/components/desktop/SnapShotCoordinator.gate.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import type { DesktopPendingSnapShot, DesktopSnapShotEvent } from "@t3tools/contracts";
import { act } from "react";
import { create, type ReactTestRenderer } from "react-test-renderer";
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";

const pendingCapture: DesktopPendingSnapShot = {
id: "12345678-1234-1234-1234-123456789abc",
name: "window.png",
mimeType: "image/png",
sizeBytes: 3,
source: {
kind: "snap-shot",
capturedAt: "2026-09-01T00:00:00.000Z",
appName: "Editor",
windowTitle: "main.ts",
},
};

const state = vi.hoisted(() => ({
enabled: false,
activeDraftThread: null as { environmentId: string; projectId: string } | null,
handleNewThread: vi.fn(async () => null),
playSound: vi.fn(),
toast: vi.fn(),
listPendingSnapShots: vi.fn(async (): Promise<ReadonlyArray<DesktopPendingSnapShot>> => []),
onSnapShotEvent: vi.fn((_listener: (event: DesktopSnapShotEvent) => void) => () => undefined),
}));

vi.mock("../../hooks/useSettings", () => ({
useClientSettings: (
select: (settings: {
snapShotEnabled: boolean;
snapShotPlaySound: boolean;
snapShotSound: "soft-pop";
snapShotAnimations: boolean;
}) => unknown,
) =>
select({
snapShotEnabled: state.enabled,
snapShotPlaySound: true,
snapShotSound: "soft-pop",
snapShotAnimations: true,
}),
}));

vi.mock("../../hooks/useHandleNewThread", () => ({
useHandleNewThread: () => ({
activeDraftThread: state.activeDraftThread,
activeThread: null,
defaultProjectRef: null,
handleNewThread: state.handleNewThread,
routeDraftId: null,
routeThreadRef: null,
}),
}));

vi.mock("../../lib/snapShotSound", () => ({
playSnapShotSound: (sound: string) => {
state.playSound(sound);
},
}));

vi.mock("../../lib/desktopSnapShot", () => ({
getDesktopSnapShotBridge: () => ({
listPendingSnapShots: state.listPendingSnapShots,
readSnapShot: vi.fn(),
acknowledgeSnapShot: vi.fn(async () => undefined),
onSnapShotEvent: state.onSnapShotEvent,
getSnapShotState: vi.fn(async () => ({ message: null })),
dismissSnapShotAnimation: vi.fn(async () => undefined),
}),
dispatchSnapShotComposerFocus: () => undefined,
}));

vi.mock("../ui/toast", () => ({
toastManager: {
add: (toast: { title?: string }) => {
state.toast(toast);
},
},
stackedThreadToast: (toast: { title: string; description?: string }) => toast,
}));

import { SnapShotCoordinator } from "./SnapShotCoordinator";

let renderer: ReactTestRenderer | undefined;
const listeners = new Map<string, Set<() => void>>();

function addEventListener(type: string, listener: () => void) {
const set = listeners.get(type) ?? new Set();
set.add(listener);
listeners.set(type, set);
}

function removeEventListener(type: string, listener: () => void) {
listeners.get(type)?.delete(listener);
}

async function render() {
await act(async () => {
if (renderer) renderer.update(<SnapShotCoordinator />);
else renderer = create(<SnapShotCoordinator />);
});
}

async function flush() {
await act(async () => {
await Promise.resolve();
});
}

beforeEach(() => {
listeners.clear();
state.enabled = false;
state.activeDraftThread = { environmentId: "env", projectId: "proj" };
state.handleNewThread.mockReset();
state.handleNewThread.mockResolvedValue(null);
state.playSound.mockReset();
state.toast.mockReset();
state.listPendingSnapShots.mockReset();
state.listPendingSnapShots.mockResolvedValue([]);
state.onSnapShotEvent.mockReset();
state.onSnapShotEvent.mockImplementation(() => () => undefined);
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
vi.stubGlobal("window", {
addEventListener,
removeEventListener,
matchMedia: () => ({ matches: false }),
});
vi.stubGlobal("document", {
visibilityState: "visible",
addEventListener,
removeEventListener,
});
});

afterEach(async () => {
await act(() => {
renderer?.unmount();
});
renderer = undefined;
vi.unstubAllGlobals();
});

describe("SnapShots while the feature is off", () => {
it("does not list, sound, or toast when chat state changes or the window focuses", async () => {
await render();
state.activeDraftThread = { environmentId: "env", projectId: "proj-typed" };
await render();
await act(async () => {
for (const listener of listeners.get("focus") ?? []) listener();
});
await flush();

expect(state.listPendingSnapShots).not.toHaveBeenCalled();
expect(state.onSnapShotEvent).not.toHaveBeenCalled();
expect(state.playSound).not.toHaveBeenCalled();
expect(state.toast).not.toHaveBeenCalled();
expect(state.handleNewThread).not.toHaveBeenCalled();
});

it("drops an in-flight pending list once the feature turns off", async () => {
let finishList: (captures: ReadonlyArray<DesktopPendingSnapShot>) => void = () => undefined;
state.listPendingSnapShots.mockImplementation(
() =>
new Promise<ReadonlyArray<DesktopPendingSnapShot>>((resolve) => {
finishList = resolve;
}),
);
state.enabled = true;
await render();
expect(state.listPendingSnapShots).toHaveBeenCalledTimes(1);

state.enabled = false;
await render();
finishList([pendingCapture]);
await flush();

expect(state.playSound).not.toHaveBeenCalled();
expect(state.toast).not.toHaveBeenCalled();
expect(state.handleNewThread).not.toHaveBeenCalled();
});

it("lists again after the feature is turned back on", async () => {
await render();
expect(state.listPendingSnapShots).not.toHaveBeenCalled();

state.enabled = true;
await render();
await flush();

expect(state.listPendingSnapShots).toHaveBeenCalledTimes(1);
});

it("sounds and toasts an undeliverable capture once across later chat updates", async () => {
state.enabled = true;
state.listPendingSnapShots.mockResolvedValue([pendingCapture]);
await render();
await vi.waitFor(() => {
expect(state.toast).toHaveBeenCalledTimes(1);
});
expect(state.playSound).toHaveBeenCalledTimes(1);
expect(state.toast.mock.calls[0]?.[0]).toMatchObject({
title: "Snapshot taken, but no project is available",
});

state.activeDraftThread = { environmentId: "env", projectId: "proj-typed" };
await render();
await vi.waitFor(() => {
expect(state.listPendingSnapShots.mock.calls.length).toBeGreaterThan(1);
});

expect(state.toast).toHaveBeenCalledTimes(1);
expect(state.playSound).toHaveBeenCalledTimes(1);
});
});
53 changes: 53 additions & 0 deletions apps/web/src/components/desktop/SnapShotCoordinator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
resolveExistingSnapShotTarget,
resolveSnapShotTargetOnce,
resolveSnapShotDeliveryTarget,
shouldReportUndeliverableSnapShot,
} from "./SnapShotCoordinator";
import {
beginSnapShotAnimation,
Expand Down Expand Up @@ -133,6 +134,58 @@ describe("window capture failures", () => {
});
});

describe("undeliverable capture reporting", () => {
it("reports a pending capture once however often the drain meets it again", () => {
const reported = new Set<string>();

expect(shouldReportUndeliverableSnapShot("capture-1", reported)).toBe(true);
expect(shouldReportUndeliverableSnapShot("capture-1", reported)).toBe(false);
expect(shouldReportUndeliverableSnapShot("capture-2", reported)).toBe(true);

reported.delete("capture-1");
expect(shouldReportUndeliverableSnapShot("capture-1", reported)).toBe(true);
});
});

describe("delivery stops when the feature turns off", () => {
it("leaves the capture pending when the file read finishes after the feature is off", async () => {
const target = DraftId.make("snap-shot-draft");
const capture = {
id: "12345678-1234-1234-1234-123456789abc",
name: "window.png",
mimeType: "image/png" as const,
sizeBytes: 3,
dataUrl: "data:image/png;base64,AQID",
source: {
kind: "snap-shot" as const,
capturedAt: "2026-09-01T00:00:00.000Z",
appName: "Editor",
windowTitle: "main.ts",
},
};
let finishRead: (value: typeof capture) => void = () => undefined;
const acknowledgeSnapShot = vi.fn(async () => undefined);
const bridge = {
readSnapShot: vi.fn(
() =>
new Promise<typeof capture>((resolve) => {
finishRead = resolve;
}),
),
acknowledgeSnapShot,
} as unknown as DesktopSnapShotBridge;
let enabled = true;

const delivery = deliverSnapShot(bridge, capture, target, () => enabled);
enabled = false;
finishRead(capture);

await expect(delivery).resolves.toBe(false);
expect(acknowledgeSnapShot).not.toHaveBeenCalled();
expect(useComposerDraftStore.getState().getComposerDraft(target)?.images ?? []).toHaveLength(0);
});
});

describe("window capture delivery", () => {
it.each([
{ target: DraftId.make("snap-shot-draft"), accessibleText: undefined },
Expand Down
Loading
Loading