Files
kandev/apps/web/e2e/tests/layout/mobile-menu-backdrops.spec.ts
T

193 lines
8.6 KiB
TypeScript

import { test, expect } from "../../fixtures/test-base";
import { assertNoDocumentHorizontalOverflow } from "../../helpers/layout-assertions";
import {
expectMenuBackdropCount,
expectMobileMenuBackdrop,
expectReachableMenuItem,
} from "../../helpers/menu-backdrop";
import { MobileKanbanPage } from "../../pages/mobile-kanban-page";
import { SessionPage } from "../../pages/session-page";
test.describe("Mobile menu backdrops", () => {
// @covers AC-UI-MOBILE-TASK-NAVIGATION-001.11
test("the backdrop fades with the closing menu sheet", async ({
testPage,
apiClient,
seedData,
}) => {
const task = await apiClient.createTask(seedData.workspaceId, "Closing backdrop task", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
});
const mobile = new MobileKanbanPage(testPage);
await mobile.goto();
await mobile.taskCard(task.id).getByRole("button", { name: "More options" }).tap();
const menu = testPage.locator('[data-slot="dropdown-menu-content"]');
await expectMobileMenuBackdrop(menu);
const closeFrame = await menu.evaluateHandle((element) => {
const frame = { content: "", opacity: "", transition: "", duration: "" };
const observer = new MutationObserver(() => {
if (element.getAttribute("data-state") !== "closed") return;
const backdrop = getComputedStyle(element.parentElement!, "::before");
frame.content = backdrop.content;
frame.opacity = backdrop.opacity;
frame.transition = backdrop.transitionProperty;
frame.duration = backdrop.transitionDuration;
observer.disconnect();
});
observer.observe(element, { attributes: true, attributeFilter: ["data-state"] });
return frame;
});
await testPage.keyboard.press("Escape");
const closing = await closeFrame.jsonValue();
expect(["none", "normal"]).not.toContain(closing.content);
expect(Number.parseFloat(closing.opacity)).toBeGreaterThan(0);
expect(closing.transition).toBe("opacity");
expect(closing.duration).toBe("0.1s");
await closeFrame.dispose();
await expect(menu).toHaveCount(0);
await expectMenuBackdropCount(testPage, 0);
});
// @covers AC-UI-MOBILE-TASK-NAVIGATION-001.3, AC-UI-MOBILE-TASK-NAVIGATION-001.9, AC-UI-MOBILE-TASK-NAVIGATION-001.11
test("Kanban task options blur the background and dismiss cleanly", async ({
testPage,
apiClient,
seedData,
prCapture,
}) => {
const task = await apiClient.createTask(seedData.workspaceId, "Mobile backdrop task", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
});
const mobile = new MobileKanbanPage(testPage);
await mobile.goto();
const trigger = mobile.taskCard(task.id).getByRole("button", { name: "More options" });
const menu = testPage.locator('[data-slot="dropdown-menu-content"][data-state="open"]');
for (const colorScheme of ["light", "dark"] as const) {
await testPage.emulateMedia({ colorScheme });
await expect(testPage.locator("html")).toHaveClass(new RegExp(`\\b${colorScheme}\\b`));
await mobile.mobileMenuButton.tap();
const drawer = testPage.getByRole("dialog", { name: "Menu", exact: true });
await expect(drawer).toBeVisible();
const drawerBackdrop = await testPage
.locator('[data-slot="drawer-overlay"]:visible')
.evaluate((element) => {
const style = getComputedStyle(element);
return { background: style.backgroundColor, blur: style.backdropFilter };
});
await testPage.keyboard.press("Escape");
await expect(drawer).toBeHidden();
await trigger.tap();
const backdrop = await expectMobileMenuBackdrop(menu);
expect(backdrop.background).toBe(drawerBackdrop.background);
expect(backdrop.blur).toBe(drawerBackdrop.blur);
await expectMenuBackdropCount(testPage, 1);
await expectReachableMenuItem(menu.getByRole("menuitem", { name: "Edit", exact: true }));
await assertNoDocumentHorizontalOverflow(testPage);
await prCapture.screenshot(`kanban-menu-${colorScheme}`, {
caption: `Mobile task options with a blurred background in ${colorScheme} mode`,
});
if (colorScheme === "light") await testPage.touchscreen.tap(20, 20);
else await testPage.keyboard.press("Escape");
await expect(menu).toBeHidden();
await expectMenuBackdropCount(testPage, 0);
await expect(trigger).toBeFocused();
await expect(testPage).toHaveURL(/\/$/);
}
await trigger.tap();
await expectMenuBackdropCount(testPage, 1);
await apiClient.deleteTask(task.id);
await expect(mobile.taskCard(task.id)).toBeHidden();
await expectMenuBackdropCount(testPage, 0);
await mobile.mobileMenuButton.tap();
await expect(mobile.menuCard).toBeVisible();
});
// @covers AC-UI-MOBILE-TASK-NAVIGATION-001.3, AC-UI-MOBILE-TASK-NAVIGATION-001.10, AC-UI-MOBILE-TASK-NAVIGATION-001.11
test("task submenus share one backdrop and preserve the task drawer", async ({
testPage,
apiClient,
seedData,
prCapture,
}) => {
const task = await apiClient.createTask(seedData.workspaceId, "Nested backdrop task", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
});
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await testPage.getByTestId("mobile-task-picker-trigger").tap();
const drawer = testPage.getByRole("dialog", { name: "Tasks", exact: true });
const row = drawer.getByTestId("sidebar-task-item").filter({ hasText: task.title });
const actions = row.getByRole("button", { name: "Task actions" });
await actions.tap();
const rootMenu = testPage.locator('[data-slot="context-menu-content"][data-state="open"]');
await expectMobileMenuBackdrop(rootMenu);
await expectMenuBackdropCount(testPage, 1);
const priority = rootMenu.getByTestId("task-context-priority");
await priority.tap();
const low = testPage.getByTestId("task-context-priority-low");
await expectReachableMenuItem(low);
await expectMenuBackdropCount(testPage, 1);
await prCapture.screenshot("nested-task-menu", {
caption: "A task priority submenu retains one menu backdrop above the task drawer",
});
await low.press("ArrowLeft");
await expect(low).toBeHidden();
await expect(rootMenu).toBeVisible();
await expectMenuBackdropCount(testPage, 1);
await priority.tap();
await low.tap();
await expect.poll(async () => (await apiClient.getTask(task.id)).priority).toBe("low");
await expectMenuBackdropCount(testPage, 0);
await expect(drawer).toBeVisible();
await expect(testPage.locator('[data-slot="drawer-overlay"]:visible')).toHaveCount(1);
await assertNoDocumentHorizontalOverflow(testPage);
await actions.tap();
await expectMobileMenuBackdrop(rootMenu);
await priority.tap();
await expect(low).toBeVisible();
await testPage.keyboard.press("Escape");
await expectMenuBackdropCount(testPage, 0);
await expect(drawer).toBeVisible();
});
// @covers AC-UI-MOBILE-TASK-NAVIGATION-001.9, AC-UI-MOBILE-TASK-NAVIGATION-001.11, AC-UI-MOBILE-TASK-NAVIGATION-001.12
test("workspace menu preserves non-modal drawer interaction", async ({ testPage, apiClient }) => {
const otherWorkspace = await apiClient.createWorkspace("Backdrop workspace");
await apiClient.createWorkflow(otherWorkspace.id, "Backdrop workflow", "simple");
const mobile = new MobileKanbanPage(testPage);
await mobile.goto();
await mobile.mobileMenuButton.tap();
const drawer = testPage.getByRole("dialog", { name: "Menu", exact: true });
const trigger = testPage.getByTestId("mobile-workspace-trigger");
await trigger.tap();
const menu = testPage.locator('[data-slot="dropdown-menu-content"][data-state="open"]');
await expectMobileMenuBackdrop(menu);
await expectMenuBackdropCount(testPage, 1);
await drawer.getByRole("heading", { name: "Menu", exact: true }).tap();
await expect(menu).toBeHidden();
await expectMenuBackdropCount(testPage, 0);
await expect(drawer).toBeVisible();
await expect(testPage.locator('[data-slot="dropdown-menu-content"]')).toHaveCount(0);
await trigger.tap();
await expectMobileMenuBackdrop(menu);
const choice = testPage.getByTestId(`mobile-workspace-item-${otherWorkspace.id}`);
await expectReachableMenuItem(choice);
await choice.tap();
await expect(drawer).toBeHidden();
await expectMenuBackdropCount(testPage, 0);
await mobile.mobileMenuButton.tap();
await expect(trigger).toContainText(otherWorkspace.name);
await assertNoDocumentHorizontalOverflow(testPage);
});
});