Open History to see the last commit
1import { expect, it, vi } from "vite-plus/test";
2import { page, userEvent } from "vite-plus/test/browser";
3
4import { act, render } from "../../../test/render";
5import { SidebarProvider } from "../ui/sidebar";
6import { SettingsSidebarNav } from "./SettingsSidebarNav";
7import { SETTINGS_NAV_GROUPS, SETTINGS_SECTION_LABELS } from "./settingsSearch";
8
9vi.mock("./useAvailableSettingsSearchItems", () => ({
10 useAvailableSettingsSearchItems: () => [
11 { id: "theme", title: "Theme", to: "/settings/appearance" },
12 ],
13}));
14
15it("ignores composing keys in the search", async () => {
16 await using screen = await render(
17 <SidebarProvider defaultOpen>
18 <SettingsSidebarNav pathname="/settings/general" />
19 </SidebarProvider>,
20 { router: "/settings/general" },
21 );
22 const search = page.getByRole("combobox", { name: "Search settings" });
23 await search.fill("theme");
24 await expect.element(page.getByRole("option", { name: /Theme/ })).toBeVisible();
25
26 for (const composition of [
27 { key: "Enter", isComposing: true, keyCode: 13 },
28 { key: "Enter", isComposing: false, keyCode: 229 },
29 { key: "Escape", isComposing: true, keyCode: 27 },
30 { key: "Escape", isComposing: false, keyCode: 229 },
31 { key: "ArrowDown", isComposing: true, keyCode: 40 },
32 { key: "ArrowUp", isComposing: true, keyCode: 38 },
33 ]) {
34 const event = new KeyboardEvent("keydown", { ...composition, bubbles: true, cancelable: true });
35 await act(() => search.element().dispatchEvent(event));
36 expect(event.defaultPrevented, JSON.stringify(composition)).toBe(false);
37 expect(screen.router?.state.location.pathname).toBe("/settings/general");
38 await expect.element(search).toHaveValue("theme");
39 }
40
41 await userEvent.keyboard("{Enter}");
42 await vi.waitFor(() =>
43 expect(screen.router?.state.location.pathname).toBe("/settings/appearance"),
44 );
45});
46
47function renderNav() {
48 return render(
49 <SidebarProvider defaultOpen>
50 <SettingsSidebarNav pathname="/settings/general" />
51 </SidebarProvider>,
52 { router: "/settings/general" },
53 );
54}
55
56it("opens search from the physical Slash and F keys on non-US layouts", async () => {
57 await using _screen = await renderNav();
58 const search = page.getByRole("combobox", { name: "Search settings" });
59
60 for (const { init, opens } of [
61 { init: { key: "/", code: "Digit7", shiftKey: true }, opens: true }, // German Shift+7
62 { init: { key: ".", code: "Slash" }, opens: true }, // Russian Slash key
63 { init: { key: "а", code: "KeyF", ctrlKey: true }, opens: true }, // Russian Ctrl+F
64 { init: { key: "?", code: "Slash", shiftKey: true }, opens: false },
65 ]) {
66 if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
67 const event = new KeyboardEvent("keydown", { ...init, bubbles: true, cancelable: true });
68 await act(() => document.body.dispatchEvent(event));
69 expect(event.defaultPrevented, JSON.stringify(init)).toBe(opens);
70 if (opens) await expect.element(search).toHaveFocus();
71 }
72});
73
74it("moves Tab through every settings section", async () => {
75 await page.viewport(1280, 800);
76 await using _screen = await renderNav();
77 await userEvent.click(page.getByRole("combobox", { name: "Search settings" }));
78
79 const sections = SETTINGS_NAV_GROUPS.flatMap((group) => group.paths).map(
80 (to) => SETTINGS_SECTION_LABELS[to],
81 );
82 const tabbed: string[] = [];
83 for (let step = 0; step < sections.length; step += 1) {
84 await userEvent.tab();
85 const section = document.activeElement?.closest("[data-settings-section]");
86 if (!section) break;
87 tabbed.push(section.textContent ?? "");
88 }
89 expect(tabbed).toEqual(sections);
90});
91
92it.each([1280, 390])("names each group by a heading outside it at %ipx wide", async (width) => {
93 await page.viewport(width, 800);
94 await using _screen = await renderNav();
95
96 for (const name of ["Agents", "Code", "Connections"]) {
97 const heading = page.getByRole("heading", { name, level: 3 });
98 await expect.element(heading).toBeInTheDocument();
99 const group = page.getByRole("group", { name }).element();
100 expect(group.getAttribute("aria-labelledby")).toBe(heading.element().id);
101 expect(group.contains(heading.element()), `the ${name} heading is inside its group`).toBe(
102 false,
103 );
104 }
105 const groups = page.getByRole("group").elements();
106 expect(groups).toHaveLength(SETTINGS_NAV_GROUPS.length);
107 for (const group of groups) {
108 const first = group.querySelector("[data-settings-section]")?.textContent;
109 const heading = document.getElementById(group.getAttribute("aria-labelledby") ?? "");
110 expect(heading?.tagName, `the group starting at ${first} has no heading`).toBe("H3");
111 }
112 expect(page.getByRole("tree").query()).toBeNull();
113});