diff --git a/tools/web-ux/browser-tests/console_history_shell_test.ts b/tools/web-ux/browser-tests/console_history_shell_test.ts index f83a5f125..909e8e693 100644 --- a/tools/web-ux/browser-tests/console_history_shell_test.ts +++ b/tools/web-ux/browser-tests/console_history_shell_test.ts @@ -596,6 +596,16 @@ async function checkConsoleHistory(viewportHeight: number): Promise { await touchPage.reload(); await touchPage.getByRole("button", { name: "Show sidebar", exact: true }).waitFor(); assertEquals(await touchPage.evaluate(() => localStorage.getItem("yoi.sidebar.mode.v1")), "pinned"); + // Different-route and same-page links both dismiss the mobile overlay. + for (let visit = 0; visit < 2; visit++) { + await touchPage.getByRole("button", { name: "Show sidebar", exact: true }).tap(); + await touchPage.getByRole("link", { name: "Workspace home", exact: true }).tap(); + await touchPage.waitForURL(`**/w/${workspaceId}`); + await touchPage.getByRole("button", { name: "Show sidebar", exact: true }).waitFor(); + assertEquals(await touchPage.locator(".app-shell__main").evaluate((element) => (element as HTMLElement).inert), false); + assertEquals(await touchPage.evaluate(() => localStorage.getItem("yoi.sidebar.mode.v1")), "pinned"); + } + await touchPage.goto(page.url()); // A wide touchscreen still has a temporary-open control in hover mode. await touchPage.setViewportSize({ width: 1024, height: 600 }); await touchPage.getByRole("button", { name: "Unpin sidebar", exact: true }).tap(); diff --git a/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte b/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte index 60dbd596e..760759d9e 100644 --- a/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte +++ b/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte @@ -1,5 +1,6 @@ diff --git a/web/workspace/src/lib/workspace/sidebar/sidebar-navigation.browser.spec.ts b/web/workspace/src/lib/workspace/sidebar/sidebar-navigation.browser.spec.ts new file mode 100644 index 000000000..db602e352 --- /dev/null +++ b/web/workspace/src/lib/workspace/sidebar/sidebar-navigation.browser.spec.ts @@ -0,0 +1,63 @@ +// @vitest-environment happy-dom + +import { cleanup, fireEvent, render } from "@testing-library/svelte"; +import { createRawSnippet } from "svelte"; +import { afterEach, beforeEach, expect, test, vi } from "vitest"; +import SidebarFrame from "./SidebarFrame.svelte"; + +// Stop happy-dom navigation after the component has handled the bubbling click. +const preventBrowserNavigation = (event: MouseEvent) => event.preventDefault(); +beforeEach(() => document.addEventListener("click", preventBrowserNavigation)); +afterEach(() => { + cleanup(); + document.removeEventListener("click", preventBrowserNavigation); +}); + +function mount(mobile = true, mode: "pinned" | "hover" = "pinned") { + const onOpenChange = vi.fn(); + const onModeChange = vi.fn(); + const children = createRawSnippet(() => ({ + render: () => ``, + })); + return { ...render(SidebarFrame, { + mobile, mode, open: true, children, onOpenChange, onModeChange, + }), onOpenChange, onModeChange }; +} + +test.each(["pinned", "hover"] as const)("mobile %s closes on nested, current-page and hash links", async (mode) => { + const view = mount(true, mode); + for (const label of ["Workspace home", "Current page", "Section"]) { + view.onOpenChange.mockClear(); + await fireEvent.click(view.getByText(label)); + expect(view.onOpenChange).toHaveBeenCalledExactlyOnceWith(false); + expect(view.onModeChange).not.toHaveBeenCalled(); + } +}); + +test("mobile ignores disclosure buttons, modified clicks, new tabs, downloads and cancelled navigation", async () => { + const view = mount(); + for (const label of ["Expand group", "New tab", "Download"]) { + await fireEvent.click(view.getByText(label)); + } + const link = view.getByText("Workspace home"); + for (const options of [{ ctrlKey: true }, { metaKey: true }, { shiftKey: true }, { altKey: true }, { button: 1 }]) { + await fireEvent.click(link, options); + } + link.addEventListener("click", (event) => event.preventDefault(), { once: true }); + await fireEvent.click(link); + expect(view.onOpenChange).not.toHaveBeenCalled(); +}); + +test.each(["pinned", "hover"] as const)("desktop %s navigation does not change sidebar mode or open state", async (mode) => { + const view = mount(false, mode); + await fireEvent.click(view.getByText("Workspace home")); + expect(view.onOpenChange).not.toHaveBeenCalled(); + expect(view.onModeChange).not.toHaveBeenCalled(); +});