Skip to content
Source
Sign inSign up

ComposerPermissionsSection.browser.test.tsx

173 lines7.5 KB

1import type { RuntimeMode } from "@t3tools/contracts";
2import { type ReactNode, useRef, useState } from "react";
3import { expect, it, vi } from "vite-plus/test";
4import { page, userEvent } from "vite-plus/test/browser";
5​
6import { render } from "../../../test/render";
7import { ComposerHandleContext, type ComposerHandleRef } from "../../composerHandleContext";
8import { ComposerContextPermissions } from "../ComposerContextPermissions";
9import { Menu, MenuPopup, MenuTrigger } from "../ui/menu";
10import {
11 focusRequestedPermission,
12 markOpenToPermissions,
13 useComposerMenuProps,
14} from "./composerEventScope";
15import { ComposerFooterModeControls, ComposerPermissionsSection } from "./ComposerFooterControls";
16import { runtimeModeConfig, runtimeModeOptions } from "./runtimeModeConfig";
17​
18const options = runtimeModeOptions.map((mode) => ({ mode, ...runtimeModeConfig[mode] }));
19​
20// Mirrors the overflow menu, which holds the Permissions section.
21function OverflowMenu() {
22 const [mode, setMode] = useState<RuntimeMode>("full-access");
23 const composerMenuProps = useComposerMenuProps();
24 const triggerRef = useRef<HTMLButtonElement>(null);
25 return (
26 <>
27 <Menu onOpenChangeComplete={(open) => focusRequestedPermission(open, triggerRef.current)}>
28 <MenuTrigger ref={triggerRef}>More</MenuTrigger>
29 <MenuPopup {...composerMenuProps}>
30 <ComposerPermissionsSection
31 runtimeMode={mode}
32 runtimeModeOptions={options}
33 onRuntimeModeChange={setMode}
34 />
35 </MenuPopup>
36 </Menu>
37 <p role="status">{mode}</p>
38 </>
39 );
41​
42it("reaches Permissions with arrow keys, previews, and picks with Enter", async () => {
43 await using screen = await render(<OverflowMenu />, { router: false }); // no routes involved
44 const trigger = screen.getByRole("button", { name: "More" });
45 await userEvent.tab();
46 await expect.element(trigger).toHaveFocus();
47 await userEvent.keyboard("{ArrowDown}");
48 const supervised = page.getByRole("menuitemradio", { name: "Supervised" });
49 await expect.element(supervised).toHaveFocus();
50 await expect
51 .element(supervised)
52 .toHaveAccessibleDescription("Ask before commands and file changes.");
53​
54 await userEvent.keyboard("{ArrowRight}");
55 await expect
56 .element(page.getByRole("menuitemradio", { name: "Auto-accept edits" }))
57 .toHaveFocus();
58 await userEvent.keyboard("{ArrowLeft}{ArrowLeft}");
59 await expect.element(page.getByRole("menuitemradio", { name: "Supervised" })).toHaveFocus();
60​
61 await userEvent.keyboard("{ArrowDown}{Enter}");
62 await expect.element(screen.getByRole("status")).toHaveTextContent("auto-accept-edits");
63 await expect
64 .element(page.getByRole("menuitemradio", { name: "Supervised" }))
65 .not.toBeInTheDocument();
66 await expect.element(trigger).toHaveFocus();
67});
68​
69it("marks the current mode as checked and closes on Escape", async () => {
70 await using screen = await render(<OverflowMenu />, { router: false }); // no routes involved
71 await userEvent.tab();
72 await userEvent.keyboard("{ArrowDown}");
73 const fullAccess = page.getByRole("menuitemradio", { name: "Full access" });
74 await expect.element(fullAccess).toHaveAttribute("aria-checked", "true");
75 await userEvent.keyboard("{Escape}");
76 await expect.element(fullAccess).not.toBeInTheDocument();
77 await expect.element(screen.getByRole("button", { name: "More" })).toHaveFocus();
78});
79​
80// The real composer menu focus rule (useComposerMenuProps), with a stub composer handle.
81function withComposer(children: ReactNode) {
82 const handle = { focusAtEnd: vi.fn() };
83 const ref = { current: handle } as unknown as ComposerHandleRef;
84 return { handle, ui: <ComposerHandleContext value={ref}>{children}</ComposerHandleContext> };
86​
87it("returns focus to the editor after a pick", async () => {
88 const { handle, ui } = withComposer(<OverflowMenu />);
89 await using screen = await render(ui, { router: false }); // no routes involved
90 await userEvent.tab();
91 await userEvent.keyboard("{ArrowDown}");
92 await expect.element(page.getByRole("menuitemradio", { name: "Supervised" })).toHaveFocus();
93 await userEvent.keyboard("{Enter}");
94 await expect.element(screen.getByRole("status")).toHaveTextContent("approval-required");
95 await vi.waitFor(() => expect(handle.focusAtEnd).toHaveBeenCalled());
96});
97​
98it("opens on the checked permission when composer.mode marked the trigger", async () => {
99 await using screen = await render(<OverflowMenu />, { router: false }); // no routes involved
100 const trigger = screen.getByRole("button", { name: "More" });
101 markOpenToPermissions(trigger.element() as HTMLElement);
102 await userEvent.click(trigger);
103 await expect.element(page.getByRole("menuitemradio", { name: "Full access" })).toHaveFocus();
104​
105 // The mark is used once: a plain reopen lands on the menu's usual first focus.
106 await userEvent.keyboard("{Escape}");
107 await userEvent.click(trigger);
108 await expect.element(page.getByRole("menuitemradio", { name: "Full access" })).not.toHaveFocus();
109});
110​
111// Mirrors ComposerControlsBar: the chip shows only while the mode differs from the default.
112function PermissionsChip() {
113 const [mode, setMode] = useState<RuntimeMode>("approval-required");
114 return (
115 <div data-slot="composer-shell">
116 <button type="button" data-composer-shortcut="composer.effort composer.mode">
117 Model
118 </button>
119 {mode === "full-access" ? null : (
120 <ComposerFooterModeControls
121 runtimeMode={mode}
122 runtimeModeOptions={options}
123 accessDisplay="label"
124 onRuntimeModeChange={setMode}
125 />
126 )}
127 </div>
128 );
130​
131it("returns focus to the editor when picking the default hides the chip", async () => {
132 const { handle, ui } = withComposer(<PermissionsChip />);
133 await using screen = await render(ui, { router: false }); // no routes involved
134 await userEvent.click(screen.getByRole("button", { name: "Permissions: Supervised" }));
135 await userEvent.click(page.getByRole("menuitemradio", { name: "Full access" }));
136 await vi.waitFor(() => expect(handle.focusAtEnd).toHaveBeenCalled());
137 await expect.element(screen.getByRole("button", { name: "Model" })).not.toHaveFocus();
138});
139​
140function ContextPermissions() {
141 const [mode, setMode] = useState<RuntimeMode>("full-access");
142 return (
143 <ComposerContextPermissions
144 environmentId={null}
145 runtimeMode={mode}
146 supportedRuntimeModes={["approval-required", "full-access"]}
147 onRuntimeModeChange={setMode}
148 />
149 );
151​
152it("opens permissions directly from the context strip and applies a supported mode", async () => {
153 await using screen = await render(<ContextPermissions />, { router: false }); // no routes involved
154 await userEvent.click(screen.getByRole("button", { name: "Permissions: Full access" }));
155 await expect
156 .element(page.getByRole("menuitemradio", { name: "Full access" }))
157 .toHaveAttribute("aria-checked", "true");
158 await expect
159 .element(page.getByRole("menuitemradio", { name: "Auto", exact: true }))
160 .not.toBeInTheDocument();
161 await userEvent.click(page.getByRole("menuitemradio", { name: "Supervised" }));
162 const trigger = screen.getByRole("button", { name: "Permissions: Supervised" });
163 await expect.element(trigger).toBeVisible();
164 await expect
165 .element(page.getByRole("menuitemradio", { name: "Supervised" }))
166 .not.toBeInTheDocument();
167 await userEvent.click(trigger);
168 await expect
169 .element(page.getByRole("menuitemradio", { name: "Supervised" }))
170 .toHaveAttribute("aria-checked", "true");
171 await userEvent.keyboard("{Escape}");
172 await expect.element(trigger).toHaveFocus();
173});