tests/e2e/activation-flow-workspace.spec.ts
Rama main · réplica de lectura del repositorio original.
import { expect, test } from "@playwright/test";
import { enableScopedE2EAuth } from "./helpers/auth";
import {
connectedUser,
getTrackedEvents,
initializeTrackedEvents,
mockActivationFlow,
waitForTrackedEvent,
} from "./helpers/activation-fixtures";
import { capturePageErrors } from "./helpers/page-errors";
import { navigateWithPalette } from "./helpers/command-palette-nav";
const workspacePath = `/${connectedUser.username}/projects/workspace`;
test("activation flow tracks repo sync, preview start, workspace open, and preview feedback", async ({
page,
}) => {
await initializeTrackedEvents(page);
await enableScopedE2EAuth(page);
const harness = await mockActivationFlow(page);
await page.goto(workspacePath);
await page.waitForLoadState("networkidle");
await expect(page.getByTestId("home-sync-repos")).toBeVisible();
await page.getByTestId("home-sync-repos").click();
await expect(page.getByTestId("home-open-workspace-repo-1")).toBeVisible();
await waitForTrackedEvent(page, "repo_sync_completed");
let events = await getTrackedEvents(page);
expect(events).toContainEqual(
expect.objectContaining({
name: "repo_sync_started",
properties: expect.objectContaining({ source: "home_setup" }),
})
);
expect(events).toContainEqual(
expect.objectContaining({
name: "repo_sync_completed",
properties: expect.objectContaining({
source: "home_setup",
repo_count: 1,
}),
})
);
await page.getByTestId("home-open-workspace-repo-1").click();
await waitForTrackedEvent(page, "preview_started");
await waitForTrackedEvent(page, "workspace_opened");
events = await getTrackedEvents(page);
expect(events).toContainEqual(
expect.objectContaining({
name: "preview_started",
properties: expect.objectContaining({
source: "home_pane",
trigger: "open_workspace",
repo_id: "repo-1",
}),
})
);
expect(harness.getSandboxLaunchRequests()).toBe(1);
await expect(page.getByTestId("preview-grab-button")).toBeVisible();
await expect(page.getByTestId("preview-grab-button")).toBeEnabled();
events = await getTrackedEvents(page);
expect(events).toContainEqual(
expect.objectContaining({
name: "workspace_opened",
properties: expect.objectContaining({
source: "home_pane",
repo_id: "repo-1",
preview_state: "launch_requested",
}),
})
);
await page.getByTestId("preview-grab-button").click();
const previewStage = page.getByTestId("preview-stage");
const box = await previewStage.boundingBox();
if (!box) {
throw new Error("Preview stage was not visible");
}
await page.mouse.move(box.x + box.width * 0.2, box.y + box.height * 0.2);
await page.mouse.down();
await page.mouse.move(box.x + box.width * 0.7, box.y + box.height * 0.5, {
steps: 12,
});
await page.mouse.up();
await expect(page.getByTestId("preview-feedback-input")).toBeVisible();
await page
.getByTestId("preview-feedback-input")
.fill("Make the header clearer and tighten the hero spacing.");
const chatResponse = page.waitForResponse(
(response) =>
response.url().includes("/api/chat") &&
response.request().method() === "POST"
);
await page.getByTestId("preview-feedback-send").click();
await chatResponse;
const chatBody = harness.getLastChatBody();
expect(JSON.stringify(chatBody)).toContain("[Preview feedback —");
expect(JSON.stringify(chatBody)).toContain(
"Make the header clearer and tighten the hero spacing."
);
expect(chatBody).toEqual(
expect.objectContaining({
conversationId: expect.any(String),
repoId: "repo-1",
repoFullName: "acme/demo-app",
repoOwner: "acme",
repoName: "demo-app",
repoBranch: "main",
})
);
await waitForTrackedEvent(page, "preview_feedback_sent");
events = await getTrackedEvents(page);
expect(events).toContainEqual(
expect.objectContaining({
name: "preview_feedback_sent",
properties: expect.objectContaining({
source: "preview_pane",
}),
})
);
});
test("workspace bootstrap shows default panes and pane add/close works", async ({
page,
}) => {
await initializeTrackedEvents(page);
await enableScopedE2EAuth(page);
await mockActivationFlow(page);
await page.goto(workspacePath);
await page.waitForLoadState("networkidle");
await page.getByTestId("home-sync-repos").click();
await page.getByTestId("home-open-workspace-repo-1").click();
await expect(page.getByText("Workspace Chat · demo-app")).toBeVisible();
await expect(page.getByText("Live Preview")).toBeVisible();
await expect(page.getByText("Terminal")).toBeVisible();
await expect(page.getByText("Terminal Ready")).toBeVisible();
await expect(page.getByTestId("preview-grab-button")).toBeEnabled();
await expect(page.getByText("panes: 3")).toBeVisible();
const terminal = page
.locator('[data-pane-type="terminal"]')
.locator(".wterm");
const verticalResizeHandle = page
.locator(
'[data-slot="resizable-handle"][data-panel-group-direction="vertical"]'
)
.first();
const resizeHandleBox = await verticalResizeHandle.boundingBox();
if (!resizeHandleBox) {
throw new Error("Terminal resize handle was not visible");
}
// Flush the component's two-frame resize correction plus one assertion frame.
const waitForDeferredTerminalRender = () =>
page.evaluate(
() =>
new Promise<void>((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => resolve());
});
});
})
);
await terminal.focus();
// Empty submissions are echoed locally, so this creates scrollback without
// introducing sandbox PTY round trips into the layout regression.
for (let i = 0; i < 40; i += 1) {
await page.keyboard.press("Enter");
}
await expect(terminal).toHaveClass(/has-scrollback/);
// Arrange the following state explicitly: wterm's input handler aligns
// scrollback to whole rows, so typing alone need not reach the exact bottom.
// This regression exercises resize while following, then resize after the
// user deliberately scrolls away below.
await terminal.evaluate((element) => {
element.scrollTop = element.scrollHeight;
element.dispatchEvent(new Event("scroll"));
});
await waitForDeferredTerminalRender();
await page.mouse.move(
resizeHandleBox.x + resizeHandleBox.width / 2,
resizeHandleBox.y + resizeHandleBox.height / 2
);
await page.mouse.down();
await page.mouse.move(
resizeHandleBox.x + resizeHandleBox.width / 2,
resizeHandleBox.y - 80,
{ steps: 8 }
);
await page.mouse.up();
await expect
.poll(() =>
terminal.evaluate((element) => {
const distanceFromBottom =
element.scrollHeight - element.scrollTop - element.clientHeight;
return Math.abs(distanceFromBottom) < 1;
})
)
.toBe(true);
await waitForDeferredTerminalRender();
await terminal.evaluate((element) => {
element.scrollTop = 0;
element.dispatchEvent(new Event("scroll"));
});
const terminalHeightBeforeManualScrollResize = await terminal.evaluate(
(element) => element.clientHeight
);
const currentResizeHandleBox = await verticalResizeHandle.boundingBox();
if (!currentResizeHandleBox) {
throw new Error("Terminal resize handle disappeared after the first drag");
}
await page.mouse.move(
currentResizeHandleBox.x + currentResizeHandleBox.width / 2,
currentResizeHandleBox.y + currentResizeHandleBox.height / 2
);
await page.mouse.down();
await page.mouse.move(
currentResizeHandleBox.x + currentResizeHandleBox.width / 2,
currentResizeHandleBox.y + 40,
{ steps: 4 }
);
await page.mouse.up();
await expect
.poll(() => terminal.evaluate((element) => element.clientHeight))
.not.toBe(terminalHeightBeforeManualScrollResize);
await waitForDeferredTerminalRender();
expect(await terminal.evaluate((element) => element.scrollTop)).toBe(0);
const agentPane = page.locator('[data-pane-type="agent"]').first();
await agentPane.getByTitle("Add pane").click();
// The add-pane menu lists each pane type twice (split right + "Split below");
// take the first (horizontal split).
await page.getByRole("menuitem", { name: "Files" }).first().click();
await expect(page.getByText("panes: 4")).toBeVisible();
const filesPane = page.locator('[data-pane-type="files"]').last();
// File tree renders names across multiple elements; use the treeitem role
await expect(
filesPane.getByRole("treeitem", { name: "package.json" })
).toBeVisible();
await filesPane.getByTitle("Close pane").click();
await expect(page.getByText("panes: 3")).toBeVisible();
await expect(page.locator('[data-pane-type="files"]')).toHaveCount(0);
});
test("workspace editor starts language diagnostics without page errors", async ({
page,
}) => {
const pageErrors = capturePageErrors(page);
await enableScopedE2EAuth(page);
await mockActivationFlow(page);
await page.goto(workspacePath);
await page.waitForLoadState("networkidle");
await page.getByTestId("home-sync-repos").click();
await page.getByTestId("home-open-workspace-repo-1").click();
await page.getByTitle("Add pane").first().click();
await page.getByRole("menuitem", { name: "Files" }).first().click();
await page
.locator('[data-pane-type="files"]')
.getByRole("treeitem", { name: "package.json" })
.click();
const editorPane = page.locator('[data-pane-type="editor"]');
await expect(editorPane.locator(".monaco-editor")).toBeVisible();
await expect(editorPane).not.toContainText("Illegal value for token color");
const editorLines = editorPane.locator(".view-lines");
await expect(editorLines).toContainText("demo-app");
await editorLines.click();
await page.keyboard.press("ControlOrMeta+A");
await page.keyboard.type('{"name":');
await expect(editorPane.locator(".squiggly-error").first()).toBeVisible();
expect(pageErrors).toEqual([]);
});
test("terminal session survives navigation away from the workspace", async ({
page,
}) => {
await initializeTrackedEvents(page);
await enableScopedE2EAuth(page);
await mockActivationFlow(page);
await page.goto(workspacePath);
await page.waitForLoadState("networkidle");
await page.getByTestId("home-sync-repos").click();
await page.getByTestId("home-open-workspace-repo-1").click();
const terminal = page
.locator('[data-pane-type="terminal"]')
.locator(".wterm");
await expect(terminal).toContainText("Terminal Ready");
await terminal.focus();
await page.keyboard.type("route-persistence-marker");
await expect(terminal).toContainText("route-persistence-marker");
const terminalHost = page.locator("[data-terminal-session-host]").first();
const terminalHostHandle = await terminalHost.elementHandle();
if (!terminalHostHandle) {
throw new Error("Terminal session host was not mounted");
}
await page.getByTestId("app-nav-sandboxes").click();
await expect(page).toHaveURL(`/${connectedUser.username}/sandboxes`);
expect(
await terminalHostHandle.evaluate((element) => element.isConnected)
).toBe(true);
await navigateWithPalette(page, "Workspace");
await expect(page).toHaveURL(workspacePath);
await expect(terminal).toContainText("route-persistence-marker");
const returnedTerminalHostHandle = await page
.locator("[data-terminal-session-host]")
.first()
.elementHandle();
if (!returnedTerminalHostHandle) {
throw new Error("Terminal session host did not return to the workspace");
}
expect(
await page.evaluate(
([before, after]) => before === after,
[terminalHostHandle, returnedTerminalHostHandle]
)
).toBe(true);
});
test("workspace chat conversation persists after reload", async ({ page }) => {
await initializeTrackedEvents(page);
await enableScopedE2EAuth(page);
await mockActivationFlow(page);
await page.goto(workspacePath);
await page.waitForLoadState("networkidle");
await page.getByTestId("home-sync-repos").click();
await page.getByTestId("home-open-workspace-repo-1").click();
// Let the conversation load and the terminal finish opening before sending.
await page.waitForLoadState("networkidle");
const prompt = "remember this workspace message";
const saveResponse = page.waitForResponse(
(response) =>
response.url().includes("/api/conversations") &&
response.request().method() === "PUT" &&
(response.request().postData() || "").includes(prompt)
);
const chatInput = page.getByRole("textbox", {
name: "Ask the agent what to build, fix, or explain. Type / for commands or drop files here.",
});
await chatInput.fill(prompt);
await chatInput.press("Enter");
await expect(page.getByText("Applied preview feedback.")).toBeVisible();
await saveResponse;
await page.reload();
await page.waitForLoadState("networkidle");
if ((await page.getByText("Workspace Chat · demo-app").count()) === 0) {
await page.getByTestId("home-open-workspace-repo-1").click();
}
await expect(page.getByText(prompt)).toBeVisible();
await expect(page.getByText("Applied preview feedback.")).toBeVisible();
});