Files
paseo/packages/app/e2e/browser/composer-attachments.spec.ts
Mohamed Boudra 0b55d63ab1 fix(app): preserve workspace creation state through adoption
Publish the last draft edit before capturing cleanup ownership and keep published workspaces working while their initial agent creation is pending. Update browser journeys for the normal editable agent panel and make the placement assertion portable across platforms.
2026-10-26 10:29:24 +01:00

314 lines
11 KiB
TypeScript

import { expect, test } from "../support/fixtures";
import { clickNewChat } from "../support/helpers/launcher";
import { expectComposerVisible } from "../support/helpers/composer";
import { expectAgentIdle } from "../support/helpers/agent-stream";
import {
openAttachmentMenu,
expectAttachmentSheetRowsOnTitleRail,
openGithubPickerFromMenu,
attachImageFromMenu,
expectAttachmentPill,
removeAttachmentPill,
openImageLightbox,
closeImageLightbox,
pressInterruptShortcut,
expectComposerDraft,
expectComposerEditable,
fillComposerDraft,
dropFileOnComposer,
sendDraftToQueue,
expectQueuedMessageButton,
startRunningMockAgent,
selectGithubOption,
expectGithubAttachmentPill,
openGithubWorkspace,
} from "../support/helpers/composer";
import {
delayBrowserAgentCreatedStatus,
openNewWorkspaceComposer,
} from "../support/helpers/new-workspace";
import { gotoAppShell } from "../support/helpers/app";
import {
waitForSidebarHydration,
switchWorkspaceViaSidebar,
} from "../support/helpers/workspace-ui";
import { seedWorkspace } from "../support/helpers/seed-client";
import { hasGithubAuth, createTempGithubRepo } from "../support/helpers/github-fixtures";
import { getServerId } from "../support/helpers/server-id";
import { openFileExplorer } from "../support/helpers/file-explorer";
import { attachFileFromMenu, controlFileUploadCompletion } from "../support/helpers/composer";
const MINIMAL_PNG = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
"base64",
);
const TEST_IMAGE = { name: "test.png", mimeType: "image/png", buffer: MINIMAL_PNG };
const TEST_JSON = {
name: "config.json",
mimeType: "application/json",
buffer: Buffer.from(JSON.stringify({ composer: "drop" })),
};
test.describe("Composer attachments", () => {
test("selected file shows a loading attachment until upload is acknowledged", async ({
page,
withWorkspace,
}) => {
const upload = await controlFileUploadCompletion(page);
const workspace = await withWorkspace({ prefix: "attach-upload-pending-" });
await workspace.navigateTo();
await clickNewChat(page);
await expectComposerVisible(page);
upload.hold();
await attachFileFromMenu(page, TEST_JSON);
await upload.waitForUpload();
const pending = page.getByTestId("composer-pending-file-attachment");
await expect(pending).toContainText(TEST_JSON.name);
await expect(pending.getByRole("progressbar")).toBeVisible();
await expect(page.getByTestId("composer-file-attachment-pill")).toHaveCount(0);
upload.complete();
await expect(pending).toHaveCount(0);
await expect(page.getByTestId("composer-file-attachment-pill")).toContainText(TEST_JSON.name);
await expectComposerEditable(page);
});
test("compact Plus menu aligns attachment rows with its sheet title", async ({
page,
withWorkspace,
}) => {
test.setTimeout(60_000);
const workspace = await withWorkspace({ prefix: "attach-sheet-rails-" });
await workspace.navigateTo();
await clickNewChat(page);
await expectComposerVisible(page);
await page.setViewportSize({ width: 390, height: 844 });
await openAttachmentMenu(page);
await expectAttachmentSheetRowsOnTitleRail(page);
});
test("GitHub combobox does not render until the picker is opened", async ({
page,
withWorkspace,
}) => {
test.setTimeout(60_000);
const workspace = await withWorkspace({ prefix: "attach-gh-lazy-" });
await workspace.navigateTo();
await clickNewChat(page);
await expectComposerVisible(page);
await expect(page.getByTestId("combobox-desktop-container")).not.toBeVisible();
await openGithubPickerFromMenu(page);
await expect(page.getByPlaceholder("Search issues and PRs...")).toBeVisible({ timeout: 5_000 });
await expect(
page.getByTestId("combobox-empty-text").or(page.getByText("Searching...")),
).toBeVisible({ timeout: 10_000 });
});
test("attaches an issue and a pull request from the repository picker", async ({ page }) => {
test.setTimeout(120_000);
if (!hasGithubAuth()) {
test.skip(true, "GitHub auth not available in this environment");
}
const ghRepo = await createTempGithubRepo({
category: "attachments",
issues: [{ title: "fix: attachment issue" }],
prs: [{ title: "feat: attachment pull request", state: "open" }],
});
const handle = await openGithubWorkspace(page, ghRepo.prs[0].localPath);
try {
await clickNewChat(page);
await expectComposerVisible(page);
// Choose from the repository list. Newly created repositories are not
// necessarily available in GitHub's separate text-search index yet.
await selectGithubOption(page, "", `issue:${ghRepo.issues[0].number}`);
await expectGithubAttachmentPill(page, ghRepo.issues[0]);
await selectGithubOption(page, "", `change_request:${ghRepo.prs[0].number}`);
await expectGithubAttachmentPill(page, ghRepo.prs[0]);
await expectGithubAttachmentPill(page, ghRepo.issues[0]);
} finally {
await handle.cleanup();
await ghRepo.cleanup();
}
});
test.fixme("workspace-review pill suppresses on X-click and reappears after send", async () => {
// The review attachment is created via InlineReviewEditor in surface.tsx (addComment action).
// Automating this requires: a workspace with staged changes, navigating to the diff panel,
// hovering the gutter "+" button, typing a comment, and submitting. A dedicated
// helpers/review.ts with addInlineReviewComment(page, filePath, lineNumber, comment) is
// needed before this can be exercised end-to-end.
});
test("attaches, previews, and removes an image before dropping a file", async ({
page,
withWorkspace,
}) => {
test.setTimeout(60_000);
const workspace = await withWorkspace({ prefix: "attach-lightbox-" });
await workspace.navigateTo();
await clickNewChat(page);
await expectComposerVisible(page);
await test.step("open the available attachment choices", async () => {
await openAttachmentMenu(page);
await expect(page.getByTestId("message-input-attachment-menu-item-image")).toBeVisible();
await expect(page.getByTestId("message-input-attachment-menu-item-github")).toBeVisible();
await page.keyboard.press("Escape");
});
await attachImageFromMenu(page, TEST_IMAGE);
await expectAttachmentPill(page, "composer-image-attachment-pill");
await openImageLightbox(page);
await closeImageLightbox(page);
await test.step("remove the image and attach a dropped file", async () => {
await removeAttachmentPill(page, "composer-image-attachment-pill", "Remove image attachment");
await expect(page.getByTestId("composer-image-attachment-pill")).toHaveCount(0);
await dropFileOnComposer(page, TEST_JSON);
await expectAttachmentPill(page, "composer-file-attachment-pill");
});
});
test("dropped JSON file renders as a file attachment in New Workspace", async ({ page }) => {
test.setTimeout(120_000);
const workspace = await seedWorkspace({ repoPrefix: "attach-drop-new-workspace-" });
try {
await gotoAppShell(page);
await waitForSidebarHydration(page);
await switchWorkspaceViaSidebar({
page,
serverId: getServerId(),
workspaceId: workspace.workspaceId,
});
await openNewWorkspaceComposer(page, {
projectKey: workspace.projectKey,
projectDisplayName: workspace.projectDisplayName,
});
await dropFileOnComposer(page, TEST_JSON);
await expectAttachmentPill(page, "composer-file-attachment-pill");
} finally {
await workspace.cleanup();
}
});
test("submitting while agent is running queues the message and clears the draft", async ({
page,
}) => {
test.setTimeout(120_000);
const agent = await startRunningMockAgent(page, {
prefix: "attach-queue-",
model: "one-minute-stream",
prompt: "Stay running for queue test.",
});
try {
await fillComposerDraft(page, "queued draft text");
await sendDraftToQueue(page);
await expectQueuedMessageButton(page);
await expectComposerDraft(page, "");
} finally {
await agent.cleanup();
}
});
test("Escape interrupt cancels the running agent and preserves composer draft", async ({
page,
}) => {
test.setTimeout(120_000);
const agent = await startRunningMockAgent(page, {
prefix: "attach-interrupt-",
model: "ten-second-stream",
prompt: "Stay running for interrupt test.",
});
try {
await fillComposerDraft(page, "preserve me");
await pressInterruptShortcut(page);
await expectAgentIdle(page, 15_000);
await expectComposerDraft(page, "preserve me");
} finally {
await agent.cleanup();
}
});
test("Escape cancels file creation without interrupting the running agent", async ({ page }) => {
test.setTimeout(120_000);
const agent = await startRunningMockAgent(page, {
prefix: "file-create-escape-",
model: "one-minute-stream",
prompt: "Stay running while a file draft is cancelled.",
});
try {
await openFileExplorer(page);
await page.getByTestId("files-new-file").click();
const nameInput = page.getByTestId("file-explorer-name-input");
await expect(nameInput).toBeVisible();
await nameInput.press("Escape");
await expect(nameInput).toBeHidden();
await expect(page.getByRole("button", { name: /stop|cancel/i }).first()).toBeVisible();
} finally {
await agent.cleanup();
}
});
test("composer and attachments stay available while new workspace agent is being created", async ({
page,
}) => {
test.setTimeout(120_000);
const serverId = getServerId();
const agentCreatedDelay = await delayBrowserAgentCreatedStatus(page);
const workspace = await seedWorkspace({ repoPrefix: "attach-lock-" });
try {
await gotoAppShell(page);
await waitForSidebarHydration(page);
await switchWorkspaceViaSidebar({
page,
serverId,
workspaceId: workspace.workspaceId,
});
await openNewWorkspaceComposer(page, {
projectKey: workspace.projectKey,
projectDisplayName: workspace.projectDisplayName,
});
await fillComposerDraft(page, "lock test prompt");
const createButton = page
.getByTestId("message-input-root")
.getByRole("button", { name: "Create" });
await expect(createButton).toBeVisible({ timeout: 30_000 });
await createButton.click();
await agentCreatedDelay.waitForCreateRequest();
await agentCreatedDelay.waitForDelayedCreatedStatus();
await expectComposerEditable(page);
await attachImageFromMenu(page, TEST_IMAGE);
await expectAttachmentPill(page, "composer-image-attachment-pill");
agentCreatedDelay.release();
await expectComposerEditable(page);
} finally {
agentCreatedDelay.release();
await workspace.cleanup();
}
});
});