| ... | @@ -67,6 +67,70 @@ describe("isPreviewRefreshShortcut", () => { | ... | @@ -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 | describe("previewWindowOpenAction", () => { | 134 | describe("previewWindowOpenAction", () => { |
| 71 | const details = (overrides: { | 135 | const details = (overrides: { |
| 72 | readonly url?: string; | 136 | readonly url?: string; |
| ... | @@ -484,6 +548,7 @@ describe("PreviewManager", () => { | ... | @@ -484,6 +548,7 @@ describe("PreviewManager", () => { |
| 484 | const hostWebContents = { sendInputEvent }; | 548 | const hostWebContents = { sendInputEvent }; |
| 485 | Object.assign(preview.webContents, { hostWebContents }); | 549 | Object.assign(preview.webContents, { hostWebContents }); |
| 486 | fromId.mockReturnValue(preview.webContents); | 550 | fromId.mockReturnValue(preview.webContents); |
| | 551 | getFocusedWebContents.mockReturnValue(preview.webContents as never); |
| 487 | yield* manager.setMainWindow({ | 552 | yield* manager.setMainWindow({ |
| 488 | isDestroyed: () => false, | 553 | isDestroyed: () => false, |
| 489 | once: vi.fn(), | 554 | once: vi.fn(), |
| ... | @@ -497,7 +562,7 @@ describe("PreviewManager", () => { | ... | @@ -497,7 +562,7 @@ describe("PreviewManager", () => { |
| 497 | ).toHaveBeenCalledWith(true); | 562 | ).toHaveBeenCalledWith(true); |
| 498 | const beforeInput = preview.listeners.get("before-input-event")!; | 563 | const beforeInput = preview.listeners.get("before-input-event")!; |
| 499 | for (const control of [false, true]) { | 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 | for (const type of ["keyDown", "keyUp"]) { | 566 | for (const type of ["keyDown", "keyUp"]) { |
| 502 | const preventDefault = vi.fn(); | 567 | const preventDefault = vi.fn(); |
| 503 | beforeInput( | 568 | beforeInput( |
| ... | @@ -506,6 +571,9 @@ describe("PreviewManager", () => { | ... | @@ -506,6 +571,9 @@ describe("PreviewManager", () => { |
| 506 | ); | 571 | ); |
| 507 | yield* Effect.yieldNow; | 572 | yield* Effect.yieldNow; |
| 508 | expect(preventDefault).not.toHaveBeenCalled(); | 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,12 +595,55 @@ describe("PreviewManager", () => { |
| 527 | expect(preventDefault).toHaveBeenCalledOnce(); | 595 | expect(preventDefault).toHaveBeenCalledOnce(); |
| 528 | expect(preview.reload).toHaveBeenCalledOnce(); | 596 | expect(preview.reload).toHaveBeenCalledOnce(); |
| 529 | expect(sendInputEvent).not.toHaveBeenCalled(); | 597 | expect(sendInputEvent).not.toHaveBeenCalled(); |
| | 598 | }), |
| | 599 | ), |
| | 600 | ); |
| 530 | | 601 | |
| 531 | const setIgnoreMenuShortcuts = vi.fn(); | 602 | effectIt.effect("preserves focused browser editing in tabs and sign-in popups", () => |
| 532 | preview.listeners.get("did-create-window")!({ | 603 | withManager((manager) => |
| 533 | webContents: { setIgnoreMenuShortcuts, setWindowOpenHandler: vi.fn() }, | 604 | Effect.gen(function* () { |
| 534 | } as never); | 605 | const preview = makeFaviconWebContents(); |
| 535 | expect(setIgnoreMenuShortcuts).toHaveBeenCalledWith(true); | 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 | ); |