Files
kandev/apps/web/e2e/tests/plugins/mobile-plugin-task-panel.spec.ts
T
2026-09-23 11:00:23 +01:00

196 lines
7.9 KiB
TypeScript

// Routing: /t/{taskId}. File starts with "mobile-" so it runs on the
// mobile-chrome Playwright project (Pixel 5 emulation) — see
// mobile-file-viewer.spec.ts's header for the convention.
//
// E2E: a mobileEnabled task panel (registerTaskPanel) gets a phone
// bottom-nav entry, and selecting it renders the plugin's Component
// full-width in the mobile panel area (AC7). Uses the same real
// `plugin-fixture` package as plugin-task-panel.spec.ts.
import { expect, test } from "../../fixtures/test-base";
import { installFixturePlugin, PLUGIN_ID } from "../../helpers/plugin-fixture";
import { SessionPage } from "../../pages/session-page";
import { MobileKanbanPage } from "../../pages/mobile-kanban-page";
test.describe("Mobile plugin task panel", () => {
test.afterEach(async ({ apiClient }) => {
await apiClient.rawRequest("DELETE", `/api/plugins/${PLUGIN_ID}`).catch(() => undefined);
});
test("bottom-nav entry opens the plugin's Component full-width (AC7)", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(90_000);
// Install through the real Settings > Plugins upload flow (reachable on
// a phone via the settings menu sheet, same as mobile-plugin-nav.spec.ts).
await installFixturePlugin(testPage);
// A repo-backed task with an agent (not a bare createTask) so the task
// has a real session — the mobile bottom-nav panel switch this test
// exercises is a no-op without one (handlePanelChange in
// use-session-layout-state.ts guards on effectiveSessionId), same as
// every other mobile-nav e2e spec (see mobile-terminal-keybar.spec.ts).
const seedTask = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Mobile plugin panel task",
seedData.agentProfileId,
{
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
await testPage.goto(`/t/${seedTask.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
// Mobile groups all plugin panels behind one bounded bottom-nav action.
const panelsNavButton = testPage.getByRole("button", { name: "Panels", exact: true });
await expect(panelsNavButton).toBeVisible({ timeout: 15_000 });
expect((await panelsNavButton.boundingBox())?.height).toBeGreaterThanOrEqual(44);
await panelsNavButton.tap();
const notesOption = testPage.getByTestId("mobile-plugin-panel-option-kandev-plugin-e2e-notes");
await expect(notesOption).toBeVisible({ timeout: 10_000 });
expect((await notesOption.boundingBox())?.height).toBeGreaterThanOrEqual(44);
const notesEditor = testPage.getByTestId("e2e-notes-panel");
// On mobile the bottom-nav button can be tapped before hydration wires
// its handler; a lost tap leaves the panel unmounted (see
// mobile-terminal-helpers.ts's tapTerminalTab/switchToTerminalPanel for
// the same pattern). Re-tap once if the first tap didn't take.
await notesOption.tap();
if (!(await notesEditor.isVisible())) {
await panelsNavButton.tap();
await notesOption.tap();
}
await expect(notesEditor).toBeVisible({ timeout: 10_000 });
await expect(notesEditor).toHaveAttribute("data-presentation", "mobile");
// host.storage round-trips the same way it does on desktop.
await notesEditor.fill("hello from mobile e2e");
await expect
.poll(
async () => {
const res = await apiClient.rawRequest(
"GET",
`/api/plugins/${PLUGIN_ID}/user-state/task/${seedTask.id}/note`,
);
if (res.status !== 200) return null;
const body = (await res.json()) as { value: string };
return body.value;
},
{ timeout: 10_000, intervals: [250, 500, 1000] },
)
.toBe("hello from mobile e2e");
// Definitive disable while focused selects Chat and removes the grouped
// plugin action rather than leaving a dead panel selection behind.
await testPage.goto("/settings/plugins");
const pluginRow = testPage.getByTestId(`plugin-row-${PLUGIN_ID}`);
await pluginRow.getByRole("button", { name: "Disable" }).click();
await expect(pluginRow.getByText("Disabled", { exact: true })).toBeVisible({ timeout: 10_000 });
await testPage.goto(`/t/${seedTask.id}`);
await session.waitForLoad();
await expect(notesEditor).toHaveCount(0);
// Prompt history now shares the grouped Panels action for session
// tasks, so the button remains after the plugin is disabled; the
// disabled plugin's option must be gone from the picker and Chat must
// stay the active panel (no dead panel selection behind).
await expect(panelsNavButton).toBeVisible();
await panelsNavButton.tap();
await expect(
testPage.getByTestId("mobile-plugin-panel-option-kandev-plugin-e2e-notes"),
).toHaveCount(0);
await expect(testPage.getByTestId("mobile-prompt-history-option")).toBeVisible();
await testPage.keyboard.press("Escape");
await expect(testPage.getByRole("dialog", { name: "Panels" })).toHaveCount(0);
await expect(testPage.getByRole("button", { name: "Chat", exact: true })).toHaveClass(
/text-primary/,
);
expect(
await testPage.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth),
).toBe(true);
});
test("passes mobile presentation to the kanban plugin action", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(90_000);
await installFixturePlugin(testPage);
const task = await apiClient.createTask(seedData.workspaceId, "Mobile plugin menu 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" }).click();
const menu = testPage.locator('[data-slot="dropdown-menu-content"]:visible');
await menu.getByRole("menuitem", { name: "Edit", exact: true }).click();
await testPage.getByRole("menuitem", { name: "Enhance notes" }).click();
await expect
.poll(
async () => {
const res = await apiClient.rawRequest(
"GET",
`/api/plugins/${PLUGIN_ID}/user-state/task/${task.id}/menu-presentation`,
);
if (res.status !== 200) return null;
const body = (await res.json()) as { value: string };
return body.value;
},
{ timeout: 10_000, intervals: [250, 500, 1000] },
)
.toBe("mobile");
});
test("opens and selects a primary plugin submenu by touch", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(90_000);
await installFixturePlugin(testPage);
const task = await apiClient.createTask(seedData.workspaceId, "Mobile plugin submenu 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"]:visible');
const submenu = menu.getByRole("menuitem", { name: "Task shortcuts", exact: true });
await expect(submenu).toBeVisible();
await submenu.tap();
const child = testPage.getByRole("menuitem", { name: "Record menu presentation", exact: true });
await expect(child).toBeVisible();
await child.tap();
await expect
.poll(
async () => {
const res = await apiClient.rawRequest(
"GET",
`/api/plugins/${PLUGIN_ID}/user-state/task/${task.id}/primary-submenu-presentation`,
);
if (res.status !== 200) return null;
const body = (await res.json()) as { value: string };
return body.value;
},
{ timeout: 10_000, intervals: [250, 500, 1000] },
)
.toBe("mobile");
});
});