| ... | @@ -392,6 +392,23 @@ async function waitForElement<T extends Element>( | ... | @@ -392,6 +392,23 @@ async function waitForElement<T extends Element>( |
| 392 | return element; | 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 | async function waitForImagesToLoad(scope: ParentNode): Promise<void> { | 412 | async function waitForImagesToLoad(scope: ParentNode): Promise<void> { |
| 396 | const images = Array.from(scope.querySelectorAll("img")); | 413 | const images = Array.from(scope.querySelectorAll("img")); |
| 397 | if (images.length === 0) { | 414 | if (images.length === 0) { |
| ... | @@ -785,4 +802,69 @@ describe("ChatView timeline estimator parity (full app)", () => { | ... | @@ -785,4 +802,69 @@ describe("ChatView timeline estimator parity (full app)", () => { |
| 785 | await mounted.cleanup(); | 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 | }); |