f8708da1e0
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
196 lines
7.9 KiB
TypeScript
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");
|
|
});
|
|
});
|