e2e/sidebar-interactions.spec.ts
Linked repository file from this build.
import { expect, test, type Page } from "@playwright/test";
async function setup(page: Page, layout: string, sidebar: string) {
await page.setViewportSize({ width: 1440, height: 900 });
await page.addInitScript(
(stored) => localStorage.setItem("qccrm.appearance", JSON.stringify(stored)),
{ layout, sidebar },
);
await page.route("**/api/inbox/events", (route) =>
route.fulfill({ json: { unreadCount: 3, events: [] } }),
);
await page.goto("/reviews");
await expect(page.locator("html")).toHaveAttribute("data-layout", layout);
}
for (const layout of ["workbench", "desk"]) {
test(`${layout}: icons stay collapsed on hover and focus; hamburger pins queues with navigation`, async ({
page,
}) => {
await setup(page, layout, "icons");
const nav = page.locator(".app-nav");
const reviewLink = nav.getByRole("link", { name: "Reviews", exact: true });
await expect(nav.getByRole("link", { name: "Home", exact: true })).toHaveCount(1);
await reviewLink.hover();
await expect(nav).toHaveCSS("width", "56px");
await reviewLink.focus();
await expect(nav).toHaveCSS("width", "56px");
await reviewLink.click();
await expect(nav).toHaveCSS("width", "56px");
const collapsedMenu = (await nav
.getByRole("button", { name: "Pin sidebar open", exact: true })
.boundingBox())!;
await nav.getByRole("button", { name: "Pin sidebar open", exact: true }).click();
const pin = nav.getByRole("button", { name: "Unpin sidebar", exact: true });
await expect(pin).toHaveAttribute("aria-pressed", "true");
await expect(nav.locator(".app-nav-label").first()).toBeVisible();
await expect(nav.getByText("IRIS", { exact: true })).toBeVisible();
await expect.poll(async () => (await nav.boundingBox())!.width).toBeGreaterThan(240);
await expect.poll(async () => (await nav.boundingBox())!.width).toBeLessThan(256);
const expandedMenu = (await pin.boundingBox())!;
expect(Math.abs(expandedMenu.x - collapsedMenu.x)).toBeLessThan(2);
expect(Math.abs(expandedMenu.y - collapsedMenu.y)).toBeLessThan(2);
const mainOpen = (await page.locator(".app-main").boundingBox())!.width;
if (layout === "desk") await expect(nav.locator(".app-queue")).toBeVisible();
await pin.click();
await page.getByRole("textbox", { name: "Search reviews" }).hover();
await expect(nav).toHaveCSS("width", "56px");
if (layout === "desk") {
await expect(nav.locator(".app-queue")).toBeHidden();
expect((await page.locator(".app-main").boundingBox())!.width).toBeGreaterThan(
mainOpen + 100,
);
}
});
test(`${layout}: hidden sidebar includes bottom controls and unread notifications open upward`, async ({
page,
}) => {
await setup(page, layout, "hidden");
const nav = page.locator(".app-nav");
await nav.getByRole("button", { name: "Pin sidebar open", exact: true }).hover();
await expect(nav.locator(".app-nav-label").first()).toBeVisible();
for (const name of ["Appearance", "Notifications, 3 unread", "Sign in"]) {
await nav.getByRole("button", { name, exact: true }).hover();
await expect(nav.locator(".app-nav-label").first()).toBeVisible();
expect((await nav.boundingBox())!.width).toBeGreaterThan(100);
}
const bell = nav.getByRole("button", { name: "Notifications, 3 unread", exact: true });
await bell.click();
const panel = nav.locator('.header-popover-panel[aria-label="Notifications, 3 unread"]');
await expect(panel).toBeVisible();
const box = (await panel.boundingBox())!;
const trigger = (await bell.boundingBox())!;
expect(box.y).toBeGreaterThanOrEqual(0);
expect(box.y + box.height).toBeLessThanOrEqual(trigger.y);
expect(box.x + box.width).toBeLessThanOrEqual(1440);
await panel.hover();
await expect(nav.locator(".app-nav-label").first()).toBeVisible();
await page.keyboard.press("Escape");
await page.getByRole("textbox", { name: "Search reviews" }).click();
await expect(nav.locator(".app-nav-label").first()).toBeHidden();
});
}
test("unpin hides the sidebar immediately while the pointer stays over it", async ({ page }) => {
await setup(page, "desk", "hidden");
const nav = page.locator(".app-nav");
await nav.getByRole("button", { name: "Pin sidebar open", exact: true }).click();
await expect(nav.getByRole("button", { name: "Unpin sidebar", exact: true })).toBeVisible();
await expect(nav.locator(".app-nav-label").first()).toBeVisible();
await nav.getByRole("button", { name: "Unpin sidebar", exact: true }).click();
await expect(nav).toHaveCSS("width", "56px");
await expect(nav.locator(".app-nav-label").first()).toBeHidden();
});
test("search remains centered in the viewport across breakpoints and pinned sidebars", async ({
page,
}) => {
await page
.context()
.addCookies([{ name: "qc-test-staff", value: "401", url: "http://localhost:3001" }]);
await setup(page, "classic", "icons");
for (const width of [1024, 1279, 1280, 1440, 1600, 1920, 2400]) {
await page.setViewportSize({ width, height: 900 });
const search = page.locator(".app-search");
await expect
.poll(async () => {
const box = (await search.boundingBox())!;
return Math.abs(box.x + box.width / 2 - width / 2);
})
.toBeLessThan(2);
const box = (await search.boundingBox())!;
expect(box.width).toBeGreaterThanOrEqual(300);
const nav = (await page.locator(".app-nav-content").boundingBox())!;
const tools = (await page.locator(".app-tools").boundingBox())!;
expect(box.x).toBeGreaterThanOrEqual(nav.x + nav.width);
expect(box.x + box.width).toBeLessThanOrEqual(tools.x);
}
for (const layout of ["Sidebar", "Queue"]) {
await page.getByRole("button", { name: "Appearance", exact: true }).click();
await page
.getByRole("radiogroup", { name: "Layout", exact: true })
.getByText(layout, { exact: true })
.click();
await page.keyboard.press("Escape");
await page.getByRole("button", { name: "Pin sidebar open", exact: true }).click();
for (const width of [1024, 1280, 1440, 1920]) {
await page.setViewportSize({ width, height: 900 });
const search = (await page.locator(".app-search").boundingBox())!;
expect(Math.abs(search.x + search.width / 2 - width / 2)).toBeLessThan(2);
expect(search.width).toBeGreaterThanOrEqual(300);
}
await page.getByRole("button", { name: "Unpin sidebar", exact: true }).click();
}
});
test("long notification lists stay inside a short viewport in icon mode", async ({ page }) => {
await setup(page, "workbench", "icons");
await page.route("**/api/inbox/events", (route) =>
route.fulfill({
json: {
unreadCount: 24,
events: Array.from({ length: 24 }, (_, i) => ({
id: `notification-${i}`,
kind: "distributed",
reviewNumber: "60001",
reviewKey: "aa1635c0-c99d-5f12-8f26-54d3f23bf13d",
fiscalYear: 2026,
createdAt: new Date().toISOString(),
readAt: null,
})),
},
}),
);
await page.setViewportSize({ width: 768, height: 480 });
await page.reload();
const nav = page.locator(".app-nav");
await nav.getByRole("button", { name: "Notifications, 24 unread", exact: true }).click();
const panel = nav.locator('.header-popover-panel[aria-label="Notifications, 24 unread"]');
await expect(panel).toBeVisible();
const box = (await panel.boundingBox())!;
expect(box.y).toBeGreaterThanOrEqual(0);
expect(box.y + box.height).toBeLessThanOrEqual(480);
expect(box.x + box.width).toBeLessThanOrEqual(768);
expect(await panel.evaluate((element) => element.scrollHeight > element.clientHeight)).toBe(true);
await expect(nav).toHaveCSS("width", "56px");
});