mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 07:33:18 +00:00
Give rail-specific split journeys enough column width, assert launched Diff content independently of header shape, and retain pane identity across compact transitions. Expect the compact selector in the half-screen Explorer workflow. Co-authored-by: Elsa Granger <zeyugao@outlook.com>
863 lines
34 KiB
TypeScript
863 lines
34 KiB
TypeScript
import type { Locator } from "@playwright/test";
|
|
import { writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { expect, test, type Page } from "../support/fixtures";
|
|
import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent";
|
|
import { gateNextAgentMessage } from "../support/helpers/agent-message-gate";
|
|
import {
|
|
fillComposerDraft,
|
|
expectComposerDraft,
|
|
attachFileFromMenu,
|
|
} from "../support/helpers/composer";
|
|
import { ensureExplorerSidebar, openFilesPanel } from "../support/helpers/workspace-tabs";
|
|
|
|
const APP_SETTINGS_KEY = "@paseo:app-settings";
|
|
|
|
function visibleMainPane(page: Page) {
|
|
return page.getByTestId("workspace-pane-main").filter({ visible: true });
|
|
}
|
|
|
|
function composerChangesPill(page: Page) {
|
|
return page.getByRole("button", { name: "Open Changes tab", exact: true });
|
|
}
|
|
|
|
async function revealComposerChangesInExplorer(page: Page) {
|
|
await composerChangesPill(page).click();
|
|
|
|
const explorer = await ensureExplorerSidebar(page);
|
|
await expect(explorer.getByTestId("changes-tree-panel")).toBeVisible({ timeout: 30_000 });
|
|
await expect(page.getByTestId("workspace-tab-working_diff")).toHaveCount(0);
|
|
}
|
|
|
|
async function openComposerDiff(page: Page) {
|
|
await composerChangesPill(page).click();
|
|
}
|
|
|
|
async function seedChangedAgent(
|
|
repoPrefix: string,
|
|
featureValues?: Record<string, unknown>,
|
|
files: Array<{ path: string; content: string }> = [],
|
|
) {
|
|
const workspace = await seedMockAgentWorkspace({
|
|
repoPrefix,
|
|
featureValues,
|
|
title: "Composer diff stat",
|
|
repo: {
|
|
withRemote: true,
|
|
// Exclude the local remote before the daemon starts observing this checkout.
|
|
files: [{ path: ".gitignore", content: "/remote.git/\n" }, ...files],
|
|
},
|
|
});
|
|
try {
|
|
await writeFile(
|
|
path.join(workspace.cwd, "README.md"),
|
|
"# Temp Repo\nexport const one = 1;\nexport const two = 2;\n",
|
|
);
|
|
await workspace.client.checkoutRefresh(workspace.cwd);
|
|
await expect
|
|
.poll(async () => {
|
|
const workspaces = await workspace.client.fetchWorkspaces();
|
|
return (
|
|
workspaces.entries.find((entry) => entry.id === workspace.workspaceId)?.diffStat ?? null
|
|
);
|
|
})
|
|
.toEqual({ additions: 2, deletions: 0 });
|
|
return workspace;
|
|
} catch (error) {
|
|
await workspace.cleanup();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
test("composer diff stat reveals Changes, then opens the diff in the configured side pane", async ({
|
|
page,
|
|
}) => {
|
|
await page.addInitScript((settingsKey) => {
|
|
localStorage.setItem(settingsKey, JSON.stringify({ openInSidePane: { diffs: true } }));
|
|
}, APP_SETTINGS_KEY);
|
|
const workspace = await seedChangedAgent("composer-diff-stat-side-");
|
|
|
|
try {
|
|
// Keep both workspace rails visible alongside app navigation and Explorer.
|
|
await page.setViewportSize({ width: 1900, height: 900 });
|
|
await openAgentRoute(page, {
|
|
workspaceId: workspace.workspaceId,
|
|
agentId: workspace.agentId,
|
|
});
|
|
|
|
const pill = composerChangesPill(page);
|
|
await expect(pill).toBeVisible({ timeout: 30_000 });
|
|
await expect(pill).toContainText("+2");
|
|
await expect(pill).toContainText("-0");
|
|
await revealComposerChangesInExplorer(page);
|
|
await openComposerDiff(page);
|
|
|
|
const sidePane = page
|
|
.locator('[data-testid^="workspace-pane-"]')
|
|
.filter({ visible: true })
|
|
.filter({ has: page.getByTestId("working-diff-panel") });
|
|
await expect(sidePane.getByTestId("workspace-tab-working_diff")).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
await expect(sidePane.getByTestId("working-diff-panel")).toBeVisible({ timeout: 30_000 });
|
|
await expect(visibleMainPane(page).getByTestId("working-diff-panel")).toHaveCount(0);
|
|
|
|
await test.step("Explorer navigation does not replace the side pane", async () => {
|
|
await openFilesPanel(page);
|
|
await expect(page.getByTestId("workspace-explorer-sidebar")).toContainText("Files");
|
|
|
|
await pill.click();
|
|
await expect(sidePane.getByTestId("working-diff-panel")).toBeVisible();
|
|
await expect(page.getByTestId("workspace-tab-working_diff")).toHaveCount(1);
|
|
});
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
test("composer diff stat opens the compact explorer instead of a Changes tab", async ({ page }) => {
|
|
const workspace = await seedChangedAgent("composer-diff-stat-compact-");
|
|
|
|
try {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await openAgentRoute(page, {
|
|
workspaceId: workspace.workspaceId,
|
|
agentId: workspace.agentId,
|
|
});
|
|
|
|
const closeExplorer = page
|
|
.getByTestId("workspace-explorer-sidebar")
|
|
.getByRole("button", { name: "Close Explorer sidebar" });
|
|
await expect(closeExplorer).not.toBeInViewport();
|
|
|
|
await page.getByTestId("composer-diff-stat-pill").click();
|
|
|
|
await expect(closeExplorer).toBeInViewport({ timeout: 30_000 });
|
|
await expect(page.getByTestId("changes-header").filter({ visible: true }).first()).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
await expect(page.getByTestId("workspace-tab-working_diff")).toHaveCount(0);
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
test("composer diff stat reveals Changes, then opens the diff in the focused pane by default", async ({
|
|
page,
|
|
}) => {
|
|
const workspace = await seedChangedAgent("composer-diff-stat-tab-");
|
|
|
|
try {
|
|
await page.setViewportSize({ width: 1400, height: 900 });
|
|
await openAgentRoute(page, {
|
|
workspaceId: workspace.workspaceId,
|
|
agentId: workspace.agentId,
|
|
});
|
|
|
|
await revealComposerChangesInExplorer(page);
|
|
await openComposerDiff(page);
|
|
|
|
const mainPane = visibleMainPane(page);
|
|
await expect(mainPane.getByTestId("workspace-tab-working_diff")).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
await expect(mainPane.getByTestId("working-diff-panel")).toBeVisible({ timeout: 30_000 });
|
|
await expect(
|
|
page.locator('[data-testid^="workspace-pane-"]').filter({ visible: true }),
|
|
).toHaveCount(1);
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
async function tapFirstAddedDiffLine(
|
|
page: Page,
|
|
surface: Locator = page.getByTestId("working-diff-panel"),
|
|
) {
|
|
const body = surface.getByTestId("diff-file-0-body");
|
|
await expect(body).toBeVisible({ timeout: 30000 });
|
|
const bounds = await body.boundingBox();
|
|
if (!bounds) throw new Error("Expanded diff body has no bounds");
|
|
const fontSize = await surface
|
|
.getByTestId("git-diff-canvas")
|
|
.evaluate((element) => Number.parseFloat(getComputedStyle(element).fontSize));
|
|
await page.touchscreen.tap(bounds.x + 120, bounds.y + Math.round(fontSize * 1.5) * 2.5);
|
|
}
|
|
|
|
async function openReviewDiff(page: Page, workspace: Awaited<ReturnType<typeof seedChangedAgent>>) {
|
|
await page.setViewportSize({ width: 1400, height: 900 });
|
|
await openAgentRoute(page, { workspaceId: workspace.workspaceId, agentId: workspace.agentId });
|
|
await expect(composerChangesPill(page)).toBeVisible({ timeout: 30000 });
|
|
if (await page.getByTestId("workspace-tab-working_diff").count()) {
|
|
await selectDiffTab(page);
|
|
} else {
|
|
await revealComposerChangesInExplorer(page);
|
|
await openComposerDiff(page);
|
|
}
|
|
await expect(page.getByTestId("diff-file-0-body")).toBeVisible();
|
|
}
|
|
|
|
async function saveReviewComment(page: Page, body: string) {
|
|
await page.getByRole("textbox", { name: "Review comment" }).fill(body);
|
|
await page.getByRole("button", { name: "Save review comment", exact: true }).click();
|
|
await expect(page.getByRole("button", { name: "Bottom sheet backdrop" })).toHaveCount(0);
|
|
}
|
|
|
|
async function cancelReviewComment(page: Page) {
|
|
await page.getByRole("button", { name: "Cancel review comment", exact: true }).click();
|
|
await expect(page.getByRole("button", { name: "Bottom sheet backdrop" })).toHaveCount(0);
|
|
}
|
|
|
|
test.describe("review comments", () => {
|
|
test.use({ hasTouch: true });
|
|
|
|
test("compact review comments open a sheet and preserve saved comments through save, edit, cancel and reload", async ({
|
|
page,
|
|
}) => {
|
|
const workspace = await seedChangedAgent("compact-review-comment-");
|
|
try {
|
|
await test.step("Open a compact sheet with reachable input and actions", async () => {
|
|
await openReviewDiff(page, workspace);
|
|
await showCompactLayout(page);
|
|
await tapFirstAddedDiffLine(page);
|
|
await expectCompactCommentSheet(page);
|
|
});
|
|
await test.step("Save, discard an edit, then save an updated comment", async () => {
|
|
await saveReviewComment(page, " Please simplify this. ");
|
|
await expectSavedReview(page, "Please simplify this.");
|
|
await editSavedReview(page, "Please simplify this.");
|
|
await fillReviewComment(page, "Discard this edit");
|
|
await cancelReviewComment(page);
|
|
await expectSavedReview(page, "Please simplify this.");
|
|
await editSavedReview(page, "Please simplify this.");
|
|
await saveReviewComment(page, "Updated comment");
|
|
await expectSavedReview(page, "Updated comment");
|
|
});
|
|
await test.step("Discard a new comment and retain the saved one after reload", async () => {
|
|
await tapFirstAddedDiffLine(page);
|
|
await fillReviewComment(page, "Discard new comment");
|
|
await cancelReviewComment(page);
|
|
await expectReviewAbsent(page, "Discard new comment");
|
|
await reloadReview(page);
|
|
await expectSavedReview(page, "Updated comment");
|
|
});
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
test("wide review comments retain inline editing", async ({ page }) => {
|
|
const workspace = await seedChangedAgent("wide-review-comment-");
|
|
try {
|
|
await test.step("Save and cancel an inline edit on a wide screen", async () => {
|
|
await openReviewDiff(page, workspace);
|
|
await tapFirstAddedDiffLine(page);
|
|
await expectInlineCommentEditor(page);
|
|
await saveReviewComment(page, "Wide comment");
|
|
await expectSavedReview(page, "Wide comment");
|
|
await editSavedReview(page, "Wide comment");
|
|
await expectInlineCommentEditor(page);
|
|
await cancelReviewComment(page);
|
|
await expectSavedReview(page, "Wide comment");
|
|
});
|
|
await test.step("The existing composer still sends the review", async () => {
|
|
await sendReviewThroughComposer(page, workspace.agentId);
|
|
await selectDiffTab(page);
|
|
await expectReviewAbsent(page, "Wide comment");
|
|
});
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
});
|
|
|
|
async function sendReviewThroughComposer(page: Page, agentId: string) {
|
|
await page.getByTestId(`workspace-tab-agent_${agentId}`).click();
|
|
await expect(page.getByTestId("composer-review-attachment-pill")).toBeVisible();
|
|
await page.getByRole("textbox", { name: "Message agent..." }).fill("Address this review");
|
|
await page.getByRole("button", { name: "Send message", exact: true }).click();
|
|
await expect(page.getByText("Address this review", { exact: true }).first()).toBeVisible();
|
|
await expect(page.getByTestId("composer-review-attachment-pill")).toHaveCount(0);
|
|
}
|
|
|
|
async function addSavedReview(
|
|
page: Page,
|
|
body: string,
|
|
surface: Locator = page.getByTestId("working-diff-panel"),
|
|
) {
|
|
await tapFirstAddedDiffLine(page, surface);
|
|
await saveReviewComment(page, body);
|
|
await expectSavedReview(page, body, surface);
|
|
}
|
|
|
|
async function expectSavedReview(
|
|
page: Page,
|
|
body: string,
|
|
surface: Locator = page.getByTestId("working-diff-panel"),
|
|
) {
|
|
await expect(surface.getByText(body, { exact: true })).toBeVisible();
|
|
}
|
|
|
|
async function editSavedReview(page: Page, expectedBody: string) {
|
|
await page
|
|
.getByTestId("working-diff-panel")
|
|
.getByRole("button", { name: "Edit review comment", exact: true })
|
|
.click();
|
|
await expect(page.getByRole("textbox", { name: "Review comment" })).toHaveValue(expectedBody);
|
|
}
|
|
|
|
async function sendFeedback(page: Page, count: number) {
|
|
await page.getByRole("button", { name: `Send feedback (${count})`, exact: true }).click();
|
|
}
|
|
|
|
async function expectFeedbackSent(page: Page, recipient: string) {
|
|
await expect(page.getByTestId("app-toast")).toHaveText(`Feedback sent to ${recipient}`);
|
|
await expect(page.getByTestId("working-diff-panel")).toBeVisible();
|
|
}
|
|
|
|
test.describe("send review feedback", () => {
|
|
test.use({ hasTouch: true });
|
|
|
|
test("compact feedback sends directly to the only workspace agent and preserves its composer draft", async ({
|
|
page,
|
|
}) => {
|
|
const workspace = await seedChangedAgent("review-feedback-single-");
|
|
try {
|
|
await test.step("Save a review without changing the chat draft", async () => {
|
|
await openReviewDiff(page, workspace);
|
|
await selectAgentTab(page, workspace.agentId);
|
|
await fillComposerDraft(page, "Keep this thought");
|
|
await attachFileFromMenu(page, {
|
|
name: "unrelated.txt",
|
|
mimeType: "text/plain",
|
|
buffer: Buffer.from("Unrelated draft attachment"),
|
|
});
|
|
await selectDiffTab(page);
|
|
await showCompactLayout(page);
|
|
await addSavedReview(page, "Please simplify this.");
|
|
});
|
|
await test.step("Send directly and stay on the diff", async () => {
|
|
await sendFeedback(page, 1);
|
|
await expectFeedbackSent(page, "Composer diff stat");
|
|
await expectReviewAbsent(page, "Please simplify this.");
|
|
await captureFeedback(page, "single");
|
|
await expect(
|
|
page.getByText("Feedback sent to Composer diff stat", { exact: true }),
|
|
).toHaveCount(0);
|
|
});
|
|
await test.step("Return to the untouched composer draft", async () => {
|
|
await showWideLayout(page);
|
|
await selectAgentTab(page, workspace.agentId);
|
|
await expectComposerDraft(page, "Keep this thought");
|
|
await expect(page.getByText("unrelated.txt", { exact: true })).toBeVisible();
|
|
await expect(page.getByTestId("composer-review-attachment-pill")).toHaveCount(0);
|
|
});
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
test("feedback pending ownership follows the draft across Diff and compact Changes", async ({
|
|
page,
|
|
}) => {
|
|
const workspace = await seedChangedAgent("review-shared-pending-");
|
|
const gate = await gateNextAgentMessage(page);
|
|
try {
|
|
await test.step("Send feedback from the Diff tab and hold its real request", async () => {
|
|
await openReviewDiff(page, workspace);
|
|
await showCompactLayout(page);
|
|
await addSavedReview(page, "One draft across surfaces");
|
|
await sendFeedback(page, 1);
|
|
await gate.waitForRequest();
|
|
});
|
|
await test.step("The compact Changes surface observes the same pending operation", async () => {
|
|
await revealCompactChanges(page);
|
|
await expect(
|
|
compactReviewSurface(page).getByRole("button", {
|
|
name: "Sending feedback (1)",
|
|
exact: true,
|
|
}),
|
|
).toBeDisabled();
|
|
expect(gate.requestCount()).toBe(1);
|
|
gate.accept();
|
|
await expectFeedbackSent(page, "Composer diff stat");
|
|
});
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
test("an unsaved review survives presentation changes and acknowledgement before Save", async ({
|
|
page,
|
|
}) => {
|
|
const workspace = await seedChangedAgent("review-editor-ownership-");
|
|
const gate = await gateNextAgentMessage(page);
|
|
try {
|
|
await test.step("Carry unsaved inline text into the compact sheet and back", async () => {
|
|
await openReviewDiff(page, workspace);
|
|
await tapFirstAddedDiffLine(page);
|
|
await fillReviewComment(page, "Body across presentations");
|
|
await showCompactLayout(page);
|
|
await expectReviewEditorBody(page, "Body across presentations");
|
|
await showWideLayout(page);
|
|
await expectReviewEditorBody(page, "Body across presentations");
|
|
await saveReviewComment(page, "Body across presentations");
|
|
});
|
|
await test.step("Keep an open edit saveable after acknowledgement removes its original", async () => {
|
|
await sendFeedback(page, 1);
|
|
await gate.waitForRequest();
|
|
await editSavedReview(page, "Body across presentations");
|
|
await fillReviewComment(page, "Keep this unsaved edit");
|
|
gate.accept();
|
|
await expectFeedbackSent(page, "Composer diff stat");
|
|
await expectReviewEditorBody(page, "Keep this unsaved edit");
|
|
await showCompactLayout(page);
|
|
await expectReviewEditorBody(page, "Keep this unsaved edit");
|
|
await saveReviewComment(page, "Keep this unsaved edit");
|
|
await expectSavedReview(page, "Keep this unsaved edit");
|
|
});
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
test("saved feedback preserves navigation across multiple diff files", async ({ page }) => {
|
|
const workspace = await seedMultiFileReview();
|
|
try {
|
|
await test.step("Save compact feedback and retain both floating actions", async () => {
|
|
await openCompactReviewExplorer(page, workspace);
|
|
await addSavedReview(page, "Keep file navigation available", compactReviewSurface(page));
|
|
await expectReviewNavigationReachable(page);
|
|
});
|
|
await test.step("Jump to another file without covering the feedback action", async () => {
|
|
await jumpToReviewFile(page, "z-last.ts");
|
|
await scrollReviewToLastLine(page);
|
|
await expectReviewNavigationReachable(page);
|
|
await captureFeedback(page, "navigation-compact");
|
|
});
|
|
await test.step("Wide file navigation and feedback remain reachable", async () => {
|
|
await openReviewDiff(page, workspace);
|
|
await navigateWideReviewFile(page, "README.md");
|
|
await expectFeedbackReachable(page);
|
|
await expectSavedReview(page, "Keep file navigation available");
|
|
await captureFeedback(page, "navigation-wide");
|
|
});
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
test("feedback sends only matching comments and preserves orphaned comments", async ({
|
|
page,
|
|
}) => {
|
|
const workspace = await seedMultiFileReview();
|
|
try {
|
|
await test.step("A saved comment whose diff disappears has no sendable context", async () => {
|
|
await openReviewDiff(page, workspace);
|
|
await showCompactLayout(page);
|
|
await addSavedReview(page, "Retain this orphan");
|
|
await setReadmeDiff(workspace, false);
|
|
await expectUnmatchedFeedback(page);
|
|
});
|
|
await test.step("Send the matching subset with an accurate count", async () => {
|
|
await addSavedReview(page, "Send this matching comment");
|
|
await sendFeedback(page, 1);
|
|
await expectFeedbackSent(page, "Composer diff stat");
|
|
await expectReviewAbsent(page, "Send this matching comment");
|
|
await expectUnmatchedFeedback(page);
|
|
});
|
|
await test.step("Restoring the missing diff reveals the unsent comment", async () => {
|
|
await setReadmeDiff(workspace, true);
|
|
await expectSavedReview(page, "Retain this orphan");
|
|
await expectFeedbackReachable(page);
|
|
});
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
test("zero open agent tabs retain feedback with a disabled reason", async ({ page }) => {
|
|
const workspace = await seedChangedAgent("review-feedback-zero-");
|
|
try {
|
|
await test.step("Close the agent tab while keeping the review", async () => {
|
|
await openReviewDiff(page, workspace);
|
|
await addSavedReview(page, "Keep until an agent is opened");
|
|
await closeAgentTab(page, workspace.agentId);
|
|
});
|
|
await test.step("The disabled action explains why in wide and compact layouts", async () => {
|
|
await expectFeedbackUnavailable(page);
|
|
await captureFeedback(page, "zero-wide");
|
|
await showCompactLayout(page);
|
|
await expectFeedbackUnavailable(page);
|
|
await expectSavedReview(page, "Keep until an agent is opened");
|
|
await captureFeedback(page, "zero-compact");
|
|
});
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
test("only open tabs in this workspace appear in compact and wide recipient menus", async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(120000);
|
|
const workspace = await seedChangedAgent("review-feedback-multiple-");
|
|
const other = await seedChangedAgent("review-feedback-other-");
|
|
try {
|
|
const second = await createReviewRecipient(workspace, "Second reviewer");
|
|
const historical = await createReviewRecipient(workspace, "Closed historical agent");
|
|
await test.step("Open two local tabs and a tab in another workspace", async () => {
|
|
await openReviewDiff(page, other);
|
|
await addSavedReview(page, "Other workspace review");
|
|
await openReviewDiff(page, workspace);
|
|
await closeAgentTab(page, historical.id);
|
|
await openAgentRoute(page, { workspaceId: workspace.workspaceId, agentId: second.id });
|
|
await selectDiffTab(page);
|
|
await addSavedReview(page, "Review for second agent");
|
|
});
|
|
await test.step("The wide menu includes only local open agents", async () => {
|
|
await captureWideFeedbackAppearance(page);
|
|
});
|
|
await test.step("Escape returns keyboard focus to feedback so the chooser can reopen", async () => {
|
|
await openFeedbackWithKeyboard(page);
|
|
await dismissRecipientMenu(page);
|
|
await expectFeedbackFocused(page);
|
|
await reopenFeedbackWithKeyboard(page);
|
|
await dismissRecipientMenu(page);
|
|
await expectFeedbackFocused(page);
|
|
});
|
|
await test.step("Choose the second agent in the compact menu and stay on the diff", async () => {
|
|
await showCompactLayout(page);
|
|
await captureFeedback(page, "cta-compact");
|
|
await sendFeedback(page, 1);
|
|
await expectRecipientMenu(page);
|
|
await captureFeedback(page, "menu-compact");
|
|
await chooseFeedbackRecipient(page, "Second reviewer");
|
|
await expectFeedbackSent(page, "Second reviewer");
|
|
await expectReviewAbsent(page, "Review for second agent");
|
|
});
|
|
await test.step("The other workspace's saved review remains", async () => {
|
|
await openReviewDiff(page, other);
|
|
await expectSavedReview(page, "Other workspace review");
|
|
});
|
|
} finally {
|
|
await workspace.cleanup();
|
|
await other.cleanup();
|
|
}
|
|
});
|
|
|
|
test("failed feedback remains editable and retries through the real provider", async ({
|
|
page,
|
|
}) => {
|
|
const workspace = await seedChangedAgent("review-feedback-retry-", { mockPromptRejections: 1 });
|
|
try {
|
|
await test.step("A provider rejection retains the saved review", async () => {
|
|
await openReviewDiff(page, workspace);
|
|
await showCompactLayout(page);
|
|
await addSavedReview(page, "Initial feedback");
|
|
await sendFeedback(page, 1);
|
|
await expectFeedbackFailure(page);
|
|
await expectSavedReview(page, "Initial feedback");
|
|
await captureFeedback(page, "failure");
|
|
});
|
|
await test.step("Edit the retained feedback and retry successfully", async () => {
|
|
await editSavedReview(page, "Initial feedback");
|
|
await saveReviewComment(page, "Corrected feedback");
|
|
await sendFeedback(page, 1);
|
|
await expectFeedbackSent(page, "Composer diff stat");
|
|
await expectReviewAbsent(page, "Corrected feedback");
|
|
});
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
test("pending feedback blocks repeat submission and preserves edits and additions", async ({
|
|
page,
|
|
}) => {
|
|
const workspace = await seedChangedAgent("review-feedback-pending-");
|
|
const gate = await gateNextAgentMessage(page);
|
|
try {
|
|
await test.step("Hold the real request while feedback is pending", async () => {
|
|
await openReviewDiff(page, workspace);
|
|
await showCompactLayout(page);
|
|
await addSavedReview(page, "Sent version");
|
|
await sendFeedback(page, 1);
|
|
const request = await gate.waitForRequest();
|
|
expect(request.agentId).toBe(workspace.agentId);
|
|
expect(request.text).toBe("Please address this code review.");
|
|
expect(request.attachments).toMatchObject([
|
|
{
|
|
type: "review",
|
|
cwd: workspace.cwd,
|
|
comments: [
|
|
{
|
|
filePath: "README.md",
|
|
lineNumber: 2,
|
|
body: "Sent version",
|
|
context: { targetLine: { content: "export const one = 1;", newLineNumber: 2 } },
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
await expectFeedbackPending(page);
|
|
});
|
|
await test.step("Save an edit and another comment before acknowledgement", async () => {
|
|
await editSavedReview(page, "Sent version");
|
|
await saveReviewComment(page, "Edited during send");
|
|
await addSavedReview(page, "Added during send");
|
|
await expectFeedbackPending(page);
|
|
expect(gate.requestCount()).toBe(1);
|
|
await captureFeedback(page, "pending");
|
|
});
|
|
await test.step("A real acknowledgement preserves both new versions", async () => {
|
|
gate.accept();
|
|
await expectFeedbackSent(page, "Composer diff stat");
|
|
await expectSavedReview(page, "Edited during send");
|
|
await expectSavedReview(page, "Added during send");
|
|
await expectReviewAbsent(page, "Sent version");
|
|
});
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
});
|
|
|
|
async function showCompactLayout(page: Page) {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
}
|
|
async function showWideLayout(page: Page) {
|
|
await page.setViewportSize({ width: 1400, height: 900 });
|
|
}
|
|
async function selectAgentTab(page: Page, agentId: string) {
|
|
await page.getByTestId(`workspace-tab-agent_${agentId}`).click({ position: { x: 12, y: 13 } });
|
|
}
|
|
async function selectDiffTab(page: Page) {
|
|
await page.getByTestId("workspace-tab-working_diff").click({ position: { x: 12, y: 13 } });
|
|
}
|
|
async function fillReviewComment(page: Page, body: string) {
|
|
await page.getByRole("textbox", { name: "Review comment" }).fill(body);
|
|
}
|
|
async function expectReviewAbsent(page: Page, body: string) {
|
|
await expect(page.getByTestId("working-diff-panel").getByText(body, { exact: true })).toHaveCount(
|
|
0,
|
|
);
|
|
}
|
|
async function reloadReview(page: Page) {
|
|
await page.reload();
|
|
await expect(page.getByTestId("working-diff-panel")).toBeVisible({ timeout: 30000 });
|
|
}
|
|
async function expectInlineCommentEditor(page: Page) {
|
|
await expect(page.getByTestId("inline-review-editor")).toBeVisible();
|
|
await expect(page.getByTestId("review-comment-sheet")).toHaveCount(0);
|
|
}
|
|
async function expectCompactCommentSheet(page: Page) {
|
|
await expect(page.getByTestId("review-comment-sheet")).toBeVisible();
|
|
await expect(page.getByText("README.md · +2", { exact: true })).toBeVisible();
|
|
await expect(page.getByRole("textbox", { name: "Review comment" })).toBeInViewport({ ratio: 1 });
|
|
for (const action of ["Save", "Cancel"])
|
|
await expect(
|
|
page.getByRole("button", { name: `${action} review comment`, exact: true }),
|
|
).toBeInViewport({ ratio: 1 });
|
|
await expect(page.getByTestId("inline-review-editor")).toHaveCount(0);
|
|
await expect(
|
|
page.getByRole("button", { name: "Save review comment", exact: true }),
|
|
).toBeDisabled();
|
|
}
|
|
function feedbackButton(page: Page) {
|
|
return page.getByRole("button", { name: "Send feedback (1)", exact: true });
|
|
}
|
|
async function openFeedbackWithKeyboard(page: Page) {
|
|
await feedbackButton(page).focus();
|
|
await expectFeedbackFocused(page);
|
|
await reopenFeedbackWithKeyboard(page);
|
|
}
|
|
async function reopenFeedbackWithKeyboard(page: Page) {
|
|
await page.keyboard.press("Enter");
|
|
await expectRecipientMenu(page);
|
|
const recipient = page.getByRole("menuitem", { name: "Composer diff stat", exact: true });
|
|
await recipient.focus();
|
|
await expect(recipient).toBeFocused();
|
|
}
|
|
async function expectFeedbackFocused(page: Page) {
|
|
await expect(feedbackButton(page)).toBeFocused();
|
|
}
|
|
|
|
async function captureWideFeedbackAppearance(page: Page) {
|
|
for (const colorScheme of ["light", "dark"] as const) {
|
|
await page.emulateMedia({ colorScheme });
|
|
await expectSavedReview(page, "Review for second agent");
|
|
await captureFeedback(page, `cta-wide-${colorScheme}-closed`);
|
|
await sendFeedback(page, 1);
|
|
await expectRecipientMenu(page);
|
|
await captureFeedback(page, `cta-wide-${colorScheme}-open`);
|
|
await dismissRecipientMenu(page);
|
|
}
|
|
await page.emulateMedia({ colorScheme: "light" });
|
|
}
|
|
|
|
async function captureFeedback(page: Page, name: string) {
|
|
await page.screenshot({ path: `/tmp/phase2-feedback-${name}.png` });
|
|
}
|
|
|
|
async function closeAgentTab(page: Page, agentId: string) {
|
|
await page.getByTestId(`workspace-tab-agent_${agentId}`).click({ button: "right" });
|
|
await page.getByTestId(`workspace-tab-context-agent_${agentId}-close`).click();
|
|
await expect(page.getByTestId(`workspace-tab-agent_${agentId}`)).toHaveCount(0);
|
|
}
|
|
async function expectFeedbackUnavailable(page: Page) {
|
|
await expect(page.getByRole("button", { name: "Send feedback (1)", exact: true })).toBeDisabled();
|
|
await expect(
|
|
page.getByText("Open an agent tab in this workspace to send feedback.", { exact: true }),
|
|
).toBeVisible();
|
|
}
|
|
async function createReviewRecipient(
|
|
workspace: Awaited<ReturnType<typeof seedChangedAgent>>,
|
|
title: string,
|
|
) {
|
|
return workspace.client.createAgent({
|
|
provider: "mock",
|
|
cwd: workspace.cwd,
|
|
workspaceId: workspace.workspaceId,
|
|
title,
|
|
modeId: "load-test",
|
|
model: "e2e-fast-stream",
|
|
});
|
|
}
|
|
async function expectRecipientMenu(page: Page) {
|
|
for (const name of ["Composer diff stat", "Second reviewer"]) {
|
|
await expect(page.getByRole("menuitem", { name, exact: true })).toBeInViewport({ ratio: 1 });
|
|
}
|
|
await expect(page.getByRole("menuitem")).toHaveCount(2);
|
|
}
|
|
async function dismissRecipientMenu(page: Page) {
|
|
await page.keyboard.press("Escape");
|
|
await expect(page.getByRole("menuitem")).toHaveCount(0);
|
|
}
|
|
async function chooseFeedbackRecipient(page: Page, name: string) {
|
|
await page.getByRole("menuitem", { name, exact: true }).click();
|
|
}
|
|
async function expectFeedbackFailure(page: Page) {
|
|
await expect(
|
|
page.getByText("Requested mock prompt rejection", { exact: true }).first(),
|
|
).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Send feedback (1)", exact: true })).toBeEnabled();
|
|
}
|
|
async function expectFeedbackPending(page: Page, count = 1) {
|
|
await expect(
|
|
page.getByRole("button", { name: `Sending feedback (${count})`, exact: true }),
|
|
).toBeDisabled();
|
|
}
|
|
|
|
async function seedMultiFileReview() {
|
|
const workspace = await seedChangedAgent("review-feedback-navigation-", undefined, [
|
|
{ path: "z-last.ts", content: "export const final = 0;\n" },
|
|
]);
|
|
await writeFile(
|
|
path.join(workspace.cwd, "z-last.ts"),
|
|
Array.from({ length: 120 }, (_, index) => `export const value${index} = ${index};`).join("\n") +
|
|
"\n",
|
|
);
|
|
await workspace.client.checkoutRefresh(workspace.cwd);
|
|
return workspace;
|
|
}
|
|
async function expectFeedbackReachable(page: Page) {
|
|
await expect(page.getByRole("button", { name: "Send feedback (1)", exact: true })).toBeInViewport(
|
|
{ ratio: 1 },
|
|
);
|
|
}
|
|
async function expectReviewNavigationReachable(page: Page) {
|
|
await expectFeedbackReachable(page);
|
|
const jump = page.getByRole("button", { name: "Jump to file", exact: true });
|
|
await expect(jump).toBeInViewport({ ratio: 1 });
|
|
const navigationBounds = await jump.boundingBox();
|
|
const feedbackBounds = await page
|
|
.getByRole("button", { name: "Send feedback (1)", exact: true })
|
|
.boundingBox();
|
|
if (!navigationBounds || !feedbackBounds) throw new Error("Review actions have no bounds");
|
|
expect(feedbackBounds.y + feedbackBounds.height).toBeLessThanOrEqual(navigationBounds.y);
|
|
}
|
|
async function jumpToReviewFile(page: Page, name: string) {
|
|
await page.getByRole("button", { name: "Jump to file", exact: true }).click();
|
|
const sheet = page.getByTestId("changes-jump-to-file-sheet");
|
|
await page
|
|
.getByTestId("changes-file-tree")
|
|
.filter({ visible: true })
|
|
.getByText(name, { exact: true })
|
|
.click();
|
|
await expect(sheet).toHaveCount(0);
|
|
await expect(reviewFileHeader(compactReviewSurface(page), name)).toBeInViewport();
|
|
}
|
|
async function navigateWideReviewFile(page: Page, name: string) {
|
|
const explorer = await ensureExplorerSidebar(page);
|
|
await explorer.getByTestId("changes-tree-panel").getByText(name, { exact: true }).click();
|
|
await expect(reviewFileHeader(page.getByTestId("working-diff-panel"), name)).toBeInViewport();
|
|
}
|
|
|
|
async function setReadmeDiff(
|
|
workspace: Awaited<ReturnType<typeof seedChangedAgent>>,
|
|
changed: boolean,
|
|
) {
|
|
await writeFile(
|
|
path.join(workspace.cwd, "README.md"),
|
|
changed ? "# Temp Repo\nexport const one = 1;\nexport const two = 2;\n" : "# Temp Repo\n",
|
|
);
|
|
await workspace.client.checkoutRefresh(workspace.cwd);
|
|
}
|
|
async function expectUnmatchedFeedback(page: Page) {
|
|
await expect(page.getByRole("button", { name: "Send feedback (0)", exact: true })).toBeDisabled();
|
|
await expect(
|
|
page.getByText("Saved comments no longer match this diff.", { exact: true }),
|
|
).toBeVisible();
|
|
}
|
|
|
|
function compactReviewSurface(page: Page) {
|
|
return page.getByTestId("workspace-explorer-sidebar").filter({ visible: true });
|
|
}
|
|
async function openCompactReviewExplorer(
|
|
page: Page,
|
|
workspace: Awaited<ReturnType<typeof seedChangedAgent>>,
|
|
) {
|
|
await showWideLayout(page);
|
|
await openAgentRoute(page, { workspaceId: workspace.workspaceId, agentId: workspace.agentId });
|
|
await selectAgentTab(page, workspace.agentId);
|
|
await showCompactLayout(page);
|
|
await composerChangesPill(page).click();
|
|
await expect(compactReviewSurface(page).getByTestId("diff-file-0-body")).toBeVisible();
|
|
}
|
|
|
|
function reviewFileHeader(surface: Locator, name: string) {
|
|
return surface.locator(`[data-diff-header-path="${name}"]`).getByTestId(/^diff-file-\d+$/);
|
|
}
|
|
|
|
async function scrollReviewToLastLine(page: Page) {
|
|
const surface = compactReviewSurface(page);
|
|
const bounds = await surface.boundingBox();
|
|
if (!bounds) throw new Error("Compact review has no bounds");
|
|
await page.mouse.move(bounds.x + 120, bounds.y + 200);
|
|
await page.mouse.wheel(0, 10000);
|
|
await expect
|
|
.poll(async () => {
|
|
const body = await surface.getByTestId("diff-file-1-body").boundingBox();
|
|
const feedback = await page
|
|
.getByRole("button", { name: "Send feedback (1)", exact: true })
|
|
.boundingBox();
|
|
if (!body || !feedback) return false;
|
|
return body.y + body.height <= feedback.y;
|
|
})
|
|
.toBe(true);
|
|
}
|
|
|
|
async function revealCompactChanges(page: Page) {
|
|
await page.getByTestId("workspace-explorer-toggle").first().click();
|
|
await page
|
|
.getByTestId("workspace-explorer-sidebar")
|
|
.getByTestId("workspace-tab-changes_tree")
|
|
.click();
|
|
await expect(compactReviewSurface(page).getByTestId("diff-file-0-body")).toBeVisible();
|
|
}
|
|
async function expectReviewEditorBody(page: Page, body: string) {
|
|
await expect(page.getByRole("textbox", { name: "Review comment", exact: true })).toHaveValue(
|
|
body,
|
|
);
|
|
}
|