1import { assert, describe, it } from "@effect/vitest";
2import * as Cause from "effect/Cause";
3import * as Effect from "effect/Effect";
4import * as Exit from "effect/Exit";
5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
7
8import * as ElectronWindow from "../../electron/ElectronWindow.ts";
9import * as ElectronDialog from "../../electron/ElectronDialog.ts";
10import * as DesktopSnapShot from "../../snapShot/DesktopSnapShot.ts";
11import {
12 checkSnapShotShortcut,
13 requestSnapShotPermissions,
14 setupSnapShot,
15 previewSnapShotConfig,
16 applySnapShotConfig,
17 setSnapShotAnimationDestination,
18 setSnapShotShortcutSuppressed,
19 snapShotScreenFrame,
20 snapShotRelativeFrame,
21} from "./snapShot.ts";
22
23describe("window capture IPC", () => {
24 const configPreview = {
25 id: "12345678-1234-1234-1234-123456789abc",
26 path: "/config/niri/config.kdl",
27 resolvedPath: "/config/niri/config.kdl",
28 before: "binds {}\n",
29 after: "binds {\n}\n",
30 shortcut: "Ctrl+Shift+2",
31 operation: "install" as const,
32 };
33 it.effect("requires a trusted renderer for both config read and write approval", () => {
34 const calls: string[] = [];
35 return Effect.gen(function* () {
36 const request = { operation: "install" as const, chooseFile: false };
37 const untrustedRead = yield* Effect.exit(
38 previewSnapShotConfig.handler(request, { sender: { id: 8 } }),
39 );
40 const untrustedWrite = yield* Effect.exit(
41 applySnapShotConfig.handler(configPreview.id, { sender: { id: 8 } }),
42 );
43 assert(Exit.isFailure(untrustedRead));
44 assert(Exit.isFailure(untrustedWrite));
45 assert.deepEqual(calls, []);
46 yield* previewSnapShotConfig.handler(request, { sender: { id: 7 } });
47 assert.deepEqual(calls, ["read"]);
48 yield* applySnapShotConfig.handler(configPreview.id, { sender: { id: 7 } });
49 assert.deepEqual(calls, ["read", configPreview.id]);
50 }).pipe(
51 Effect.provide(
52 Layer.mergeAll(
53 Layer.succeed(ElectronWindow.ElectronWindow, {
54 main: Effect.succeedSome({ webContents: { id: 7 } }),
55 } as ElectronWindow.ElectronWindow["Service"]),
56 Layer.succeed(DesktopSnapShot.DesktopSnapShot, {
57 previewConfig: () =>
58 Effect.sync(() => {
59 calls.push("read");
60 return configPreview;
61 }),
62 applyConfig: (id: string) =>
63 Effect.sync(() => {
64 calls.push(id);
65 return { backupPath: null, warning: null };
66 }),
67 } as unknown as DesktopSnapShot.DesktopSnapShot["Service"]),
68 Layer.succeed(
69 ElectronDialog.ElectronDialog,
70 {} as ElectronDialog.ElectronDialog["Service"],
71 ),
72 ),
73 ),
74 );
75 });
76
77 it.effect("cancelling custom file selection reads and writes nothing", () => {
78 let read = false;
79 return Effect.gen(function* () {
80 const preview = yield* previewSnapShotConfig.handler(
81 { operation: "install", chooseFile: true },
82 { sender: { id: 7 } },
83 );
84 assert.isNull(preview);
85 assert.isFalse(read);
86 }).pipe(
87 Effect.provide(
88 Layer.mergeAll(
89 Layer.succeed(ElectronWindow.ElectronWindow, {
90 main: Effect.succeedSome({ webContents: { id: 7 } }),
91 } as ElectronWindow.ElectronWindow["Service"]),
92 Layer.succeed(DesktopSnapShot.DesktopSnapShot, {
93 state: Effect.succeed({
94 linuxBackend: "niri",
95 shortcutConfigPath: "/config/niri/config.kdl",
96 }),
97 previewConfig: () =>
98 Effect.sync(() => {
99 read = true;
100 return configPreview;
101 }),
102 } as unknown as DesktopSnapShot.DesktopSnapShot["Service"]),
103 Layer.succeed(ElectronDialog.ElectronDialog, {
104 pickFiles: () => Effect.succeed([]),
105 } as unknown as ElectronDialog.ElectronDialog["Service"]),
106 ),
107 ),
108 );
109 });
110
111 it.effect("uses only the file returned by the native custom config picker", () => {
112 let path: string | undefined;
113 return Effect.gen(function* () {
114 yield* previewSnapShotConfig.handler(
115 { operation: "install", chooseFile: true },
116 { sender: { id: 7 } },
117 );
118 assert.equal(path, "/chosen/config.kdl");
119 }).pipe(
120 Effect.provide(
121 Layer.mergeAll(
122 Layer.succeed(ElectronWindow.ElectronWindow, {
123 main: Effect.succeedSome({ webContents: { id: 7 } }),
124 } as ElectronWindow.ElectronWindow["Service"]),
125 Layer.succeed(DesktopSnapShot.DesktopSnapShot, {
126 state: Effect.succeed({ linuxBackend: "niri" }),
127 previewConfig: (_: unknown, selected: string) =>
128 Effect.sync(() => {
129 path = selected;
130 return configPreview;
131 }),
132 } as unknown as DesktopSnapShot.DesktopSnapShot["Service"]),
133 Layer.succeed(ElectronDialog.ElectronDialog, {
134 pickFiles: () => Effect.succeed(["/chosen/config.kdl"]),
135 } as unknown as ElectronDialog.ElectronDialog["Service"]),
136 ),
137 ),
138 );
139 });
140
141 it("converts renderer viewport coordinates from the content origin using the window zoom", () => {
142 assert.deepEqual(
143 snapShotScreenFrame(
144 { x: 12, y: 20, width: 208, height: 112 },
145 { x: 100, y: 80, width: 1_000, height: 700 },
146 1.25,
147 ),
148 { x: 115, y: 105, width: 260, height: 140 },
149 );
150 });
151
152 it.effect("forwards a trusted renderer animation destination in screen coordinates", () => {
153 let received: unknown;
154 const webContents = { id: 7, getZoomFactor: () => 1.25 };
155 const layer = Layer.mergeAll(
156 Layer.succeed(
157 ElectronWindow.ElectronWindow,
158 ElectronWindow.ElectronWindow.of({
159 main: Effect.succeedSome({
160 getBounds: () => ({ x: 100, y: 80, width: 1_000, height: 700 }),
161 getContentBounds: () => ({ x: 100, y: 118, width: 1_000, height: 662 }),
162 webContents,
163 }),
164 } as ElectronWindow.ElectronWindow["Service"]),
165 ),
166 Layer.succeed(
167 DesktopSnapShot.DesktopSnapShot,
168 DesktopSnapShot.DesktopSnapShot.of({
169 setAnimationDestination: (id: string, destination: unknown) =>
170 Effect.sync(() => {
171 received = { id, destination };
172 }),
173 } as unknown as DesktopSnapShot.DesktopSnapShot["Service"]),
174 ),
175 );
176
177 return Effect.gen(function* () {
178 yield* setSnapShotAnimationDestination.handler(
179 {
180 id: "12345678-1234-1234-1234-123456789abc",
181 viewportFrame: { x: 12, y: 20, width: 208, height: 112 },
182 backgroundColor: "rgb(20, 20, 20)",
183 borderColor: "rgba(80, 80, 80, 0.8)",
184 borderWidth: 1,
185 cornerRadius: 8,
186 details: {
187 appName: "T3 Code",
188 windowTitle: "Capture animation",
189 appIconDataUrl: "data:image/png;base64,aWNvbg==",
190 },
191 },
192 { sender: webContents },
193 );
194 assert.deepEqual(received, {
195 id: "12345678-1234-1234-1234-123456789abc",
196 destination: {
197 relativeFrame: { x: 15 / 1000, y: 25 / 662, width: 260 / 1000, height: 140 / 662 },
198 frame: { x: 115, y: 143, width: 260, height: 140 },
199 backgroundColor: "rgb(20, 20, 20)",
200 borderColor: "rgba(80, 80, 80, 0.8)",
201 borderWidth: 1.25,
202 cornerRadius: 10,
203 scaleFactor: 1.25,
204 details: {
205 appName: "T3 Code",
206 windowTitle: "Capture animation",
207 appIconDataUrl: "data:image/png;base64,aWNvbg==",
208 },
209 },
210 });
211 }).pipe(Effect.provide(layer));
212 });
213
214 it.effect("forwards the accessibility permission preference from a trusted renderer", () => {
215 let includeAccessibility: boolean | undefined;
216 const webContents = { id: 7 };
217 const layer = Layer.mergeAll(
218 Layer.succeed(
219 ElectronWindow.ElectronWindow,
220 ElectronWindow.ElectronWindow.of({
221 main: Effect.succeedSome({ webContents }),
222 } as ElectronWindow.ElectronWindow["Service"]),
223 ),
224 Layer.succeed(
225 DesktopSnapShot.DesktopSnapShot,
226 DesktopSnapShot.DesktopSnapShot.of({
227 requestPermissions: (include: boolean) =>
228 Effect.sync(() => {
229 includeAccessibility = include;
230 }),
231 } as unknown as DesktopSnapShot.DesktopSnapShot["Service"]),
232 ),
233 );
234
235 return Effect.gen(function* () {
236 yield* requestSnapShotPermissions.handler(false, { sender: webContents });
237 assert.isFalse(includeAccessibility);
238 }).pipe(Effect.provide(layer));
239 });
240
241 it.effect("rejects an untrusted renderer at the IPC boundary", () =>
242 Effect.gen(function* () {
243 const exit = yield* Effect.exit(
244 requestSnapShotPermissions.handler(false, { sender: { id: 8 } }),
245 );
246 assert(Exit.isFailure(exit));
247 const failure = Cause.findErrorOption(exit.cause);
248 assert(Option.isSome(failure));
249 const error = failure.value;
250
251 assert.equal((error as { readonly _tag: string })._tag, "SnapShotIpcUnauthorizedSenderError");
252 assert.equal((error as Error).message, "Snapshot request was rejected.");
253 }).pipe(
254 Effect.provideService(
255 ElectronWindow.ElectronWindow,
256 ElectronWindow.ElectronWindow.of({
257 main: Effect.succeedSome({ webContents: { id: 7 } }),
258 } as ElectronWindow.ElectronWindow["Service"]),
259 ),
260 Effect.provideService(DesktopSnapShot.DesktopSnapShot, null as never),
261 ),
262 );
263
264 it.effect("allows capture setup only from the trusted main renderer", () => {
265 const actions: string[] = [];
266 return Effect.gen(function* () {
267 yield* setupSnapShot.handler("install-extension", { sender: { id: 7 } });
268 assert.deepEqual(actions, ["install-extension"]);
269 const rejected = yield* Effect.exit(
270 setupSnapShot.handler("enable-extension", { sender: { id: 8 } }),
271 );
272 assert(Exit.isFailure(rejected));
273 assert.deepEqual(actions, ["install-extension"]);
274 }).pipe(
275 Effect.provide(
276 Layer.mergeAll(
277 Layer.succeed(ElectronWindow.ElectronWindow, {
278 main: Effect.succeedSome({ webContents: { id: 7 } }),
279 } as ElectronWindow.ElectronWindow["Service"]),
280 Layer.succeed(DesktopSnapShot.DesktopSnapShot, {
281 setup: (action: string) =>
282 Effect.sync(() => {
283 actions.push(action);
284 }),
285 } as unknown as DesktopSnapShot.DesktopSnapShot["Service"]),
286 ),
287 ),
288 );
289 });
290
291 it.effect("checks shortcut availability for a trusted renderer", () => {
292 const layer = Layer.mergeAll(
293 Layer.succeed(
294 ElectronWindow.ElectronWindow,
295 ElectronWindow.ElectronWindow.of({
296 main: Effect.succeedSome({ webContents: { id: 7 } }),
297 } as ElectronWindow.ElectronWindow["Service"]),
298 ),
299 Layer.succeed(
300 DesktopSnapShot.DesktopSnapShot,
301 DesktopSnapShot.DesktopSnapShot.of({
302 checkShortcut: () => Effect.succeed({ available: true, message: null }),
303 } as unknown as DesktopSnapShot.DesktopSnapShot["Service"]),
304 ),
305 );
306
307 return Effect.gen(function* () {
308 const result = yield* checkSnapShotShortcut.handler(
309 { kind: "both-shift-keys" },
310 { sender: { id: 7 } },
311 );
312 assert.deepEqual(result, { available: true, message: null });
313 }).pipe(Effect.provide(layer));
314 });
315 it.effect("suppresses the active shortcut for a trusted renderer", () => {
316 let suppressed = false;
317 const layer = Layer.mergeAll(
318 Layer.succeed(
319 ElectronWindow.ElectronWindow,
320 ElectronWindow.ElectronWindow.of({
321 main: Effect.succeedSome({ webContents: { id: 7 } }),
322 } as ElectronWindow.ElectronWindow["Service"]),
323 ),
324 Layer.succeed(
325 DesktopSnapShot.DesktopSnapShot,
326 DesktopSnapShot.DesktopSnapShot.of({
327 setShortcutSuppressed: (next: boolean) =>
328 Effect.sync(() => {
329 suppressed = next;
330 }),
331 } as unknown as DesktopSnapShot.DesktopSnapShot["Service"]),
332 ),
333 );
334
335 return Effect.gen(function* () {
336 yield* setSnapShotShortcutSuppressed.handler(true, { sender: { id: 7 } });
337 assert.isTrue(suppressed);
338 }).pipe(Effect.provide(layer));
339 });
340});
341
342it("normalizes Wayland attachment coordinates without trusting Electron's screen origin", () => {
343 const frame = { x: 100, y: 300, width: 200, height: 100 };
344 const bounds = { x: 0, y: 0, width: 1000, height: 600 };
345 assert.deepEqual(snapShotRelativeFrame(frame, bounds, 1.25), {
346 x: 0.125,
347 y: 0.625,
348 width: 0.25,
349 height: 125 / 600,
350 });
351 assert.deepEqual(
352 snapShotRelativeFrame(frame, { ...bounds, x: -3840, y: 900 }, 1.25),
353 snapShotRelativeFrame(frame, bounds, 1.25),
354 );
355 assert.isUndefined(snapShotRelativeFrame(frame, { ...bounds, width: 0 }, 1));
356});