tests/e2e/control-workspace-panes.spec.ts
Rama main · réplica de lectura del repositorio original.
import { expect, test } from "@playwright/test";
import { mockControlWorkspace } from "./helpers/control-workspace-fixture";
test("task tools stay beside the conversation and preserve its draft", async ({
page,
}, testInfo) => {
await page.setViewportSize({ width: 1600, height: 1000 });
const { mutations } = await mockControlWorkspace(page);
const composer = page.getByRole("textbox", {
name: "Ask for follow-up changes",
});
await composer.fill("Keep this draft while I inspect the result");
await page.getByRole("button", { name: "Open preview", exact: true }).click();
const panel = page.getByRole("complementary", { name: "Task tools" });
await expect(
panel.getByTitle("Sandbox Preview", { exact: true })
).toHaveAttribute("src", "https://preview.example.test/one");
await expect(composer).toBeVisible();
await page.screenshot({ path: testInfo.outputPath("workspace-desktop.png") });
await expect(
page.getByText("The welcome page is ready for review.", { exact: true })
).toBeVisible();
const frame = await panel
.getByTitle("Sandbox Preview", { exact: true })
.elementHandle();
await page.getByRole("button", { name: "Close task tools" }).click();
await page.getByRole("button", { name: "Open preview", exact: true }).click();
expect(await frame!.evaluate((node) => node.isConnected)).toBe(true);
await page.getByRole("button", { name: "Open changes", exact: true }).click();
await expect(panel).toContainText("app/page.tsx");
await panel.getByText("app/page.tsx", { exact: true }).click();
await expect(panel).toContainText("Welcome home");
await expect(page.getByRole("dialog")).toHaveCount(0);
await page.getByRole("button", { name: "Open plan", exact: true }).click();
await expect(panel).toContainText("Update welcome copy");
await page.getByRole("button", { name: "Close task tools" }).click();
await expect(composer).toHaveValue(
"Keep this draft while I inspect the result"
);
await page
.getByRole("button", { name: "Open terminal", exact: true })
.click();
await expect(
page.getByRole("region", { name: "Task terminal" })
).toBeVisible();
await expect(page).toHaveURL(/\/control\?mission=task-one/);
await page.getByRole("tab", { name: "Agent commands", exact: true }).click();
await expect(
page.getByRole("region", { name: "Task terminal" })
).toContainText("12 checks passed");
await page.getByRole("button", { name: "Close terminal" }).click();
await expect(composer).toHaveValue(
"Keep this draft while I inspect the result"
);
expect(mutations).toEqual([]);
});
test("mobile tools use a full-width view and return to the same conversation", async ({
page,
}, testInfo) => {
await page.setViewportSize({ width: 390, height: 844 });
await mockControlWorkspace(page);
await expect(page.locator(".app-statusbar")).toContainText("Sandboxes");
await expect(page.locator(".app-statusbar")).not.toContainText(
"Open a workspace"
);
const composer = page.getByRole("textbox", {
name: "Ask for follow-up changes",
});
await composer.fill("Mobile draft");
expect((await page.getByRole("banner").boundingBox())!.height).toBeLessThan(
80
);
await page.getByRole("button", { name: "Open plan", exact: true }).click();
const panel = page.getByRole("complementary", { name: "Task tools" });
await expect(panel).toContainText("Update welcome copy");
await page.screenshot({ path: testInfo.outputPath("workspace-mobile.png") });
await expect(panel).toHaveCSS("transform", "none");
const box = await panel.boundingBox();
expect(box!.width).toBeGreaterThan(350);
expect(box!.x + box!.width).toBeLessThanOrEqual(390);
await page.getByRole("button", { name: "Close task tools" }).click();
await expect(composer).toBeVisible();
await expect(composer).toHaveValue("Mobile draft");
expect(
await page.evaluate(() => document.documentElement.scrollWidth)
).toBeLessThanOrEqual(390);
});
for (const state of ["paused", "stopped"] as const) {
test(`${state} task previews show recovery controls without loading a stopped provider page`, async ({
page,
}) => {
const { sandboxes } = await mockControlWorkspace(page);
const sandbox = sandboxes[0];
sandbox.status = state;
sandbox.health_status = state;
sandbox.runtime_summary = {
...sandbox.runtime_summary!,
status: state,
health_status: state,
persistent: true,
};
await page.route("**/api/sandbox/*/health", (route) =>
route.fulfill({
json: { status: state, healthStatus: state, sandbox },
})
);
let providerRequests = 0;
page.on("request", (request) => {
if (request.url().startsWith("https://preview.example.test/"))
providerRequests++;
});
await page.reload();
await expect(
page.getByRole("button", {
name: new RegExp(
`Selected sandbox acme/widgets, branch work/one, ${state === "paused" ? "Paused" : "Stopped"}`
),
})
).toBeVisible();
await page
.getByRole("button", { name: "Open preview", exact: true })
.click();
const panel = page.getByRole("complementary", { name: "Task tools" });
await expect(
panel.getByRole("button", {
name: state === "paused" ? "Resume preview" : "Restart on this branch",
exact: true,
})
).toBeVisible();
await expect(panel.locator("iframe")).toHaveCount(0);
expect(providerRequests).toBe(0);
});
}
for (const missingBinding of [false, true]) {
test(`preview requires a task environment when ${missingBinding ? "the saved sandbox is missing" : "multiple sandboxes are available"}`, async ({
page,
}) => {
const { sessions, mutations } = await mockControlWorkspace(page);
if (missingBinding)
sessions[1].messages = [
{
id: "old-execution",
role: "assistant",
parts: [
{
type: "dynamic-tool",
toolName: "run_command",
toolCallId: "old-command",
state: "output-available",
input: { command: "pwd" },
output: { sandboxId: "missing-sandbox", stdout: "/workspace" },
},
],
},
];
let providerRequests = 0;
page.on("request", (request) => {
if (request.url().startsWith("https://preview.example.test/"))
providerRequests++;
});
await page.getByRole("button", { name: /^Review billing flow/ }).click();
await page
.getByRole("button", { name: "Open preview", exact: true })
.click();
const panel = page.getByRole("complementary", { name: "Task tools" });
await expect(panel.locator("iframe")).toHaveCount(0);
await expect(panel).toContainText("Select a sandbox for this task");
await panel
.getByRole("button", { name: "Choose sandbox", exact: true })
.click();
await expect(panel).toContainText("work/one");
await expect(panel).toContainText("work/two");
expect(providerRequests).toBe(0);
expect(mutations).toEqual([]);
});
}
test("sandbox choices and drafts stay attached to their task across switches", async ({
page,
}) => {
await page.setViewportSize({ width: 1600, height: 1000 });
const { mutations } = await mockControlWorkspace(page);
const composer = page.getByRole("textbox", {
name: "Ask for follow-up changes",
});
await composer.fill("Welcome draft");
await page.getByRole("button", { name: "Open preview", exact: true }).click();
await page.getByRole("button", { name: /^Review billing flow/ }).click();
await expect(
page.getByRole("button", {
name: "Choose sandbox for this task",
exact: true,
})
).toBeVisible();
await composer.fill("Billing draft");
await page
.getByRole("button", { name: "Choose sandbox for this task", exact: true })
.click();
await page
.getByRole("menuitem", { name: /Select acme\/widgets, branch work\/two/ })
.click();
await page.getByRole("button", { name: "Open preview", exact: true }).click();
await expect(
page.getByTitle("Sandbox Preview", { exact: true })
).toHaveAttribute("src", "https://preview.example.test/two");
await page.getByRole("button", { name: /^Refresh welcome page/ }).click();
await expect(composer).toHaveValue("Welcome draft");
await expect(
page.getByTitle("Sandbox Preview", { exact: true })
).toHaveAttribute("src", "https://preview.example.test/one");
await page.getByRole("button", { name: /^Review billing flow/ }).click();
await expect(composer).toHaveValue("Billing draft");
await page.reload();
await expect(
page.getByRole("button", {
name: /Selected sandbox acme\/widgets, branch work\/two/,
})
).toBeVisible();
expect(mutations).toEqual([]);
});
test("files can be inspected without writing to the sandbox", async ({
page,
}) => {
const { mutations } = await mockControlWorkspace(page);
await page.getByRole("button", { name: "Open files", exact: true }).click();
const panel = page.getByRole("complementary", { name: "Task tools" });
await panel.getByRole("treeitem", { name: "page.tsx", exact: true }).click();
await expect(panel.locator(".monaco-editor")).toContainText("Welcome home");
await panel.getByRole("button", { name: "Back to files" }).click();
await expect(
panel.getByRole("treeitem", { name: "page.tsx", exact: true })
).toBeVisible();
expect(mutations).toEqual([]);
});
test("workspace tools remain usable at tablet width in light mode", async ({
page,
}, testInfo) => {
await page.setViewportSize({ width: 820, height: 1024 });
await mockControlWorkspace(page, "light");
await page.getByRole("button", { name: "Open preview", exact: true }).click();
await expect(page.locator("html")).toHaveClass(/light/);
const panel = page.getByRole("complementary", { name: "Task tools" });
await expect(
panel.getByTitle("Sandbox Preview", { exact: true })
).toHaveAttribute("src", "https://preview.example.test/one");
for (const tool of ["preview", "changes", "files", "plan", "terminal"]) {
await expect(
page.getByRole("button", { name: `Open ${tool}`, exact: true })
).toBeInViewport({ ratio: 1 });
}
const bounds = await panel.boundingBox();
expect(bounds!.width).toBeGreaterThan(400);
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(820);
await page.screenshot({
path: testInfo.outputPath("workspace-tablet-light.png"),
});
await page.getByRole("button", { name: "Close task tools" }).click();
await expect(
page.getByRole("button", { name: "Open preview", exact: true })
).toBeFocused();
await expect(
page.getByRole("textbox", { name: "Ask for follow-up changes" })
).toBeVisible();
});
test("failed change requests surface an error and can be retried", async ({
page,
}) => {
await mockControlWorkspace(page);
let fail = true;
await page.route("**/api/sandbox/sandbox-one/changes", (route) =>
route.fulfill({
status: fail ? 503 : 200,
contentType: "application/json",
body: JSON.stringify(
fail
? { error: "Sandbox is reconnecting" }
: {
branch: "work/one",
baseBranch: "main",
ahead: 0,
behind: 0,
files: [],
}
),
})
);
await page.getByRole("button", { name: "Open changes", exact: true }).click();
const panel = page.getByRole("complementary", { name: "Task tools" });
await expect(panel.getByRole("alert")).toHaveText("Sandbox is reconnecting");
fail = false;
await panel.getByRole("button", { name: "Retry changes" }).click();
await expect(panel).toContainText("No uncommitted changes.");
await expect(panel.getByRole("alert")).toHaveCount(0);
});
test("generated files open as task artifacts and can be reopened", async ({
page,
}) => {
const { sessions } = await mockControlWorkspace(page);
sessions[0].messages.push({
id: "attachment",
role: "assistant",
parts: [
{
type: "file",
url: "data:text/plain,Ready%20to%20review",
mediaType: "text/plain",
filename: "handoff.txt",
},
],
});
await page.reload();
const panel = page.getByRole("complementary", { name: "Task tools" });
await expect(
panel.getByRole("link", { name: "Open handoff.txt" })
).toBeVisible();
await page.getByRole("button", { name: "Close task tools" }).click();
await expect(panel).toBeHidden();
await page
.getByRole("button", { name: "Open artifacts", exact: true })
.click();
await expect(
panel.getByRole("link", { name: "Open handoff.txt" })
).toBeVisible();
});
test("compact navigation expands consistently after keyboard resizing", async ({
page,
}) => {
await page.setViewportSize({ width: 1600, height: 1000 });
await mockControlWorkspace(page);
const navigation = page.getByTestId("app-sidebar");
await expect(navigation).toHaveAttribute("data-compact", "true");
await page.getByRole("separator", { name: "Resize app navigation" }).focus();
await page.keyboard.press("ArrowRight");
expect((await navigation.boundingBox())!.width).toBeLessThan(100);
await page
.getByRole("button", { name: "Expand navigation", exact: true })
.click();
await expect(navigation).toHaveAttribute("data-compact", "false");
await page
.getByRole("button", { name: "Collapse navigation", exact: true })
.click();
await expect(navigation).toHaveAttribute("data-compact", "true");
});