6683a7005d
* feat(web): add sidebar view system to mobile task switcher * test(web): scope mobile view-system gear indicator to the sheet * fix(web): disable view-chip drag-reorder on touch to keep row scrollable * fix(web): use mouse+touch sensors so chip drag works on hybrid devices * fix(web): match kanban PointerSensor+TouchSensor convention for chip dnd * fix(web): use MouseSensor+TouchSensor so touch swipe scrolls chip row * fix(web): wire group collapse + align/clean view bar in mobile sheet
144 lines
6.0 KiB
TypeScript
144 lines
6.0 KiB
TypeScript
/**
|
|
* Mobile parity for the sidebar view system (filters / sort / group + saved
|
|
* views).
|
|
*
|
|
* The desktop sidebar exposes a `SidebarFilterBar` (saved-view chip row + a
|
|
* filter gear that opens the shared `SidebarFilterPopover`). That same bar is
|
|
* now folded into the mobile task-switcher sheet so mobile users can switch and
|
|
* edit views instead of being stuck on whatever view was last picked on
|
|
* desktop. The sheet already applied the active view via `applyView`; these
|
|
* tests guard the newly-added switching/editing UI.
|
|
*
|
|
* Lives in `mobile-*.spec.ts` so the `mobile-chrome` Playwright project applies
|
|
* the mobile device automatically.
|
|
*/
|
|
import { test, expect, type SeedData } from "../../fixtures/test-base";
|
|
import type { Page, Locator } from "@playwright/test";
|
|
import type { ApiClient } from "../../helpers/api-client";
|
|
import { SessionPage } from "../../pages/session-page";
|
|
|
|
async function seedAndOpenSheet(
|
|
testPage: Page,
|
|
apiClient: ApiClient,
|
|
seedData: SeedData,
|
|
titles: string[],
|
|
): Promise<Locator> {
|
|
const stepOpts = {
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
};
|
|
for (const title of titles) {
|
|
await apiClient.seedTask(seedData.workspaceId, title, stepOpts);
|
|
}
|
|
// The nav task is seeded (not createTask) so it has a primary session and the
|
|
// mobile chat panel renders — `session.waitForLoad()` gates on session-chat.
|
|
const navTask = await apiClient.seedTask(seedData.workspaceId, "Mobile Views Nav", stepOpts);
|
|
await testPage.goto(`/t/${navTask.task_id}`);
|
|
const session = new SessionPage(testPage);
|
|
await session.waitForLoad();
|
|
|
|
// Open the task-switcher sheet from the mobile session top bar.
|
|
await testPage.getByTestId("mobile-session-menu").click();
|
|
const sheet = testPage.getByRole("dialog");
|
|
await expect(sheet.getByTestId("sidebar-filter-bar")).toBeVisible({ timeout: 10_000 });
|
|
return sheet;
|
|
}
|
|
|
|
/** Add a "Title contains <value>" filter clause via the (portaled) popover. */
|
|
async function addTitleFilter(testPage: Page, sheet: Locator, value: string): Promise<void> {
|
|
await sheet.getByTestId("sidebar-filter-gear").click();
|
|
const popover = testPage.getByTestId("sidebar-filter-popover");
|
|
await expect(popover).toBeVisible();
|
|
await popover.getByTestId("filter-add-button").click();
|
|
await popover.getByTestId("filter-dimension-select").click();
|
|
// Radix Select portals options to document.body (not under `popover`), so we
|
|
// can't scope to the popover here; `.first()` is the deliberate convention for
|
|
// this case (see apps/web/AGENTS.md). Only one select is open at a time.
|
|
await testPage.getByRole("option", { name: "Title", exact: false }).first().click();
|
|
await popover.getByTestId("filter-value-input").fill(value);
|
|
}
|
|
|
|
test.describe("Mobile sidebar — view system", () => {
|
|
test("editing filters in the mobile sheet narrows the task list live", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
const sheet = await seedAndOpenSheet(testPage, apiClient, seedData, [
|
|
"Fix auth bug",
|
|
"Update deps",
|
|
"Refactor auth",
|
|
]);
|
|
|
|
// All seeded tasks visible before filtering.
|
|
await expect(sheet.getByText("Fix auth bug")).toBeVisible({ timeout: 10_000 });
|
|
await expect(sheet.getByText("Update deps")).toBeVisible();
|
|
|
|
await addTitleFilter(testPage, sheet, "auth");
|
|
// Draft is active — the gear shows its unsaved indicator. Scope to the sheet:
|
|
// the globally-mounted (hidden on mobile) AppSidebar TasksViewPicker renders
|
|
// the same testid, so a page-level query is a strict-mode collision.
|
|
await expect(sheet.getByTestId("sidebar-filter-gear-indicator")).toBeVisible();
|
|
await testPage.keyboard.press("Escape");
|
|
|
|
// The list inside the sheet re-filters live via applyView.
|
|
await expect(sheet.getByText("Fix auth bug")).toBeVisible();
|
|
await expect(sheet.getByText("Refactor auth")).toBeVisible();
|
|
await expect(sheet.getByText("Update deps")).toHaveCount(0);
|
|
});
|
|
|
|
test("switching saved views swaps the filtered list in the sheet", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
const sheet = await seedAndOpenSheet(testPage, apiClient, seedData, [
|
|
"Fix auth bug",
|
|
"Update deps",
|
|
]);
|
|
|
|
// Build a saved "Auth View" that only keeps auth tasks.
|
|
await addTitleFilter(testPage, sheet, "auth");
|
|
const popover = testPage.getByTestId("sidebar-filter-popover");
|
|
await popover.getByTestId("view-save-as-button").click();
|
|
await popover.getByTestId("view-save-as-name-input").fill("Auth View");
|
|
await popover.getByTestId("view-save-as-confirm").click();
|
|
await testPage.keyboard.press("Escape");
|
|
|
|
const chipRow = sheet.getByTestId("sidebar-view-chip-row");
|
|
// Auth View is active; non-auth task hidden.
|
|
await expect(
|
|
chipRow.getByTestId("sidebar-view-chip").filter({ hasText: "Auth View" }),
|
|
).toHaveAttribute("data-active", "true");
|
|
await expect(sheet.getByText("Update deps")).toHaveCount(0);
|
|
|
|
// Switch back to the default "All tasks" chip — full list returns.
|
|
await chipRow.getByTestId("sidebar-view-chip").filter({ hasText: "All tasks" }).click();
|
|
await expect(sheet.getByText("Update deps")).toBeVisible();
|
|
await expect(sheet.getByText("Fix auth bug")).toBeVisible();
|
|
});
|
|
|
|
test("tapping a group header collapses and expands the group in the sheet", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
// The default "All tasks" view groups by repository, so the seeded tasks
|
|
// render under a collapsible group header in the sheet.
|
|
const sheet = await seedAndOpenSheet(testPage, apiClient, seedData, [
|
|
"Collapse Task A",
|
|
"Collapse Task B",
|
|
]);
|
|
|
|
const header = sheet.getByTestId("sidebar-group-header").first();
|
|
await expect(header).toBeVisible();
|
|
await expect(sheet.getByText("Collapse Task A")).toBeVisible();
|
|
|
|
// Collapse hides the group's tasks; expand brings them back.
|
|
await header.click();
|
|
await expect(sheet.getByText("Collapse Task A")).toHaveCount(0);
|
|
await header.click();
|
|
await expect(sheet.getByText("Collapse Task A")).toBeVisible();
|
|
});
|
|
});
|