1import { it as effectIt } from "@effect/vitest";
2import * as HostProcess from "@t3tools/shared/HostProcess";
3import * as Effect from "effect/Effect";
4import { describe, expect, it, vi } from "vite-plus/test";
5
6import {
7 accessibleWindowElementTree,
8 accessibleWindowText,
9 capturedImageBounds,
10 compactAccessibilityTree,
11 findAccessibleWindow,
12 findCaptureSource,
13 hideAndWaitForBlur,
14 isWaylandSession,
15 snapShotShortcutRegistrationFailureMessage,
16 snapShotShortcutSystemConflict,
17 toElectronAccelerator,
18} from "./snapShot.ts";
19import { DesktopSnapShotError } from "./DesktopSnapShot.ts";
20
21describe("window capture errors", () => {
22 it.each([
23 ["unsupported", "SnapShots are not supported here."],
24 ["disabled", "Enable SnapShots in Settings first."],
25 ["no-window-selected", "No window was selected."],
26 ["window-unavailable", "The active window is not available for capture."],
27 ["capture", "Could not capture the active window."],
28 ] as const)("keeps %s failures user-facing", (operation, message) => {
29 expect(new DesktopSnapShotError({ operation }).message).toBe(message);
30 });
31});
32
33describe("accessibleWindowText", () => {
34 it("keeps unique names and values in tree order", () => {
35 expect(
36 accessibleWindowText(
37 {
38 name: "Settings",
39 children: [
40 {
41 name: "General",
42 children: [],
43 },
44 {
45 name: "Name",
46 value: "Bilal",
47 children: [],
48 },
49 ],
50 },
51 100,
52 ),
53 ).toBe("Settings\nGeneral\nName\nBilal");
54 });
55
56 it("caps large text without splitting a surrogate pair", () => {
57 expect(
58 accessibleWindowText(
59 {
60 value: "abc😀def",
61 children: [],
62 },
63 5,
64 ),
65 ).toBe("abc😀");
66 });
67
68 it("stops traversing very large trees", () => {
69 expect(
70 accessibleWindowText(
71 {
72 children: [
73 ...Array.from({ length: 10_000 }, () => ({ children: [] })),
74 { value: "past node limit", children: [] },
75 ],
76 },
77 100,
78 ),
79 ).not.toContain("past node limit");
80 });
81});
82
83describe("accessibleWindowElementTree", () => {
84 it("maps element bounds into captured-image pixels and keeps semantic state", async () => {
85 const tree = await accessibleWindowElementTree(
86 {
87 role: "window",
88 name: "Editor",
89 bounds: { x: 100, y: 200, width: 800, height: 600 },
90 active: true,
91 children: async () => [
92 {
93 role: "button",
94 name: "Save",
95 description: "Save the document",
96 bounds: { x: 300, y: 350, width: 100, height: 50 },
97 focused: true,
98 actions: ["press", "press"],
99 children: async () => [],
100 },
101 ],
102 },
103 { x: 100, y: 200, width: 800, height: 600 },
104 { width: 1_600, height: 1_200 },
105 );
106
107 expect(tree).toEqual({
108 truncated: false,
109 root: {
110 role: "window",
111 name: "Editor",
112 bounds: { x: 0, y: 0, width: 1_600, height: 1_200 },
113 state: { active: true },
114 children: [
115 {
116 role: "button",
117 name: "Save",
118 description: "Save the document",
119 bounds: { x: 400, y: 300, width: 200, height: 100 },
120 state: { focused: true },
121 actions: ["press"],
122 children: [],
123 },
124 ],
125 },
126 });
127 });
128
129 it("uses null child bounds when a Wayland provider reports only the root origin", async () => {
130 const tree = await accessibleWindowElementTree(
131 {
132 role: "window",
133 bounds: { x: 0, y: 0, width: 800, height: 600 },
134 children: async () => [
135 {
136 role: "button",
137 name: "Save",
138 bounds: { x: 0, y: 0, width: 100, height: 50 },
139 children: async () => [],
140 },
141 ],
142 },
143 { x: 0, y: 0, width: 800, height: 600 },
144 { width: 1_600, height: 1_200 },
145 { locationsReliable: true, verifyDescendantLocations: true },
146 );
147
148 expect(tree?.root.bounds).toEqual({ x: 0, y: 0, width: 1_600, height: 1_200 });
149 expect(tree?.root.children[0]?.bounds).toBeNull();
150 });
151
152 it("keeps Wayland child bounds after observing real position variation", async () => {
153 const tree = await accessibleWindowElementTree(
154 {
155 role: "window",
156 bounds: { x: 0, y: 0, width: 800, height: 600 },
157 children: async () => [
158 {
159 role: "button",
160 name: "Save",
161 bounds: { x: 20, y: 30, width: 100, height: 50 },
162 children: async () => [],
163 },
164 ],
165 },
166 { x: 0, y: 0, width: 800, height: 600 },
167 { width: 1_600, height: 1_200 },
168 { locationsReliable: true, verifyDescendantLocations: true },
169 );
170
171 expect(tree?.root.children[0]?.bounds).toEqual({ x: 40, y: 60, width: 200, height: 100 });
172 });
173
174 it("does not mistake a title-bar offset for varying descendant positions", async () => {
175 const tree = await accessibleWindowElementTree(
176 {
177 role: "window",
178 bounds: { x: 100, y: 229, width: 800, height: 571 },
179 children: async () => [
180 {
181 role: "button",
182 name: "Save",
183 bounds: { x: 100, y: 229, width: 100, height: 50 },
184 children: async () => [],
185 },
186 ],
187 },
188 { x: 100, y: 200, width: 800, height: 600 },
189 { width: 1_600, height: 1_200 },
190 { locationsReliable: true, verifyDescendantLocations: true },
191 );
192 expect(tree?.root.children[0]?.bounds).toBeNull();
193 });
194
195 it("collapses anonymous wrapper chains and removes duplicate text fields", async () => {
196 const tree = await accessibleWindowElementTree(
197 {
198 role: "window",
199 name: "Editor",
200 bounds: { x: 0, y: 0, width: 800, height: 600 },
201 children: async () => [
202 {
203 role: "group",
204 bounds: { x: 0, y: 0, width: 800, height: 600 },
205 children: async () => [
206 {
207 role: "group",
208 bounds: { x: 0, y: 0, width: 800, height: 600 },
209 children: async () => [
210 {
211 role: "button",
212 name: "Save",
213 value: "Save",
214 description: "Save",
215 bounds: { x: 20, y: 30, width: 80, height: 24 },
216 actions: ["press"],
217 children: async () => [],
218 },
219 {
220 role: "group",
221 bounds: null,
222 children: async () => [],
223 },
224 ],
225 },
226 ],
227 },
228 ],
229 },
230 { x: 0, y: 0, width: 800, height: 600 },
231 { width: 800, height: 600 },
232 );
233
234 expect(tree?.root.children).toEqual([
235 {
236 role: "button",
237 name: "Save",
238 bounds: { x: 20, y: 30, width: 80, height: 24 },
239 actions: ["press"],
240 children: [],
241 },
242 ]);
243 });
244
245 it("marks trees truncated before their serialized payload grows without bound", async () => {
246 const tree = await accessibleWindowElementTree(
247 {
248 role: "window",
249 bounds: { x: 0, y: 0, width: 800, height: 600 },
250 children: async () =>
251 Array.from({ length: 10 }, (_, index) => ({
252 role: "text",
253 value: `${index}${"x".repeat(8_000)}`,
254 bounds: null,
255 children: async () => [],
256 })),
257 },
258 { x: 0, y: 0, width: 800, height: 600 },
259 { width: 800, height: 600 },
260 );
261
262 expect(tree?.truncated).toBe(true);
263 expect(tree?.root.children.length).toBeLessThan(10);
264 expect(JSON.stringify(tree).length).toBeLessThanOrEqual(32_000);
265 });
266
267 it.each(["name", "value", "description"] as const)(
268 "reports clipped %s text in partial trees",
269 async (field) => {
270 const tree = await accessibleWindowElementTree(
271 {
272 role: "window",
273 children: async () => [{ role: "text_area", [field]: "x".repeat(8_001) }],
274 },
275 { x: 0, y: 0, width: 800, height: 600 },
276 { width: 800, height: 600 },
277 );
278
279 expect(tree?.truncated).toBe(true);
280 expect(tree?.root.children[0]?.[field]?.length).toBeLessThan(8_001);
281 },
282 );
283});
284
285describe("compactAccessibilityTree", () => {
286 it("keeps anonymous groups that organize multiple semantic children", () => {
287 const compacted = compactAccessibilityTree({
288 role: "window",
289 bounds: { x: 0, y: 0, width: 800, height: 600 },
290 children: [
291 {
292 role: "group",
293 bounds: { x: 0, y: 0, width: 800, height: 100 },
294 children: [
295 { role: "button", name: "Back", bounds: null, children: [] },
296 { role: "button", name: "Forward", bounds: null, children: [] },
297 ],
298 },
299 ],
300 });
301
302 expect(compacted.root.children[0]?.role).toBe("group");
303 expect(compacted.root.children[0]?.children).toHaveLength(2);
304 });
305});
306
307describe("capturedImageBounds", () => {
308 it("clips locations to the attached image", () => {
309 expect(
310 capturedImageBounds(
311 { x: 50, y: 150, width: 200, height: 200 },
312 { x: 100, y: 200, width: 800, height: 600 },
313 { width: 1_600, height: 1_200 },
314 ),
315 ).toEqual({ x: 0, y: 0, width: 300, height: 300 });
316 });
317
318 it("returns null when an accessibility provider omits a location", () => {
319 expect(
320 capturedImageBounds(
321 null,
322 { x: 0, y: 0, width: 800, height: 600 },
323 { width: 800, height: 600 },
324 ),
325 ).toBeNull();
326 });
327});
328
329describe("findAccessibleWindow", () => {
330 const captured = {
331 title: "Editor",
332 bounds: { x: 100, y: 200, width: 800, height: 600 },
333 };
334
335 it("matches one window by its captured bounds", () => {
336 const windows = [
337 { name: "Private", bounds: { x: 0, y: 0, width: 400, height: 300 } },
338 { name: "Editor", bounds: { x: 101, y: 199, width: 800, height: 601 } },
339 ];
340
341 expect(findAccessibleWindow(windows, captured)).toBe(windows[1]);
342 });
343
344 it("uses the matched source title when macOS omits the active title", () => {
345 const windows = [{ name: "Editor", bounds: captured.bounds }];
346 expect(
347 findAccessibleWindow(windows, {
348 ...captured,
349 title: "",
350 sourceTitle: "Editor",
351 }),
352 ).toBe(windows[0]);
353 });
354
355 it("uses the captured source title when the active title changed", () => {
356 const windows = [{ name: "Editor — saved", bounds: captured.bounds }];
357
358 expect(
359 findAccessibleWindow(windows, {
360 ...captured,
361 title: "Editor — saving",
362 sourceTitle: "Editor — saved",
363 }),
364 ).toBe(windows[0]);
365 });
366
367 it("uses the active match to disambiguate equal windows", () => {
368 const windows = [
369 { name: "Editor", bounds: captured.bounds, active: false },
370 { name: "Editor", bounds: captured.bounds, active: true },
371 ];
372
373 expect(findAccessibleWindow(windows, captured)).toBe(windows[1]);
374 });
375
376 it("does not match equal bounds with a different title", () => {
377 expect(
378 findAccessibleWindow(
379 [{ name: "Private", bounds: { x: 100, y: 200, width: 800, height: 600 } }],
380 captured,
381 ),
382 ).toBeUndefined();
383 });
384
385 it("does not use a title match when the bounds differ", () => {
386 expect(
387 findAccessibleWindow(
388 [{ name: "Editor", bounds: { x: 0, y: 0, width: 800, height: 600 } }],
389 captured,
390 ),
391 ).toBeUndefined();
392 });
393
394 it("matches a Wayland window whose accessibility provider omits its screen position", () => {
395 const captured = {
396 title: "hello world (Draft) - Text Editor",
397 bounds: { x: 479, y: 342, width: 700, height: 520 },
398 };
399 const windows = [{ name: captured.title, bounds: { x: 0, y: 0, width: 700, height: 520 } }];
400
401 expect(findAccessibleWindow(windows, captured, "wayland")).toBe(windows[0]);
402 expect(findAccessibleWindow(windows, captured)).toBeUndefined();
403 });
404
405 it("matches a decorated Wayland window by its verified client size", () => {
406 const clientBounds = { x: 100, y: 229, width: 800, height: 571 };
407 const windows = [{ name: captured.title, bounds: { ...clientBounds, x: 0, y: 0 } }];
408 expect(findAccessibleWindow(windows, { ...captured, clientBounds }, "wayland")).toBe(
409 windows[0],
410 );
411 expect(findAccessibleWindow(windows, { ...captured, clientBounds })).toBeUndefined();
412 expect(findAccessibleWindow(windows, captured, "wayland")).toBeUndefined();
413 });
414
415 it("does not guess between client-size and frame-size matches", () => {
416 const clientBounds = { x: 100, y: 229, width: 800, height: 571 };
417 const windows = [
418 { name: captured.title, bounds: clientBounds },
419 { name: captured.title, bounds: captured.bounds },
420 ];
421 expect(findAccessibleWindow(windows, { ...captured, clientBounds }, "wayland")).toBeUndefined();
422 expect(
423 findAccessibleWindow(
424 [
425 { name: "Private", bounds: clientBounds },
426 { name: captured.title, bounds: { ...clientBounds, height: 550 } },
427 ],
428 { ...captured, clientBounds },
429 "wayland",
430 ),
431 ).toBeUndefined();
432 });
433
434 it("distinguishes same-title Wayland windows by size", () => {
435 const windows = [
436 { name: "Editor", bounds: { x: 0, y: 0, width: 400, height: 300 } },
437 { name: "Editor", bounds: { x: 0, y: 0, width: 801, height: 599 } },
438 ];
439
440 expect(findAccessibleWindow(windows, captured, "wayland")).toBe(windows[1]);
441 });
442
443 it.each([
444 { name: "Private", bounds: { x: 0, y: 0, width: 800, height: 600 } },
445 { name: "Editor", bounds: { x: 0, y: 0, width: 400, height: 600 } },
446 { name: "Editor", bounds: { x: 0, y: 0, width: 800, height: 300 } },
447 { name: "Editor", bounds: null },
448 ])("rejects an unverified Wayland window: %j", (window) => {
449 expect(findAccessibleWindow([window], captured, "wayland")).toBeUndefined();
450 });
451
452 it("rejects ambiguous Wayland windows even when one reports the captured screen position", () => {
453 const windows = [
454 { name: "Editor", bounds: captured.bounds },
455 { name: "Editor", bounds: { x: 0, y: 0, width: 800, height: 600 } },
456 ];
457
458 expect(findAccessibleWindow(windows, captured, "wayland")).toBeUndefined();
459 });
460
461 it("does not match an untitled Wayland window by size alone", () => {
462 const windows = [{ name: "", bounds: captured.bounds }];
463
464 expect(findAccessibleWindow(windows, { ...captured, title: "" }, "wayland")).toBeUndefined();
465 });
466
467 it("accepts one PID-scoped untitled window whose bounds match", () => {
468 const windows = [{ name: null, bounds: captured.bounds }];
469
470 expect(
471 findAccessibleWindow(windows, captured, "wayland", { allowUntitledUniqueBounds: true }),
472 ).toBe(windows[0]);
473 expect(findAccessibleWindow(windows, captured, "wayland")).toBeUndefined();
474 });
475
476 it("rejects ambiguous PID-scoped untitled windows even when bounds match", () => {
477 const windows = [
478 { name: null, bounds: captured.bounds },
479 { name: "", bounds: { ...captured.bounds, x: 0, y: 0 } },
480 ];
481
482 expect(
483 findAccessibleWindow(windows, captured, "wayland", { allowUntitledUniqueBounds: true }),
484 ).toBeUndefined();
485 });
486
487 it("does not fall back to a differently titled PID-scoped window with matching bounds", () => {
488 const windows = [{ name: "Preferences", bounds: captured.bounds }];
489
490 expect(
491 findAccessibleWindow(windows, captured, "wayland", { allowUntitledUniqueBounds: true }),
492 ).toBeUndefined();
493 });
494
495 it("does not use unique bounds when a titled match is already ambiguous", () => {
496 const windows = [
497 { name: "Editor", bounds: captured.bounds },
498 { name: "Editor", bounds: { ...captured.bounds, x: 0, y: 0 } },
499 ];
500
501 expect(
502 findAccessibleWindow(windows, captured, "wayland", { allowUntitledUniqueBounds: true }),
503 ).toBeUndefined();
504 });
505
506 it.each(["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"])(
507 "ignores a leading Wayland title spinner frame %s",
508 (frame) => {
509 const windows = [{ name: `${frame} t3code`, bounds: captured.bounds }];
510
511 expect(findAccessibleWindow(windows, { ...captured, title: "⠋ t3code" }, "wayland")).toBe(
512 windows[0],
513 );
514 },
515 );
516
517 it.each([
518 ["⠋ t3code", "t3code"],
519 ["t3code", "⠙ t3code"],
520 ])("matches a Wayland spinner starting or stopping: %s → %s", (title, name) => {
521 const windows = [{ name, bounds: captured.bounds }];
522
523 expect(findAccessibleWindow(windows, { ...captured, title }, "wayland")).toBe(windows[0]);
524 });
525
526 it.each([
527 ["⠋ t3code", "⠙ private"],
528 ["t3code ⠋", "t3code ⠙"],
529 ["⠋t3code", "⠙t3code"],
530 ["⠁ t3code", "⠙ t3code"],
531 ["⠋", "⠋"],
532 ])("does not guess a Wayland title match: %s → %s", (title, name) => {
533 expect(
534 findAccessibleWindow([{ name, bounds: captured.bounds }], { ...captured, title }, "wayland"),
535 ).toBeUndefined();
536 });
537
538 it("rejects matching spinners when the window sizes differ", () => {
539 expect(
540 findAccessibleWindow(
541 [{ name: "⠙ t3code", bounds: { ...captured.bounds, width: 400 } }],
542 { ...captured, title: "⠋ t3code" },
543 "wayland",
544 ),
545 ).toBeUndefined();
546 });
547
548 it("rejects ambiguous normalized titles even if one matches the captured spinner exactly", () => {
549 const windows = [
550 { name: "⠋ t3code", bounds: captured.bounds },
551 { name: "⠙ t3code", bounds: captured.bounds },
552 ];
553
554 expect(
555 findAccessibleWindow(windows, { ...captured, title: "⠋ t3code" }, "wayland"),
556 ).toBeUndefined();
557 });
558
559 it("keeps exact title matching outside Wayland", () => {
560 expect(
561 findAccessibleWindow([{ name: "⠙ t3code", bounds: captured.bounds }], {
562 ...captured,
563 title: "⠋ t3code",
564 }),
565 ).toBeUndefined();
566 });
567});
568
569describe("hideAndWaitForBlur", () => {
570 it("waits for a delayed blur after hiding the window", async () => {
571 let blur: (() => void) | undefined;
572 let settled = false;
573 const hidden = hideAndWaitForBlur({
574 hide: () => undefined,
575 once: (_event, listener) => {
576 blur = listener;
577 },
578 removeListener: () => undefined,
579 }).then(() => {
580 settled = true;
581 });
582
583 await Promise.resolve();
584 expect(settled).toBe(false);
585 blur?.();
586 await hidden;
587 expect(settled).toBe(true);
588 });
589
590 it("rejects when the hidden window never blurs", async () => {
591 vi.useFakeTimers();
592 try {
593 let rejected = false;
594 const hidden = hideAndWaitForBlur({
595 hide: () => undefined,
596 once: () => undefined,
597 removeListener: () => undefined,
598 }).catch(() => {
599 rejected = true;
600 });
601
602 await vi.advanceTimersByTimeAsync(1_000);
603 expect(rejected).toBe(true);
604 await hidden;
605 } finally {
606 vi.useRealTimers();
607 }
608 });
609});
610
611describe("toElectronAccelerator", () => {
612 it("converts the default portable shortcut", () => {
613 expect(
614 toElectronAccelerator({
615 key: "2",
616 metaKey: false,
617 ctrlKey: false,
618 shiftKey: true,
619 altKey: false,
620 modKey: true,
621 }),
622 ).toBe("CommandOrControl+Shift+2");
623 });
624
625 it("maps the portable meta key to Super", () => {
626 expect(
627 toElectronAccelerator({
628 key: "k",
629 metaKey: true,
630 ctrlKey: false,
631 shiftKey: false,
632 altKey: false,
633 modKey: false,
634 }),
635 ).toBe("Super+K");
636 });
637
638 it("normalizes Electron key names", () => {
639 expect(
640 toElectronAccelerator({
641 key: "ArrowUp",
642 metaKey: false,
643 ctrlKey: true,
644 shiftKey: false,
645 altKey: true,
646 modKey: false,
647 }),
648 ).toBe("Control+Alt+Up");
649 });
650});
651
652describe("findCaptureSource", () => {
653 const sources = [
654 { id: "window:42:0", name: "Terminal" },
655 { id: "window:84:0", name: "Editor" },
656 ];
657
658 it("matches the native window id before its title", () => {
659 expect(
660 findCaptureSource(sources, {
661 id: 84,
662 title: "Changed title",
663 }),
664 ).toEqual(sources[1]);
665 });
666
667 it("falls back to a unique title match", () => {
668 expect(
669 findCaptureSource(sources, {
670 id: 100,
671 title: "Terminal",
672 }),
673 ).toEqual(sources[0]);
674 });
675
676 it("does not guess when a title is ambiguous", () => {
677 expect(
678 findCaptureSource(
679 [
680 { id: "window:42:0", name: "Editor" },
681 { id: "window:84:0", name: "Editor" },
682 ],
683 {
684 id: 100,
685 title: "Editor",
686 },
687 ),
688 ).toBeUndefined();
689 });
690});
691
692describe("isWaylandSession", () => {
693 it.each([
694 ["linux", { XDG_SESSION_TYPE: "wayland" }, true],
695 ["linux", { WAYLAND_DISPLAY: "wayland-0" }, true],
696 ["linux", { XDG_SESSION_TYPE: "x11" }, false],
697 ["darwin", { XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0" }, false],
698 ] as const)("detects %s session %o as portal=%s", (platform, environment, expected) => {
699 expect(isWaylandSession(platform, environment)).toBe(expected);
700 });
701
702 effectIt.effect(
703 "falls back to a live runtime directory socket when session variables are stripped",
704 () =>
705 Effect.gen(function* () {
706 if ((yield* HostProcess.Platform) !== "linux") return;
707 yield* Effect.promise(async () => {
708 const { mkdtemp, rm, writeFile } = await import("node:fs/promises");
709 const { createServer } = await import("node:net");
710 const { tmpdir } = await import("node:os");
711 const { join } = await import("node:path");
712 const runtimeDirectory = await mkdtemp(join(tmpdir(), "t3-wayland-"));
713 const socketPath = join(runtimeDirectory, "wayland-0");
714 const server = createServer();
715 try {
716 expect(isWaylandSession("linux", { XDG_RUNTIME_DIR: runtimeDirectory })).toBe(false);
717 await writeFile(socketPath, "");
718 expect(isWaylandSession("linux", { XDG_RUNTIME_DIR: runtimeDirectory })).toBe(false);
719 await rm(socketPath);
720 await new Promise<void>((resolve, reject) => {
721 server.once("error", reject);
722 server.listen(socketPath, resolve);
723 });
724 expect(isWaylandSession("linux", { XDG_RUNTIME_DIR: runtimeDirectory })).toBe(true);
725 expect(
726 isWaylandSession("linux", {
727 XDG_RUNTIME_DIR: runtimeDirectory,
728 XDG_SESSION_TYPE: "x11",
729 }),
730 ).toBe(false);
731 expect(isWaylandSession("linux", { XDG_RUNTIME_DIR: "/nonexistent-t3-test" })).toBe(
732 false,
733 );
734 } finally {
735 if (server.listening) {
736 await new Promise<void>((resolve, reject) => {
737 server.close((error) => (error ? reject(error) : resolve()));
738 });
739 }
740 await rm(runtimeDirectory, { recursive: true, force: true });
741 }
742 });
743 }),
744 );
745});
746
747describe("snapShotShortcutRegistrationFailureMessage", () => {
748 it("distinguishes a modifier listener failure from a reserved key chord", () => {
749 expect(
750 snapShotShortcutRegistrationFailureMessage({ kind: "both-shift-keys" }, "darwin"),
751 ).toMatch(/Shift \+ Shift is not available/);
752 expect(
753 snapShotShortcutRegistrationFailureMessage(
754 { kind: "modifier-pair", modifier: "meta" },
755 "darwin",
756 ),
757 ).toMatch(/Command \+ Command is not available/);
758 expect(
759 snapShotShortcutRegistrationFailureMessage(
760 { kind: "modifier-pair", modifier: "meta" },
761 "linux",
762 ),
763 ).toMatch(/Super \+ Super is not available/);
764 expect(
765 snapShotShortcutRegistrationFailureMessage(
766 {
767 key: "2",
768 metaKey: false,
769 ctrlKey: false,
770 shiftKey: true,
771 altKey: false,
772 modKey: true,
773 },
774 "darwin",
775 ),
776 ).toMatch(/already used/);
777 });
778});
779
780describe("snapShotShortcutSystemConflict", () => {
781 it("blocks shortcuts that would break typing or common app actions", () => {
782 expect(
783 snapShotShortcutSystemConflict({
784 key: "s",
785 metaKey: false,
786 ctrlKey: false,
787 shiftKey: true,
788 altKey: false,
789 modKey: false,
790 }),
791 ).toMatch(/typing/);
792 expect(
793 snapShotShortcutSystemConflict({
794 key: "c",
795 metaKey: false,
796 ctrlKey: false,
797 shiftKey: false,
798 altKey: false,
799 modKey: true,
800 }),
801 ).toMatch(/Copy/);
802 });
803
804 it("allows a specific multi-modifier shortcut", () => {
805 expect(
806 snapShotShortcutSystemConflict({
807 key: "2",
808 metaKey: false,
809 ctrlKey: false,
810 shiftKey: true,
811 altKey: false,
812 modKey: true,
813 }),
814 ).toBeNull();
815 });
816});