| ... | ... | @@ -392,6 +392,23 @@ async function waitForElement<T extends Element>( |
| 392 | 392 | return element; |
| 393 | 393 | } |
| 394 | 394 | |
| 395 | async function waitForComposerEditor(): Promise<HTMLElement> { |
| 396 | return waitForElement( |
| 397 | () => document.querySelector<HTMLElement>('[contenteditable="true"]'), |
| 398 | "Unable to find composer editor.", |
| 399 | ); |
| 400 | } |
| 401 | |
| 402 | async function waitForInteractionModeButton(expectedLabel: "Chat" | "Plan"): Promise<HTMLButtonElement> { |
| 403 | return waitForElement( |
| 404 | () => |
| 405 | Array.from(document.querySelectorAll("button")).find( |
| 406 | (button) => button.textContent?.trim() === expectedLabel, |
| 407 | ) as HTMLButtonElement | null, |
| 408 | `Unable to find ${expectedLabel} interaction mode button.`, |
| 409 | ); |
| 410 | } |
| 411 | |
| 395 | 412 | async function waitForImagesToLoad(scope: ParentNode): Promise<void> { |
| 396 | 413 | const images = Array.from(scope.querySelectorAll("img")); |
| 397 | 414 | if (images.length === 0) { |
| ... | ... | @@ -785,4 +802,69 @@ describe("ChatView timeline estimator parity (full app)", () => { |
| 785 | 802 | await mounted.cleanup(); |
| 786 | 803 | } |
| 787 | 804 | }); |
| 805 | |
| 806 | it("toggles plan mode with Shift+Tab only while the composer is focused", async () => { |
| 807 | const mounted = await mountChatView({ |
| 808 | viewport: DEFAULT_VIEWPORT, |
| 809 | snapshot: createSnapshotForTargetUser({ |
| 810 | targetMessageId: "msg-user-target-hotkey" as MessageId, |
| 811 | targetText: "hotkey target", |
| 812 | }), |
| 813 | }); |
| 814 | |
| 815 | try { |
| 816 | const initialModeButton = await waitForInteractionModeButton("Chat"); |
| 817 | expect(initialModeButton.title).toContain("enter plan mode"); |
| 818 | |
| 819 | window.dispatchEvent( |
| 820 | new KeyboardEvent("keydown", { |
| 821 | key: "Tab", |
| 822 | shiftKey: true, |
| 823 | bubbles: true, |
| 824 | cancelable: true, |
| 825 | }), |
| 826 | ); |
| 827 | await waitForLayout(); |
| 828 | |
| 829 | expect((await waitForInteractionModeButton("Chat")).title).toContain("enter plan mode"); |
| 830 | |
| 831 | const composerEditor = await waitForComposerEditor(); |
| 832 | composerEditor.focus(); |
| 833 | composerEditor.dispatchEvent( |
| 834 | new KeyboardEvent("keydown", { |
| 835 | key: "Tab", |
| 836 | shiftKey: true, |
| 837 | bubbles: true, |
| 838 | cancelable: true, |
| 839 | }), |
| 840 | ); |
| 841 | |
| 842 | await vi.waitFor( |
| 843 | async () => { |
| 844 | expect((await waitForInteractionModeButton("Plan")).title).toContain( |
| 845 | "return to normal chat mode", |
| 846 | ); |
| 847 | }, |
| 848 | { timeout: 8_000, interval: 16 }, |
| 849 | ); |
| 850 | |
| 851 | composerEditor.dispatchEvent( |
| 852 | new KeyboardEvent("keydown", { |
| 853 | key: "Tab", |
| 854 | shiftKey: true, |
| 855 | bubbles: true, |
| 856 | cancelable: true, |
| 857 | }), |
| 858 | ); |
| 859 | |
| 860 | await vi.waitFor( |
| 861 | async () => { |
| 862 | expect((await waitForInteractionModeButton("Chat")).title).toContain("enter plan mode"); |
| 863 | }, |
| 864 | { timeout: 8_000, interval: 16 }, |
| 865 | ); |
| 866 | } finally { |
| 867 | await mounted.cleanup(); |
| 868 | } |
| 869 | }); |
| 788 | 870 | }); |