feat: open canvas setup dialog directly (#3587)

This commit is contained in:
Carlos Florêncio
2026-09-13 16:59:54 +01:00
committed by GitHub
parent 96b2814bfb
commit 32b21c8b22
64 changed files with 3924 additions and 358 deletions
@@ -1,6 +1,7 @@
import { expect, test } from "../../fixtures/test-base";
import { waitForHttp } from "../../helpers/causal-waits";
import { SessionPage } from "../../pages/session-page";
import { expectTaskDescription, readTaskDescription } from "../../pages/task-description-editor";
import type { ApiClient } from "../../helpers/api-client";
import type { Page } from "@playwright/test";
import {
@@ -117,6 +118,26 @@ test.describe("Plugin-backed canvases on mobile", () => {
localProfile!.name,
);
const tasksBeforeCancel = await apiClient.listTasks(seedData.workspaceId);
await dialog.getByTestId("task-title-input").fill("Cancelled canvas task");
await dialog
.getByTestId("task-description-input")
.fill("This draft must not create a task or canvas.");
await dialog.getByRole("button", { name: "Cancel", exact: true }).tap();
await expect(dialog).toBeHidden();
const tasksAfterCancel = await apiClient.listTasks(seedData.workspaceId);
expect(tasksAfterCancel.tasks.map((task) => task.id)).toEqual(
tasksBeforeCancel.tasks.map((task) => task.id),
);
await testPage.getByTestId("settings-create-canvas").tap();
await expect(dialog).toBeVisible();
await expect(dialog.getByTestId("task-title-input")).toHaveValue("Create a canvas");
await expectTaskDescription(
dialog.getByTestId("task-description-input"),
"Create a new Kandev canvas with a coordinator view that lists the existing tasks.\n\n@create-canvas",
);
const agentSelector = dialog.getByTestId("agent-profile-selector");
await expect(agentSelector).toBeEnabled();
await agentSelector.tap();
@@ -137,14 +158,10 @@ test.describe("Plugin-backed canvases on mobile", () => {
// the option, so this remains deterministic under strict locators.
await testPage.getByRole("button", { name: "E2E Workflow", exact: true }).last().tap();
const defaultPrompt = await dialog.getByTestId("task-description-input").inputValue();
for (const tool of [
"create_canvas_kandev",
"read_canvas_authoring_skill_kandev",
"publish_canvas_kandev",
]) {
expect(defaultPrompt, `mobile preset is missing ${tool}`).toContain(tool);
}
const defaultPrompt = await readTaskDescription(dialog.getByTestId("task-description-input"));
expect(defaultPrompt).toBe(
"Create a new Kandev canvas with a coordinator view that lists the existing tasks.\n\n@create-canvas",
);
expect(defaultPrompt).not.toContain("e2e:mcp:");
await expect
.poll(() =>
@@ -160,12 +177,96 @@ test.describe("Plugin-backed canvases on mobile", () => {
summary: "Canvas created through the guided settings task flow.",
})})`,
'e2e:message("Canvas created from settings.")',
"Create a canvas that shows the current task list.",
...Array.from(
{ length: 20 },
(_, index) =>
`Canvas detail ${index + 1}: keep this longer edited goal inside the form scroll area.`,
),
"",
"@create-canvas",
].join("\n");
await dialog.getByTestId("task-title-input").fill(taskTitle);
await dialog.getByTestId("task-description-input").fill(description);
await dialog.getByTestId("task-create-advanced-settings-trigger").tap();
const formBody = dialog.getByTestId("task-create-form-body");
const formGeometry = await testPage.evaluate(() => {
const readRect = (testId: string) => {
const rect = document.querySelector(`[data-testid="${testId}"]`)?.getBoundingClientRect();
if (!rect) return null;
return {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
height: rect.height,
};
};
const viewport = window.visualViewport;
return {
viewport: {
left: viewport?.offsetLeft ?? 0,
top: viewport?.offsetTop ?? 0,
right: (viewport?.offsetLeft ?? 0) + (viewport?.width ?? window.innerWidth),
bottom: (viewport?.offsetTop ?? 0) + (viewport?.height ?? window.innerHeight),
},
dialog: readRect("create-task-dialog"),
body: readRect("task-create-form-body"),
footer: readRect("task-create-dialog-footer"),
cancel: readRect("submit-cancel"),
start: readRect("submit-start-agent"),
};
});
expect(formGeometry.dialog).not.toBeNull();
expect(formGeometry.body).not.toBeNull();
expect(formGeometry.footer).not.toBeNull();
expect(formGeometry.cancel?.height).toBeGreaterThanOrEqual(44);
expect(formGeometry.start?.height).toBeGreaterThanOrEqual(44);
expect(formGeometry.dialog!.left).toBeGreaterThanOrEqual(formGeometry.viewport.left - 1);
expect(formGeometry.dialog!.top).toBeGreaterThanOrEqual(formGeometry.viewport.top - 1);
expect(formGeometry.dialog!.right).toBeLessThanOrEqual(formGeometry.viewport.right + 1);
expect(formGeometry.dialog!.bottom).toBeLessThanOrEqual(formGeometry.viewport.bottom + 1);
expect(formGeometry.body!.bottom).toBeLessThanOrEqual(formGeometry.footer!.top + 1);
expect(formGeometry.footer!.bottom).toBeLessThanOrEqual(formGeometry.dialog!.bottom + 1);
const scrollMetrics = await formBody.evaluate((element) => ({
clientHeight: element.clientHeight,
scrollHeight: element.scrollHeight,
scrollTop: element.scrollTop,
}));
expect(scrollMetrics.scrollHeight).toBeGreaterThan(scrollMetrics.clientHeight);
await formBody.evaluate((element) => {
element.scrollTop = element.scrollHeight;
});
await expect
.poll(() => formBody.evaluate((element) => element.scrollTop))
.toBeGreaterThan(scrollMetrics.scrollTop);
let failNextTaskCreate = true;
await testPage.route("**/api/v1/tasks", async (route) => {
if (failNextTaskCreate && route.request().method() === "POST") {
failNextTaskCreate = false;
await route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({ error: "controlled canvas task creation failure" }),
});
return;
}
await route.continue();
});
const startAgent = dialog.getByTestId("submit-start-agent");
await expect(startAgent).toBeEnabled();
await startAgent.tap();
await expect(
testPage
.locator('[data-testid="toast-message"]')
.filter({ hasText: "Failed to create task" }),
).toBeVisible();
await expectTaskDescription(dialog.getByTestId("task-description-input"), description);
const responsePromise = waitForHttp(testPage, "POST", /\/api\/v1\/tasks$/);
await startAgent.tap();
const response = await responsePromise;
@@ -214,6 +315,7 @@ test.describe("Plugin-backed canvases on mobile", () => {
sessions.find((candidate) => candidate.id === taskSessionId)?.executor_profile_id,
).toBe(localProfile!.id);
} finally {
await testPage.unroute("**/api/v1/tasks").catch(() => undefined);
await Promise.all(canvasIds.map((canvasId) => removeCanvas(apiClient, canvasId)));
if (alternateWorkflowId)
await apiClient.deleteWorkflow(alternateWorkflowId).catch(() => undefined);