feat: preview task canvases with workspace data (#3887)
This commit is contained in:
@@ -76,7 +76,7 @@ test.describe("authenticated same-origin canvas runtime", () => {
|
||||
true,
|
||||
);
|
||||
|
||||
await frame.getByTestId("canvas-fixture-continue").click();
|
||||
await frame.getByTestId("canvas-fixture-continue").dispatchEvent("click");
|
||||
await expect(frame.getByTestId("canvas-fixture-message-status")).toHaveText("accepted");
|
||||
await expect
|
||||
.poll(async () =>
|
||||
|
||||
@@ -20,6 +20,8 @@ export type CanvasRecord = {
|
||||
workspace_id: string;
|
||||
task_id?: string;
|
||||
scope_kind?: string;
|
||||
data_scope_kind?: string;
|
||||
grant_generation?: number;
|
||||
status?: string;
|
||||
active_release_id?: string;
|
||||
active_release_status?: string;
|
||||
@@ -47,6 +49,31 @@ export type SeededCanvas = {
|
||||
session: SessionPage;
|
||||
};
|
||||
|
||||
export async function seedCanvasWorkspacePreview(apiClient: ApiClient, seedData: SeedData) {
|
||||
const previewTask = await apiClient.seedTask(
|
||||
seedData.workspaceId,
|
||||
"E2E Canvas Workspace Preview Task",
|
||||
{ workflow_id: seedData.workflowId, workflow_step_id: seedData.startStepId },
|
||||
);
|
||||
const foreignWorkspaceName = "E2E Canvas Foreign Workspace";
|
||||
try {
|
||||
const foreignWorkspace = await apiClient.createWorkspace(foreignWorkspaceName);
|
||||
return {
|
||||
workspaceTaskId: previewTask.task_id,
|
||||
foreignWorkspaceId: foreignWorkspace.id,
|
||||
cleanup: async () => {
|
||||
await apiClient.deleteTask(previewTask.task_id).catch(() => undefined);
|
||||
await apiClient
|
||||
.deleteWorkspace(foreignWorkspace.id, foreignWorkspaceName)
|
||||
.catch(() => undefined);
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
await apiClient.deleteTask(previewTask.task_id).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function expectCanvasFrameFillsHost(page: Page): Promise<void> {
|
||||
await expect
|
||||
.poll(
|
||||
@@ -182,6 +209,7 @@ export async function waitForSessionWorkspace(
|
||||
export type CanvasSourceOptions = {
|
||||
noPermissions?: boolean;
|
||||
minimalPermissions?: boolean;
|
||||
foreignWorkspaceId?: string;
|
||||
};
|
||||
|
||||
function canvasCapabilities(options?: CanvasSourceOptions): string[] {
|
||||
@@ -230,10 +258,12 @@ function canvasManifest(canvas: CanvasRecord, options?: CanvasSourceOptions): st
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function canvasFixtureScript(canvas: CanvasRecord): string {
|
||||
function canvasFixtureScript(canvas: CanvasRecord, options?: CanvasSourceOptions): string {
|
||||
const taskID = JSON.stringify(canvas.task_id ?? "");
|
||||
const foreignWorkspaceID = JSON.stringify(options?.foreignWorkspaceId ?? "");
|
||||
return String.raw`(() => {
|
||||
const taskId = ${taskID};
|
||||
const foreignWorkspaceId = ${foreignWorkspaceID};
|
||||
const text = (testId, value) => {
|
||||
const element = document.querySelector('[data-testid="' + testId + '"]');
|
||||
if (element) element.textContent = String(value);
|
||||
@@ -260,6 +290,23 @@ function canvasFixtureScript(canvas: CanvasRecord): string {
|
||||
let streamReader;
|
||||
let streamCompletion = Promise.resolve();
|
||||
|
||||
const renderTasks = (tasks) => {
|
||||
const list = document.querySelector('[data-testid="canvas-fixture-task-list"]');
|
||||
if (!list) return;
|
||||
list.replaceChildren();
|
||||
tasks.forEach((candidate) => {
|
||||
const item = document.createElement("article");
|
||||
item.className = "task-item";
|
||||
const title = document.createElement("h2");
|
||||
title.textContent = candidate.title || "Workspace task";
|
||||
const status = document.createElement("span");
|
||||
status.className = "task-status";
|
||||
status.textContent = candidate.status || "Open";
|
||||
item.append(title, status);
|
||||
list.append(item);
|
||||
});
|
||||
};
|
||||
|
||||
const renderAppearance = () => {
|
||||
const root = document.documentElement;
|
||||
const styles = getComputedStyle(root);
|
||||
@@ -275,12 +322,22 @@ function canvasFixtureScript(canvas: CanvasRecord): string {
|
||||
});
|
||||
|
||||
const loadProjection = async () => {
|
||||
const [context, taskPage, workflowPage] = await Promise.all([
|
||||
const [context, tasks, workflowPage] = await Promise.all([
|
||||
api("./_kandev/v1/context"),
|
||||
api("./_kandev/v1/data/tasks?limit=10"),
|
||||
(async () => {
|
||||
const items = [];
|
||||
let cursor = "";
|
||||
do {
|
||||
const query = new URLSearchParams({ limit: "1" });
|
||||
if (cursor) query.set("cursor", cursor);
|
||||
const page = await api("./_kandev/v1/data/tasks?" + query.toString());
|
||||
items.push(...(page.items || []));
|
||||
cursor = page.page_info?.next_cursor || "";
|
||||
} while (cursor);
|
||||
return items;
|
||||
})(),
|
||||
api("./_kandev/v1/data/workflows?limit=10"),
|
||||
]);
|
||||
const tasks = taskPage.items || [];
|
||||
task = tasks.find((candidate) => candidate.id === taskId) || tasks[0];
|
||||
const workflow = (workflowPage.items || []).find(
|
||||
(candidate) => candidate.id === task?.workflow_id,
|
||||
@@ -293,8 +350,19 @@ function canvasFixtureScript(canvas: CanvasRecord): string {
|
||||
}
|
||||
text("canvas-fixture-context", context.task_id || "workspace");
|
||||
text("canvas-fixture-task-count", tasks.length);
|
||||
text("canvas-fixture-task-ids", tasks.map((candidate) => candidate.id).join(","));
|
||||
renderTasks(tasks);
|
||||
text("canvas-fixture-workflow-count", workflowPage.items?.length || 0);
|
||||
text("canvas-fixture-step-id", task?.workflow_step_id || "");
|
||||
text("canvas-fixture-refresh-status", "refreshed");
|
||||
if (foreignWorkspaceId) {
|
||||
try {
|
||||
await api("./_kandev/v1/data/tasks?workspace_id=" + encodeURIComponent(foreignWorkspaceId));
|
||||
text("canvas-fixture-foreign-workspace-status", "unexpected-allowed");
|
||||
} catch (error) {
|
||||
text("canvas-fixture-foreign-workspace-status", "denied:" + error.status);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const parseEvent = (block) => {
|
||||
@@ -425,6 +493,12 @@ function canvasFixtureScript(canvas: CanvasRecord): string {
|
||||
document.querySelector('[data-testid="canvas-fixture-resync"]')?.addEventListener("click", () => {
|
||||
void connectEvents("old-generation:1");
|
||||
});
|
||||
document.querySelector('[data-testid="canvas-fixture-refresh"]')?.addEventListener("click", () => {
|
||||
text("canvas-fixture-refresh-status", "refreshing");
|
||||
void loadProjection().catch((error) => {
|
||||
text("canvas-fixture-refresh-status", "error:" + error.status);
|
||||
});
|
||||
});
|
||||
|
||||
text("canvas-fixture-script", "inline-ready");
|
||||
renderAppearance();
|
||||
@@ -445,33 +519,39 @@ export function writeCanvasSource(
|
||||
[
|
||||
"<!doctype html>",
|
||||
'<html lang="en">',
|
||||
' <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>E2E Plugin Canvas</title><link rel="stylesheet" href="./styles.css"><script src="./appearance.js"></script></head>',
|
||||
' <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>E2E Plugin Canvas</title><script src="./appearance.js"></script>',
|
||||
" <style>",
|
||||
" :root { font-family: Inter, ui-sans-serif, system-ui, sans-serif; color: #172033; background: #f7f8fc; font-synthesis: none; }",
|
||||
" * { box-sizing: border-box; } body { margin: 0; padding: 32px; } .canvas-shell { max-width: 920px; margin: 0 auto; }",
|
||||
" .canvas-header { margin-bottom: 24px; } .eyebrow { margin: 0 0 8px; color: #6257df; font-size: 12px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; }",
|
||||
" h1 { margin: 0; font-size: clamp(24px, 4vw, 34px); line-height: 1.2; letter-spacing: -.03em; } .summary { margin: 8px 0 0; color: #687187; font-size: 14px; }",
|
||||
" .canvas-card { padding: 20px; border: 1px solid #e2e6ef; border-radius: 16px; background: white; box-shadow: 0 10px 32px #27345b0a; }",
|
||||
" .list-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; } .list-heading h2 { margin: 0; font-size: 15px; }",
|
||||
" .task-count { color: #687187; font-size: 12px; } .task-list { display: grid; gap: 10px; }",
|
||||
" .task-item { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 58px; padding: 12px 14px; border: 1px solid #e8ebf2; border-radius: 10px; background: #fff; }",
|
||||
" .task-item h2 { margin: 0; font-size: 14px; font-weight: 600; } .task-status { flex: none; padding: 4px 8px; border-radius: 999px; background: #f0efff; color: #5046c7; font-size: 11px; font-weight: 600; text-transform: capitalize; }",
|
||||
' .canvas-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; } button { min-height: 34px; padding: 7px 11px; border: 1px solid #dfe3ec; border-radius: 8px; background: white; color: #384158; font: inherit; font-size: 12px; cursor: pointer; -webkit-tap-highlight-color: transparent; } button:hover { background: #f6f7fb; } button[data-testid="canvas-fixture-refresh"] { border-color: #5548e8; background: #5548e8; color: white; }',
|
||||
" .diagnostics { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; clip-path: inset(50%); }",
|
||||
' @media (max-width: 600px) { body { padding: 20px 16px; } .canvas-card { padding: 16px; } .task-item { align-items: flex-start; flex-direction: column; gap: 8px; } .canvas-actions button:not([data-testid="canvas-fixture-refresh"]) { display: none; } }',
|
||||
" </style></head>",
|
||||
" <body>",
|
||||
' <main class="canvas-shell" data-testid="canvas-fixture-content">',
|
||||
' <header class="canvas-header"><p class="eyebrow" data-testid="canvas-fixture-eyebrow">E2E Plugin Canvas</p><h1>E2E Plugin Canvas</h1></header>',
|
||||
' <header class="canvas-header"><p class="eyebrow" data-testid="canvas-fixture-eyebrow">Workspace preview</p><h1>Your workspace tasks</h1><p class="summary">A live view of work across this workspace.</p></header>',
|
||||
' <section class="canvas-card">',
|
||||
' <p data-testid="canvas-fixture-script">loading</p>',
|
||||
' <p data-testid="canvas-fixture-context">loading</p>',
|
||||
' <p data-testid="canvas-fixture-task-count">0</p>',
|
||||
' <p data-testid="canvas-fixture-workflow-count">0</p>',
|
||||
' <p data-testid="canvas-fixture-step-id">loading</p>',
|
||||
' <p data-testid="canvas-fixture-message-status">idle</p>',
|
||||
' <p data-testid="canvas-fixture-move-status">idle</p>',
|
||||
' <p data-testid="canvas-fixture-state-status">idle</p>',
|
||||
' <p data-testid="canvas-fixture-sse-status">loading</p>',
|
||||
' <p data-testid="canvas-fixture-sse-events">0</p>',
|
||||
' <p data-testid="canvas-fixture-sse-resync">idle</p>',
|
||||
' <p data-testid="canvas-fixture-appearance-mode">loading</p>',
|
||||
' <p data-testid="canvas-fixture-appearance-background">loading</p>',
|
||||
' <p data-testid="canvas-fixture-appearance-color-scheme">loading</p>',
|
||||
' <button type="button" data-testid="canvas-fixture-continue">Continue</button>',
|
||||
' <button type="button" data-testid="canvas-fixture-move">Move workflow step</button>',
|
||||
' <button type="button" data-testid="canvas-fixture-state">Recover state</button>',
|
||||
' <button type="button" data-testid="canvas-fixture-reconnect">Reconnect events</button>',
|
||||
' <button type="button" data-testid="canvas-fixture-resync">Force resync</button>',
|
||||
' <div class="list-heading"><h2>All workspace tasks</h2><span class="task-count"><span data-testid="canvas-fixture-task-count">0</span> tasks</span></div>',
|
||||
' <div class="task-list" data-testid="canvas-fixture-task-list" aria-label="Workspace tasks"></div>',
|
||||
' <div class="canvas-actions"><button type="button" data-testid="canvas-fixture-refresh">Refresh list</button></div>',
|
||||
' <div class="diagnostics" aria-hidden="true">',
|
||||
' <p data-testid="canvas-fixture-script">loading</p><p data-testid="canvas-fixture-context">loading</p><p data-testid="canvas-fixture-task-ids"></p>',
|
||||
' <p data-testid="canvas-fixture-refresh-status">idle</p><p data-testid="canvas-fixture-foreign-workspace-status">not-tested</p><p data-testid="canvas-fixture-workflow-count">0</p><p data-testid="canvas-fixture-step-id">loading</p>',
|
||||
' <p data-testid="canvas-fixture-message-status">idle</p><p data-testid="canvas-fixture-move-status">idle</p><p data-testid="canvas-fixture-state-status">idle</p><p data-testid="canvas-fixture-sse-status">loading</p>',
|
||||
' <p data-testid="canvas-fixture-sse-events">0</p><p data-testid="canvas-fixture-sse-resync">idle</p><p data-testid="canvas-fixture-appearance-mode">loading</p><p data-testid="canvas-fixture-appearance-background">loading</p><p data-testid="canvas-fixture-appearance-color-scheme">loading</p>',
|
||||
' <button type="button" data-testid="canvas-fixture-continue">Continue</button><button type="button" data-testid="canvas-fixture-move">Move workflow step</button><button type="button" data-testid="canvas-fixture-state">Recover state</button>',
|
||||
' <button type="button" data-testid="canvas-fixture-reconnect">Reconnect events</button><button type="button" data-testid="canvas-fixture-resync">Force resync</button>',
|
||||
" </div>",
|
||||
" </section>",
|
||||
" </main>",
|
||||
` <script>${canvasFixtureScript(canvas)}</script>`,
|
||||
` <script>${canvasFixtureScript(canvas, options)}</script>`,
|
||||
' <script src="./script.js"></script>',
|
||||
" </body>",
|
||||
"</html>",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
publishTaskCanvas,
|
||||
removeCanvas,
|
||||
promoteCanvas,
|
||||
seedCanvasWorkspacePreview,
|
||||
seedTaskCanvas,
|
||||
waitForTaskCanvas,
|
||||
} from "./canvas-fixture";
|
||||
@@ -348,6 +349,117 @@ test.describe("Plugin-backed canvases on mobile", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("reviews workspace data for a legacy task canvas before enabling it", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
backend,
|
||||
seedData,
|
||||
}) => {
|
||||
test.setTimeout(150_000);
|
||||
|
||||
const releaseFeature = await enableCanvasFeature(backend, apiClient, seedData.workspaceId);
|
||||
const canvasIds: string[] = [];
|
||||
try {
|
||||
const seeded = await seedTaskCanvas(testPage, apiClient, seedData, true);
|
||||
canvasIds.push(seeded.canvas.id);
|
||||
const activeCanvas = await approvePendingCanvasThroughHost(
|
||||
testPage,
|
||||
apiClient,
|
||||
seeded.canvas,
|
||||
);
|
||||
const legacyCanvas = { ...activeCanvas, data_scope_kind: "task" };
|
||||
const permissionDigest = "legacy-task-canvas-review";
|
||||
const grantGeneration = activeCanvas.grant_generation ?? 0;
|
||||
let workspaceDataEnabled = false;
|
||||
let submittedReview: Record<string, unknown> | undefined;
|
||||
const canvasURL = new RegExp(`/api/v1/canvases/${activeCanvas.id}(?:\\?.*)?$`);
|
||||
|
||||
await testPage.route(canvasURL, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
...activeCanvas,
|
||||
data_scope_kind: workspaceDataEnabled ? "workspace" : "task",
|
||||
}),
|
||||
});
|
||||
});
|
||||
await testPage.route(
|
||||
new RegExp(`/api/v1/canvases/${activeCanvas.id}/workspace-data-preview(?:\\?.*)?$`),
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
canvas: legacyCanvas,
|
||||
active_release_id: activeCanvas.active_release_id,
|
||||
permission_digest: permissionDigest,
|
||||
grant_generation: grantGeneration,
|
||||
current_data_scope_kind: "task",
|
||||
target_data_scope_kind: "workspace",
|
||||
permissions: {
|
||||
reads: ["tasks"],
|
||||
writes: ["messages"],
|
||||
events: ["task.updated"],
|
||||
shared_state: false,
|
||||
external_origins: [],
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
await testPage.route(
|
||||
new RegExp(`/api/v1/canvases/${activeCanvas.id}/workspace-data(?:\\?.*)?$`),
|
||||
async (route) => {
|
||||
submittedReview = route.request().postDataJSON() as Record<string, unknown>;
|
||||
workspaceDataEnabled = true;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ ...legacyCanvas, data_scope_kind: "workspace" }),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
await testPage.goto(canvasHref(activeCanvas.id));
|
||||
await expect(testPage.getByTestId("canvas-data-scope")).toHaveText("Task data");
|
||||
await testPage.getByTestId("canvas-mobile-actions").tap();
|
||||
const actions = testPage.getByTestId("canvas-mobile-actions-sheet");
|
||||
await actions.getByTestId("canvas-action-enable-workspace-data").tap();
|
||||
|
||||
const review = testPage.getByTestId("canvas-workspace-data-dialog");
|
||||
await expect(review).toBeVisible();
|
||||
await expect(review.getByTestId("canvas-workspace-data-release")).toHaveText(
|
||||
activeCanvas.active_release_id ?? "",
|
||||
);
|
||||
await expect(review.getByTestId("canvas-workspace-data-current-scope")).toHaveText(
|
||||
"Task data",
|
||||
);
|
||||
await expect(review.getByTestId("canvas-workspace-data-target-scope")).toHaveText(
|
||||
"Workspace data",
|
||||
);
|
||||
await expect(review.getByText("Read task data")).toBeVisible();
|
||||
await review.getByRole("button", { name: "Enable workspace data", exact: true }).tap();
|
||||
|
||||
await expect(review).toHaveCount(0);
|
||||
await expect(testPage.getByTestId("canvas-data-scope")).toHaveText("Workspace data");
|
||||
expect(submittedReview).toEqual({
|
||||
expected_release_id: activeCanvas.active_release_id,
|
||||
expected_permission_digest: permissionDigest,
|
||||
expected_grant_generation: grantGeneration,
|
||||
});
|
||||
await testPage.getByTestId("canvas-mobile-actions").tap();
|
||||
await expect(
|
||||
testPage
|
||||
.getByTestId("canvas-mobile-actions-sheet")
|
||||
.getByTestId("canvas-action-enable-workspace-data"),
|
||||
).toHaveCount(0);
|
||||
} finally {
|
||||
await Promise.all(canvasIds.map((canvasId) => removeCanvas(apiClient, canvasId)));
|
||||
await releaseFeature();
|
||||
}
|
||||
});
|
||||
|
||||
test("uses a focused route, workspace navigation, and an inset action drawer", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
@@ -358,8 +470,12 @@ test.describe("Plugin-backed canvases on mobile", () => {
|
||||
|
||||
const releaseFeature = await enableCanvasFeature(backend, apiClient, seedData.workspaceId);
|
||||
const canvasIds: string[] = [];
|
||||
let workspacePreview: Awaited<ReturnType<typeof seedCanvasWorkspacePreview>> | undefined;
|
||||
try {
|
||||
const seeded = await seedTaskCanvas(testPage, apiClient, seedData, true);
|
||||
workspacePreview = await seedCanvasWorkspacePreview(apiClient, seedData);
|
||||
const seeded = await seedTaskCanvas(testPage, apiClient, seedData, true, {
|
||||
foreignWorkspaceId: workspacePreview.foreignWorkspaceId,
|
||||
});
|
||||
canvasIds.push(seeded.canvas.id);
|
||||
const activeCanvas = await approvePendingCanvasThroughHost(
|
||||
testPage,
|
||||
@@ -390,6 +506,21 @@ test.describe("Plugin-backed canvases on mobile", () => {
|
||||
);
|
||||
await expect(fixture.getByTestId("canvas-fixture-context")).toHaveText(seeded.taskId);
|
||||
await expect(fixture.getByTestId("canvas-fixture-sse-status")).toHaveText("connected");
|
||||
const expectedTaskCount = (await apiClient.listTasks(seedData.workspaceId)).tasks.length;
|
||||
expect(expectedTaskCount).toBeGreaterThanOrEqual(2);
|
||||
await fixture.getByTestId("canvas-fixture-refresh").tap();
|
||||
await expect(fixture.getByTestId("canvas-fixture-refresh-status")).toHaveText("refreshed");
|
||||
await expect(fixture.getByTestId("canvas-fixture-task-count")).toHaveText(
|
||||
String(expectedTaskCount),
|
||||
);
|
||||
await expect(fixture.locator(".task-item")).toHaveCount(expectedTaskCount);
|
||||
await expect(fixture.getByTestId("canvas-fixture-task-ids")).toContainText(seeded.taskId);
|
||||
await expect(fixture.getByTestId("canvas-fixture-task-ids")).toContainText(
|
||||
workspacePreview.workspaceTaskId,
|
||||
);
|
||||
await expect(fixture.getByTestId("canvas-fixture-foreign-workspace-status")).toHaveText(
|
||||
"denied:403",
|
||||
);
|
||||
|
||||
const actionsButton = testPage.getByTestId("canvas-mobile-actions");
|
||||
await expect(actionsButton).toBeVisible();
|
||||
@@ -409,7 +540,7 @@ test.describe("Plugin-backed canvases on mobile", () => {
|
||||
const promotionDialog = testPage.getByTestId("canvas-promotion-dialog");
|
||||
await expect(promotionDialog).toBeVisible();
|
||||
await expect(promotionDialog.getByTestId("canvas-promotion-target-scope")).toHaveText(
|
||||
"workspace",
|
||||
"Workspace",
|
||||
);
|
||||
await promotionDialog.getByRole("button", { name: "Confirm promotion", exact: true }).tap();
|
||||
|
||||
@@ -480,6 +611,7 @@ test.describe("Plugin-backed canvases on mobile", () => {
|
||||
).toBeVisible();
|
||||
} finally {
|
||||
await Promise.all(canvasIds.map((canvasId) => removeCanvas(apiClient, canvasId)));
|
||||
await workspacePreview?.cleanup();
|
||||
await releaseFeature();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
expectCanvasFrameFillsHost,
|
||||
listCanvasReleases,
|
||||
removeCanvas,
|
||||
seedCanvasWorkspacePreview,
|
||||
seedTaskCanvas,
|
||||
waitForSessionWorkspace,
|
||||
writeCanvasSource,
|
||||
@@ -150,8 +151,12 @@ test.describe("Plugin-backed canvases in the desktop task workbench", () => {
|
||||
|
||||
const releaseFeature = await enableCanvasFeature(backend, apiClient, seedData.workspaceId);
|
||||
let canvasId: string | undefined;
|
||||
let workspacePreview: Awaited<ReturnType<typeof seedCanvasWorkspacePreview>> | undefined;
|
||||
try {
|
||||
const seeded = await seedTaskCanvas(testPage, apiClient, seedData);
|
||||
workspacePreview = await seedCanvasWorkspacePreview(apiClient, seedData);
|
||||
const seeded = await seedTaskCanvas(testPage, apiClient, seedData, false, {
|
||||
foreignWorkspaceId: workspacePreview.foreignWorkspaceId,
|
||||
});
|
||||
canvasId = seeded.canvas.id;
|
||||
|
||||
await expect(testPage.getByTestId("dockview-task-layout")).toBeVisible();
|
||||
@@ -281,7 +286,21 @@ test.describe("Plugin-backed canvases in the desktop task workbench", () => {
|
||||
.getByTestId("canvas-fixture-appearance-background")
|
||||
.textContent();
|
||||
await expect(fixture.getByTestId("canvas-fixture-context")).toHaveText(seeded.taskId);
|
||||
await expect(fixture.getByTestId("canvas-fixture-task-count")).toHaveText("1");
|
||||
const expectedTaskCount = (await apiClient.listTasks(seedData.workspaceId)).tasks.length;
|
||||
expect(expectedTaskCount).toBeGreaterThanOrEqual(2);
|
||||
await fixture.getByTestId("canvas-fixture-refresh").click();
|
||||
await expect(fixture.getByTestId("canvas-fixture-refresh-status")).toHaveText("refreshed");
|
||||
await expect(fixture.getByTestId("canvas-fixture-task-count")).toHaveText(
|
||||
String(expectedTaskCount),
|
||||
);
|
||||
await expect(fixture.locator(".task-item")).toHaveCount(expectedTaskCount);
|
||||
await expect(fixture.getByTestId("canvas-fixture-task-ids")).toContainText(seeded.taskId);
|
||||
await expect(fixture.getByTestId("canvas-fixture-task-ids")).toContainText(
|
||||
workspacePreview.workspaceTaskId,
|
||||
);
|
||||
await expect(fixture.getByTestId("canvas-fixture-foreign-workspace-status")).toHaveText(
|
||||
"denied:403",
|
||||
);
|
||||
await expect(fixture.getByTestId("canvas-fixture-workflow-count")).toHaveText("1");
|
||||
await expect(fixture.getByTestId("canvas-fixture-step-id")).not.toHaveText("loading");
|
||||
await expect(fixture.getByTestId("canvas-fixture-sse-status")).toHaveText("connected");
|
||||
@@ -357,6 +376,7 @@ test.describe("Plugin-backed canvases in the desktop task workbench", () => {
|
||||
.not.toBe(lightBackground);
|
||||
} finally {
|
||||
if (canvasId) await removeCanvas(apiClient, canvasId);
|
||||
await workspacePreview?.cleanup();
|
||||
await releaseFeature();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
seedTaskCanvas,
|
||||
} from "./canvas-fixture";
|
||||
|
||||
test("task and workspace transition reads stay inside canvas scope", async ({
|
||||
test("workspace transition groups remain available before and after task canvas promotion", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
backend,
|
||||
@@ -45,13 +45,16 @@ test("task and workspace transition reads stay inside canvas scope", async ({
|
||||
item.to_workflow_step_id === seedData.startStepId,
|
||||
),
|
||||
).toBe(true);
|
||||
const taskScopeGroups = await body.evaluate(
|
||||
async (_, flowId) =>
|
||||
(await fetch(`./_kandev/v1/data/workflows/${encodeURIComponent(flowId)}/transition-groups`))
|
||||
.status,
|
||||
seedData.workflowId,
|
||||
const prePromotionGroups = await body.evaluate(async (_, flowId) => {
|
||||
const response = await fetch(
|
||||
`./_kandev/v1/data/workflows/${encodeURIComponent(flowId)}/transition-groups?limit=200`,
|
||||
);
|
||||
return { status: response.status, body: await response.json() };
|
||||
}, seedData.workflowId);
|
||||
expect(prePromotionGroups.status).toBe(200);
|
||||
expect(prePromotionGroups.body.items.some((item: { count: number }) => item.count > 0)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(taskScopeGroups).toBe(403);
|
||||
const workspace = await promoteCanvas(apiClient, approved);
|
||||
await testPage.goto(canvasHref(workspace.id));
|
||||
const groups = await testPage
|
||||
|
||||
Reference in New Issue
Block a user