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 8bce4e77a..9d2192627 100644 --- a/tools/web-ux/browser-tests/console_history_shell_test.ts +++ b/tools/web-ux/browser-tests/console_history_shell_test.ts @@ -1,7 +1,7 @@ /// import { assert, assertEquals } from "@std/assert"; import { dirname, fromFileUrl, join } from "@std/path"; -import { chromium } from "playwright"; +import { chromium, type Page } from "playwright"; const repositoryRoot = join(dirname(fromFileUrl(import.meta.url)), "../../.."); const workspaceRoot = join(repositoryRoot, "web/workspace"); @@ -41,6 +41,54 @@ async function historyRequestCount(baseUrl: string): Promise { return state.history_requests; } +async function checkSidebarSlide(page: Page, mobile: boolean, opening: boolean): Promise { + const toggle = page.locator(mobile ? ".app-shell__mobile-sidebar-toggle" : ".sidebar-fold-button"); + const result = await toggle.evaluate(async (button, mobile) => { + const frame = document.querySelector(".sidebar-frame")!; + const content = frame.querySelector(".sidebar-frame-content")!; + const main = document.querySelector(".app-shell__main")!; + const moving = mobile ? frame : content; + const sample = () => ({ + x: moving.getBoundingClientRect().x, + width: moving.getBoundingClientRect().width, + frameWidth: frame.getBoundingClientRect().width, + mainWidth: main.getBoundingClientRect().width, + mainHeight: main.getBoundingClientRect().height, + }); + const before = sample(); + (button as HTMLButtonElement).click(); + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))); + const animations = frame.getAnimations({ subtree: true }); + const slides = animations.filter((animation) => animation instanceof CSSTransition && + ["width", "transform"].includes(animation.transitionProperty)); + const durations = slides.map((animation) => animation.effect!.getTiming().duration); + for (const animation of animations) { + animation.pause(); + animation.currentTime = 110; + } + const middle = sample(); + for (const animation of animations) animation.finish(); + await Promise.all(animations.map((animation) => animation.finished)); + return { before, middle, after: sample(), durations, contentInert: content.inert, mainInert: main.inert }; + }, mobile); + assert(result.durations.length >= (mobile ? 1 : 2)); + assert(result.durations.every((duration) => duration === 220)); + const { before, middle, after } = result; + assert(opening ? before.x < middle.x && middle.x < after.x : before.x > middle.x && middle.x > after.x, + `sidebar must slide horizontally through an intermediate position: ${JSON.stringify(result)}`); + assertEquals(before.width, after.width, "sidebar contents must slide without reflowing"); + assertEquals(result.contentInert, !opening); + assertEquals(result.mainInert, mobile && opening); + if (mobile) { + assertEquals(before.mainWidth, after.mainWidth); + assertEquals(before.mainHeight, after.mainHeight); + assertEquals(after.frameWidth, before.frameWidth); + } else { + assert(opening ? before.frameWidth < middle.frameWidth && middle.frameWidth < after.frameWidth + : before.frameWidth > middle.frameWidth && middle.frameWidth > after.frameWidth); + } +} + async function checkConsoleHistory(viewportHeight: number): Promise { const build = await new Deno.Command(Deno.execPath(), { args: ["task", "build"], @@ -336,17 +384,33 @@ async function checkConsoleHistory(viewportHeight: number): Promise { assertEquals(new Set(rowIds).size, rowIds.length); await page.getByRole("button", { name: "Overview", exact: true }).click(); assertEquals(await transcript.getByText("searched 1 time惻ran 1 command", { exact: true }).count(), 12); - await page.getByRole("button", { name: "Fold sidebar", exact: true }).click(); + await checkSidebarSlide(page, false, false); + await checkSidebarSlide(page, false, true); + await checkSidebarSlide(page, false, false); assertEquals(await page.evaluate(() => localStorage.getItem("yoi.sidebar.folded.v1")), "true"); await page.reload(); await page.getByRole("button", { name: "Unfold sidebar", exact: true }).waitFor(); assertEquals(await page.locator(".app-shell.sidebar-open").count(), 0); await page.setViewportSize({ width: 600, height: viewportHeight }); - await page.getByRole("button", { name: "Show sidebar", exact: true }).click(); + await page.locator(".sidebar-frame").evaluate(async (element) => { + await Promise.all(element.getAnimations({ subtree: true }).map((animation) => animation.finished)); + }); + await checkSidebarSlide(page, true, true); + await checkSidebarSlide(page, true, false); + await checkSidebarSlide(page, true, true); assertEquals(await page.evaluate(() => localStorage.getItem("yoi.sidebar.folded.v1")), "false"); await page.reload(); await page.getByRole("button", { name: "Hide sidebar", exact: true }).waitFor(); assertEquals(await page.locator(".app-shell.sidebar-open").count(), 1); + await page.emulateMedia({ reducedMotion: "reduce" }); + await page.getByRole("button", { name: "Hide sidebar", exact: true }).click(); + const reducedSidebar = await page.locator(".sidebar-frame").evaluate((element) => ({ + animations: element.getAnimations({ subtree: true }).length, + right: element.getBoundingClientRect().right, + visibility: getComputedStyle(element).visibility, + })); + assertEquals(reducedSidebar, { animations: 0, right: 0, visibility: "hidden" }); + assertEquals(await page.locator(".app-shell__main").evaluate((element) => (element as HTMLElement).inert), false); assertEquals(errors, []); await context.close(); } finally { diff --git a/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte b/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte index eba272855..6bfc9e323 100644 --- a/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte +++ b/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte @@ -18,11 +18,9 @@