| ... | ... | @@ -67,6 +67,70 @@ describe("isPreviewRefreshShortcut", () => { |
| 67 | 67 | }); |
| 68 | 68 | }); |
| 69 | 69 | |
| 70 | describe("isPreviewEditingShortcut", () => { |
| 71 | const input = (platform: NodeJS.Platform, key: string, overrides: Partial<Electron.Input> = {}) => |
| 72 | ({ |
| 73 | type: "keyDown", |
| 74 | key, |
| 75 | meta: platform === "darwin", |
| 76 | control: platform !== "darwin", |
| 77 | shift: false, |
| 78 | alt: false, |
| 79 | ...overrides, |
| 80 | }) as Electron.Input; |
| 81 | |
| 82 | it.each(["darwin", "linux", "win32"] as const)( |
| 83 | "allows native editing chords on %s without allowing host shortcuts", |
| 84 | (platform) => { |
| 85 | for (const key of ["a", "c", "v", "x", "z", "V"]) { |
| 86 | expect(PreviewManager.isPreviewEditingShortcut(input(platform, key), platform)).toBe(true); |
| 87 | } |
| 88 | const redo = |
| 89 | platform === "win32" ? input(platform, "y") : input(platform, "z", { shift: true }); |
| 90 | expect(PreviewManager.isPreviewEditingShortcut(redo, platform)).toBe(true); |
| 91 | expect( |
| 92 | PreviewManager.isPreviewEditingShortcut( |
| 93 | input(platform, "v", { shift: true, alt: platform === "darwin" }), |
| 94 | platform, |
| 95 | ), |
| 96 | ).toBe(true); |
| 97 | |
| 98 | for (const key of ["k", ",", "w", "j", "q", "+", "=", "-", "0", "r", "F12"]) { |
| 99 | expect(PreviewManager.isPreviewEditingShortcut(input(platform, key), platform)).toBe(false); |
| 100 | } |
| 101 | for (const modifiers of [ |
| 102 | { meta: false, control: false }, |
| 103 | { meta: true, control: true }, |
| 104 | { meta: platform !== "darwin", control: platform === "darwin" }, |
| 105 | { alt: true }, |
| 106 | { shift: true, alt: platform !== "darwin" }, |
| 107 | ]) { |
| 108 | expect( |
| 109 | PreviewManager.isPreviewEditingShortcut(input(platform, "v", modifiers), platform), |
| 110 | ).toBe(false); |
| 111 | } |
| 112 | expect( |
| 113 | PreviewManager.isPreviewEditingShortcut(input(platform, "a", { shift: true }), platform), |
| 114 | ).toBe(false); |
| 115 | }, |
| 116 | ); |
| 117 | |
| 118 | it("recognizes macOS Paste and Match Style when Option changes the key to a symbol", () => { |
| 119 | const pasteAndMatchStyle = input("darwin", "◊", { code: "KeyV", alt: true, shift: true }); |
| 120 | expect(PreviewManager.isPreviewEditingShortcut(pasteAndMatchStyle, "darwin")).toBe(true); |
| 121 | for (const modifiers of [ |
| 122 | { code: "KeyC" }, |
| 123 | { alt: false }, |
| 124 | { shift: false }, |
| 125 | { control: true }, |
| 126 | ]) { |
| 127 | expect( |
| 128 | PreviewManager.isPreviewEditingShortcut({ ...pasteAndMatchStyle, ...modifiers }, "darwin"), |
| 129 | ).toBe(false); |
| 130 | } |
| 131 | }); |
| 132 | }); |
| 133 | |
| 70 | 134 | describe("previewWindowOpenAction", () => { |
| 71 | 135 | const details = (overrides: { |
| 72 | 136 | readonly url?: string; |
| ... | ... | @@ -484,6 +548,7 @@ describe("PreviewManager", () => { |
| 484 | 548 | const hostWebContents = { sendInputEvent }; |
| 485 | 549 | Object.assign(preview.webContents, { hostWebContents }); |
| 486 | 550 | fromId.mockReturnValue(preview.webContents); |
| 551 | getFocusedWebContents.mockReturnValue(preview.webContents as never); |
| 487 | 552 | yield* manager.setMainWindow({ |
| 488 | 553 | isDestroyed: () => false, |
| 489 | 554 | once: vi.fn(), |
| ... | ... | @@ -497,7 +562,7 @@ describe("PreviewManager", () => { |
| 497 | 562 | ).toHaveBeenCalledWith(true); |
| 498 | 563 | const beforeInput = preview.listeners.get("before-input-event")!; |
| 499 | 564 | for (const control of [false, true]) { |
| 500 | | for (const key of ["k", ",", "w", "j", "q", "+", "a", "c", "v", "x"]) { |
| 565 | for (const key of ["k", ",", "w", "j", "q", "+"]) { |
| 501 | 566 | for (const type of ["keyDown", "keyUp"]) { |
| 502 | 567 | const preventDefault = vi.fn(); |
| 503 | 568 | beforeInput( |
| ... | ... | @@ -506,6 +571,9 @@ describe("PreviewManager", () => { |
| 506 | 571 | ); |
| 507 | 572 | yield* Effect.yieldNow; |
| 508 | 573 | expect(preventDefault).not.toHaveBeenCalled(); |
| 574 | expect( |
| 575 | (preview.webContents as Electron.WebContents).setIgnoreMenuShortcuts, |
| 576 | ).toHaveBeenLastCalledWith(true); |
| 509 | 577 | } |
| 510 | 578 | } |
| 511 | 579 | } |
| ... | ... | @@ -527,12 +595,55 @@ describe("PreviewManager", () => { |
| 527 | 595 | expect(preventDefault).toHaveBeenCalledOnce(); |
| 528 | 596 | expect(preview.reload).toHaveBeenCalledOnce(); |
| 529 | 597 | expect(sendInputEvent).not.toHaveBeenCalled(); |
| 598 | }), |
| 599 | ), |
| 600 | ); |
| 530 | 601 | |
| 531 | | const setIgnoreMenuShortcuts = vi.fn(); |
| 532 | | preview.listeners.get("did-create-window")!({ |
| 533 | | webContents: { setIgnoreMenuShortcuts, setWindowOpenHandler: vi.fn() }, |
| 534 | | } as never); |
| 535 | | expect(setIgnoreMenuShortcuts).toHaveBeenCalledWith(true); |
| 602 | effectIt.effect("preserves focused browser editing in tabs and sign-in popups", () => |
| 603 | withManager((manager) => |
| 604 | Effect.gen(function* () { |
| 605 | const preview = makeFaviconWebContents(); |
| 606 | fromId.mockReturnValue(preview.webContents); |
| 607 | yield* manager.createTab("tab_editing"); |
| 608 | yield* manager.registerWebview("tab_editing", 42); |
| 609 | |
| 610 | const popup = makeFaviconWebContents({ id: 43 }); |
| 611 | preview.listeners.get("did-create-window")!({ webContents: popup.webContents } as never); |
| 612 | expect( |
| 613 | (popup.webContents as Electron.WebContents).setIgnoreMenuShortcuts, |
| 614 | ).toHaveBeenCalledWith(true); |
| 615 | |
| 616 | for (const browser of [preview, popup]) { |
| 617 | const contents = browser.webContents as Electron.WebContents; |
| 618 | getFocusedWebContents.mockReturnValue(browser.webContents as never); |
| 619 | const beforeInput = browser.listeners.get("before-input-event")!; |
| 620 | const preventDefault = vi.fn(); |
| 621 | const input = { |
| 622 | type: "keyDown", |
| 623 | key: "v", |
| 624 | meta: true, |
| 625 | control: false, |
| 626 | shift: false, |
| 627 | alt: false, |
| 628 | }; |
| 629 | beforeInput({ preventDefault } as never, input as never); |
| 630 | expect(contents.setIgnoreMenuShortcuts).toHaveBeenLastCalledWith(false); |
| 631 | // Releasing Command must not disable native fallback for the pending paste. |
| 632 | beforeInput( |
| 633 | { preventDefault } as never, |
| 634 | { ...input, type: "keyUp", key: "Meta", meta: false } as never, |
| 635 | ); |
| 636 | expect(contents.setIgnoreMenuShortcuts).toHaveBeenLastCalledWith(false); |
| 637 | |
| 638 | beforeInput({ preventDefault } as never, { ...input, key: "w" } as never); |
| 639 | expect(contents.setIgnoreMenuShortcuts).toHaveBeenLastCalledWith(true); |
| 640 | |
| 641 | // An injected paste in an unfocused guest cannot edit the active renderer. |
| 642 | getFocusedWebContents.mockReturnValue(null); |
| 643 | beforeInput({ preventDefault } as never, input as never); |
| 644 | expect(contents.setIgnoreMenuShortcuts).toHaveBeenLastCalledWith(true); |
| 645 | expect(preventDefault).not.toHaveBeenCalled(); |
| 646 | } |
| 536 | 647 | }), |
| 537 | 648 | ), |
| 538 | 649 | ); |