mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 17:52:49 +00:00
test(app): consolidate overlapping Playwright journeys (#7245)
This commit is contained in:
@@ -2,9 +2,7 @@ import { test, expect } from "../support/fixtures";
|
||||
import type { Locator } from "@playwright/test";
|
||||
import {
|
||||
gotoWorkspace,
|
||||
assertNewChatTileVisible,
|
||||
assertNewTabMenuTriggerVisible,
|
||||
assertSingleNewTabButton,
|
||||
openNewTabMenuWithShortcut,
|
||||
clickNewChat,
|
||||
clickNewTerminal,
|
||||
@@ -126,6 +124,7 @@ test.describe("Tab creation", () => {
|
||||
page,
|
||||
}) => {
|
||||
await gotoWorkspace(page, workspace.workspaceId);
|
||||
await assertNewTabMenuTriggerVisible(page);
|
||||
await pressNewTabShortcut(page);
|
||||
const newTabs = page
|
||||
.locator('[data-testid^="workspace-tab-tab_"]')
|
||||
@@ -190,11 +189,18 @@ test.describe("Tab creation", () => {
|
||||
await expect(agent).toBeFocused();
|
||||
});
|
||||
|
||||
test("clicking new agent tab creates a draft tab", async ({ page }) => {
|
||||
test("the Agent menu creates a draft and shows its composer within the visual budget", async ({
|
||||
page,
|
||||
}) => {
|
||||
await gotoWorkspace(page, workspace.workspaceId);
|
||||
|
||||
await clickNewChat(page);
|
||||
|
||||
const elapsed = await measureTileTransition(
|
||||
page,
|
||||
() => clickNewChat(page),
|
||||
composerLocator(page),
|
||||
10_000,
|
||||
);
|
||||
expect(elapsed).toBeLessThan(3_000);
|
||||
await expectComposerVisible(page);
|
||||
|
||||
const tabsAfter = await getTabTestIds(page);
|
||||
@@ -202,12 +208,17 @@ test.describe("Tab creation", () => {
|
||||
expect(draftCountAfter).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("clicking terminal button creates a standalone terminal", async ({ page }) => {
|
||||
test("the Terminal menu creates a terminal within the visual budget", async ({ page }) => {
|
||||
test.setTimeout(45_000);
|
||||
await gotoWorkspace(page, workspace.workspaceId);
|
||||
|
||||
await clickNewTerminal(page);
|
||||
|
||||
const elapsed = await measureTileTransition(
|
||||
page,
|
||||
() => clickNewTerminal(page),
|
||||
terminalSurfaceLocator(page),
|
||||
20_000,
|
||||
);
|
||||
expect(elapsed).toBeLessThan(5_000);
|
||||
await expectTerminalSurfaceVisible(page);
|
||||
|
||||
const tabsAfter = await getTabTestIds(page);
|
||||
@@ -295,13 +306,6 @@ test.describe("Tab creation", () => {
|
||||
await profileSeed.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("tab bar shows action buttons per pane", async ({ page }) => {
|
||||
await gotoWorkspace(page, workspace.workspaceId);
|
||||
await assertSingleNewTabButton(page);
|
||||
await assertNewChatTileVisible(page);
|
||||
await assertNewTabMenuTriggerVisible(page);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -339,37 +343,4 @@ test.describe("Tab transitions (no flash)", () => {
|
||||
expect(new Set(snapshots.map(tabIdentityKey)).size).toBeLessThanOrEqual(2);
|
||||
await expectTabTitleFits(page, "New Agent", { min: 64, max: 160 });
|
||||
});
|
||||
|
||||
test("Terminal transition completes within visual budget", async ({ page }) => {
|
||||
test.setTimeout(30_000);
|
||||
await gotoWorkspace(page, workspace.workspaceId);
|
||||
|
||||
const elapsed = await measureTileTransition(
|
||||
page,
|
||||
() => clickNewTerminal(page),
|
||||
terminalSurfaceLocator(page),
|
||||
20_000,
|
||||
);
|
||||
|
||||
// Terminal surface should appear within a reasonable budget.
|
||||
// Note: terminal creation involves a server round-trip, so we allow more time
|
||||
// than a pure in-memory transition, but it should still be well under 5 seconds.
|
||||
expect(elapsed).toBeLessThan(5_000);
|
||||
});
|
||||
|
||||
test("New agent tab click shows composer without flash", async ({ page }) => {
|
||||
await gotoWorkspace(page, workspace.workspaceId);
|
||||
|
||||
const elapsed = await measureTileTransition(
|
||||
page,
|
||||
() => clickNewChat(page),
|
||||
composerLocator(page),
|
||||
10_000,
|
||||
);
|
||||
|
||||
// Draft creation is fully in-memory — should be fast
|
||||
// We use a generous budget here because CI can be slow, but the key assertion
|
||||
// is that no blank/flash frame appears (tested above).
|
||||
expect(elapsed).toBeLessThan(3_000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -223,7 +223,10 @@ test.describe("Projects settings", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("user edits worktree setup from the projects page", async ({ page, editableProject }) => {
|
||||
test("edits worktree setup, navigates within the host, and removes a script", async ({
|
||||
page,
|
||||
editableProject,
|
||||
}) => {
|
||||
await openProjects(page);
|
||||
await openProjectSettings(page, editableProject.name);
|
||||
await expectNoUncommittedSetupWarning(page);
|
||||
@@ -236,15 +239,22 @@ test.describe("Projects settings", () => {
|
||||
await returnToProjectsList(page);
|
||||
await openProjectSettings(page, editableProject.name);
|
||||
await expectNoUncommittedSetupWarning(page);
|
||||
});
|
||||
|
||||
test("project navigation stays inside the selected host", async ({ page, editableProject }) => {
|
||||
await openProjects(page);
|
||||
await openProjectSettings(page, editableProject.name);
|
||||
await expectProjectHostContextHidden(page);
|
||||
await returnToProjectsList(page);
|
||||
await openProjectSettings(page, editableProject.name);
|
||||
await expectProjectSettingsHistoryRoundTrip(page, editableProject.name);
|
||||
await test.step("navigation stays inside the selected host", async () => {
|
||||
await expectProjectHostContextHidden(page);
|
||||
await returnToProjectsList(page);
|
||||
await openProjectSettings(page, editableProject.name);
|
||||
await expectProjectSettingsHistoryRoundTrip(page, editableProject.name);
|
||||
});
|
||||
|
||||
await test.step("removing a script leaves the committed setup unchanged", async () => {
|
||||
await expectScriptRowCount(page, 1);
|
||||
await removeProjectScript(page, "dev");
|
||||
await expectScriptRowCount(page, 0);
|
||||
await expectEmptyScriptList(page);
|
||||
await clickSaveProjectSettings(page);
|
||||
await expectNoUncommittedSetupWarning(page);
|
||||
});
|
||||
});
|
||||
|
||||
test("user edits worktree setup on a non-GitHub remote project", async ({
|
||||
@@ -259,18 +269,7 @@ test.describe("Projects settings", () => {
|
||||
await expectProjectConfigSaved(gitlabRemoteProject);
|
||||
});
|
||||
|
||||
test("user renames a project from the edit sheet", async ({ page, editableProject }) => {
|
||||
await openProjects(page);
|
||||
await openProjectSettings(page, editableProject.name);
|
||||
await openProjectEditSheet(page);
|
||||
await fillProjectName(page, "Renamed project");
|
||||
await saveProjectEdits(page);
|
||||
|
||||
await expectProjectEditSaved(page);
|
||||
await expectProjectTitle(page, "Renamed project");
|
||||
});
|
||||
|
||||
test("reopening the edit sheet seeds from the saved project", async ({
|
||||
test("renames a project, reopens the saved edit sheet, and adds an icon", async ({
|
||||
page,
|
||||
editableProject,
|
||||
}) => {
|
||||
@@ -279,26 +278,26 @@ test.describe("Projects settings", () => {
|
||||
await openProjectEditSheet(page);
|
||||
await fillProjectName(page, "Renamed project");
|
||||
await saveProjectEdits(page);
|
||||
|
||||
await expectProjectEditSaved(page);
|
||||
await expectProjectTitle(page, "Renamed project");
|
||||
|
||||
await openProjectEditSheet(page);
|
||||
|
||||
await expectProjectEditName(page, "Renamed project");
|
||||
await expectProjectEditsSaveDisabled(page);
|
||||
});
|
||||
|
||||
test("user picks a custom project icon from a file", async ({ page, editableProject }) => {
|
||||
await openProjects(page);
|
||||
await openProjectSettings(page, editableProject.name);
|
||||
await openProjectEditSheet(page);
|
||||
await chooseProjectIconImage(page, {
|
||||
name: "logo.png",
|
||||
mimeType: "image/png",
|
||||
buffer: PNG_1X1,
|
||||
await test.step("reopening seeds the saved name with no unsaved changes", async () => {
|
||||
await openProjectEditSheet(page);
|
||||
await expectProjectEditName(page, "Renamed project");
|
||||
await expectProjectEditsSaveDisabled(page);
|
||||
});
|
||||
await saveProjectEdits(page);
|
||||
|
||||
await expectProjectEditSaved(page);
|
||||
await test.step("an icon can be saved without another name change", async () => {
|
||||
await chooseProjectIconImage(page, {
|
||||
name: "logo.png",
|
||||
mimeType: "image/png",
|
||||
buffer: PNG_1X1,
|
||||
});
|
||||
await saveProjectEdits(page);
|
||||
await expectProjectEditSaved(page);
|
||||
await expectProjectTitle(page, "Renamed project");
|
||||
});
|
||||
});
|
||||
|
||||
test("user sets a project name and icon in one save", async ({ page, editableProject }) => {
|
||||
@@ -459,31 +458,4 @@ test.describe("Projects settings — error UX", () => {
|
||||
|
||||
await expectNoEditableTarget(page);
|
||||
});
|
||||
|
||||
test("project detail does not render a second host selector", async ({
|
||||
page,
|
||||
editableProject,
|
||||
}) => {
|
||||
await openProjects(page);
|
||||
await openProjectSettings(page, editableProject.name);
|
||||
|
||||
await expectProjectHostContextHidden(page);
|
||||
});
|
||||
|
||||
test("script removal via kebab menu removes the row from the form", async ({
|
||||
page,
|
||||
editableProject,
|
||||
}) => {
|
||||
await openProjects(page);
|
||||
await openProjectSettings(page, editableProject.name);
|
||||
|
||||
await expectScriptRowCount(page, 1);
|
||||
|
||||
await removeProjectScript(page, "dev");
|
||||
|
||||
await expectScriptRowCount(page, 0);
|
||||
await expectEmptyScriptList(page);
|
||||
await clickSaveProjectSettings(page);
|
||||
await expectNoUncommittedSetupWarning(page);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,28 +116,12 @@ async function exerciseProviderSettingsStack(page: Page) {
|
||||
|
||||
await page.getByRole("button", { name: "Refresh", exact: true }).click();
|
||||
await expectProviderSettingsVisible(page);
|
||||
await expect(page.getByText("Updated just now", { exact: true })).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
test.describe("provider settings overlay stack", () => {
|
||||
test("provider model update age says just now", async ({ page }) => {
|
||||
const session = await seedMockAgentWorkspace({
|
||||
repoPrefix: "provider-relative-time-",
|
||||
title: "Provider relative time",
|
||||
});
|
||||
try {
|
||||
await page.clock.install({ time: Date.now() });
|
||||
await page.setViewportSize(MOBILE_VIEWPORT);
|
||||
await openAgentRoute(page, session);
|
||||
await expectComposerVisible(page);
|
||||
await openProviderSettingsFromModelSelector(page);
|
||||
await expect(page.getByText("Updated just now", { exact: true })).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("provider settings covers the desktop model selector without closing it", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import { expect, test } from "../support/fixtures";
|
||||
import { clickNewChat, clickNewTerminal, gotoWorkspace } from "../support/helpers/launcher";
|
||||
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
|
||||
import {
|
||||
expectTerminalSurfaceVisible,
|
||||
focusTerminalSurface,
|
||||
typeInTerminal,
|
||||
setupDeterministicPrompt,
|
||||
waitForTerminalContent,
|
||||
} from "../support/helpers/terminal-perf";
|
||||
|
||||
interface CreatedAgentCwdAssertion {
|
||||
workspaceId: string;
|
||||
cwd: string | null;
|
||||
}
|
||||
|
||||
async function fetchSingleAgentForWorkspace(
|
||||
workspace: SeededWorkspace,
|
||||
): Promise<CreatedAgentCwdAssertion | null> {
|
||||
const agents = (await workspace.client.fetchAgents({ scope: "active" })).entries
|
||||
.map((entry) => entry.agent)
|
||||
.filter((agent) => agent.workspaceId === workspace.workspaceId);
|
||||
if (agents.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
const [agent] = agents;
|
||||
return {
|
||||
workspaceId: workspace.workspaceId,
|
||||
cwd: agent.cwd,
|
||||
};
|
||||
}
|
||||
|
||||
test.describe("Workspace cwd correctness", () => {
|
||||
test("main checkout workspace opens terminals in the project root", async ({
|
||||
page,
|
||||
withWorkspace,
|
||||
}) => {
|
||||
test.setTimeout(60_000);
|
||||
|
||||
const workspace = await withWorkspace({ prefix: "workspace-cwd-main-" });
|
||||
await workspace.navigateTo();
|
||||
await clickNewTerminal(page);
|
||||
|
||||
await expectTerminalSurfaceVisible(page);
|
||||
await focusTerminalSurface(page);
|
||||
await setupDeterministicPrompt(page, `PWD_READY_${Date.now()}`);
|
||||
await typeInTerminal(page, "pwd\n");
|
||||
await waitForTerminalContent(page, (text) => text.includes(workspace.repoPath), 10_000);
|
||||
});
|
||||
|
||||
test("draft tab creates an agent in the workspace cwd", async ({ page }) => {
|
||||
test.setTimeout(60_000);
|
||||
|
||||
const workspace = await seedWorkspace({ repoPrefix: "workspace-cwd-draft-agent-" });
|
||||
try {
|
||||
await gotoWorkspace(page, workspace.workspaceId);
|
||||
|
||||
await clickNewChat(page);
|
||||
const composer = page.getByRole("textbox", { name: "Message agent..." }).first();
|
||||
const message = `cwd draft create ${Date.now()}`;
|
||||
await expect(composer).toBeEditable({ timeout: 15_000 });
|
||||
await composer.fill(message);
|
||||
await composer.press("Enter");
|
||||
await expect(page.getByText(message, { exact: true }).first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
|
||||
await expect(page.locator('[data-testid^="workspace-tab-agent_"]').first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
|
||||
await expect
|
||||
.poll(() => fetchSingleAgentForWorkspace(workspace), { timeout: 30_000 })
|
||||
.toEqual({
|
||||
workspaceId: workspace.workspaceId,
|
||||
cwd: workspace.repoPath,
|
||||
});
|
||||
} finally {
|
||||
await workspace.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("worktree workspace opens terminals in the worktree directory", async ({
|
||||
page,
|
||||
withWorkspace,
|
||||
}) => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
const workspace = await withWorkspace({ worktree: true, prefix: "workspace-cwd-worktree-" });
|
||||
await workspace.navigateTo();
|
||||
await clickNewTerminal(page);
|
||||
|
||||
await expectTerminalSurfaceVisible(page);
|
||||
await focusTerminalSurface(page);
|
||||
await setupDeterministicPrompt(page, `PWD_READY_${Date.now()}`);
|
||||
await typeInTerminal(page, "pwd\n");
|
||||
await waitForTerminalContent(page, (text) => text.includes(workspace.repoPath), 10_000);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
createAgentChatFromLauncher,
|
||||
createStandaloneTerminalFromLauncher,
|
||||
expectTerminalCwd,
|
||||
submitAgentChatAndExpectCwd,
|
||||
} from "../support/helpers/workspace-lifecycle";
|
||||
|
||||
test.describe("Workspace lifecycle", () => {
|
||||
@@ -19,8 +20,9 @@ test.describe("Workspace lifecycle", () => {
|
||||
const workspace = await withWorkspace({ prefix: "lifecycle-main-" });
|
||||
await workspace.navigateTo();
|
||||
|
||||
await test.step("creates an agent chat", async () => {
|
||||
await test.step("submitting a draft creates an agent in the project root", async () => {
|
||||
await createAgentChatFromLauncher(page);
|
||||
await submitAgentChatAndExpectCwd(page, workspace);
|
||||
});
|
||||
|
||||
await test.step("creates a terminal in the project root", async () => {
|
||||
|
||||
@@ -80,13 +80,6 @@ export async function pressDirectNewTabShortcut(page: Page, key: string): Promis
|
||||
|
||||
// ─── Tab bar assertions ───────────────────────────────────────────────────
|
||||
|
||||
/** Assert the inline plus button is visible in the tab bar. */
|
||||
export async function assertNewChatTileVisible(page: Page): Promise<void> {
|
||||
await expect(
|
||||
page.getByTestId("workspace-new-tab-button").filter({ visible: true }).first(),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
/** Assert the New tab button is visible in the tab bar. */
|
||||
export async function assertNewTabMenuTriggerVisible(page: Page): Promise<void> {
|
||||
await expect(
|
||||
@@ -132,13 +125,6 @@ export async function waitForTabWithTitle(
|
||||
).toBeVisible({ timeout });
|
||||
}
|
||||
|
||||
/** Assert the inline plus button is visible in the tab bar. */
|
||||
export async function assertSingleNewTabButton(page: Page): Promise<void> {
|
||||
const buttons = page.getByTestId("workspace-new-tab-button").filter({ visible: true });
|
||||
const count = await buttons.count();
|
||||
expect(count).toBeGreaterThanOrEqual(1);
|
||||
}
|
||||
|
||||
// ─── No-flash measurement ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import { clickNewChat, clickNewTerminal } from "./launcher";
|
||||
import { connectSeedClient } from "./seed-client";
|
||||
import { setupDeterministicPrompt, waitForTerminalContent } from "./terminal-perf";
|
||||
|
||||
function terminalSurface(page: Page) {
|
||||
@@ -30,3 +31,35 @@ export async function createAgentChatFromLauncher(page: Page): Promise<void> {
|
||||
await expect(composerInput(page)).toBeEditable({ timeout: 15_000 });
|
||||
await expect(page.getByTestId("agent-loading")).toHaveCount(0);
|
||||
}
|
||||
|
||||
export async function submitAgentChatAndExpectCwd(
|
||||
page: Page,
|
||||
workspace: { workspaceId: string; repoPath: string },
|
||||
): Promise<void> {
|
||||
const client = await connectSeedClient();
|
||||
try {
|
||||
const message = "cwd draft create";
|
||||
await composerInput(page).fill(message);
|
||||
await composerInput(page).press("Enter");
|
||||
await expect(page.getByText(message, { exact: true }).first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(page.locator('[data-testid^="workspace-tab-agent_"]').first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const agents = await client.fetchAgents({ scope: "active" });
|
||||
return agents.entries
|
||||
.map((entry) => entry.agent)
|
||||
.filter((agent) => agent.workspaceId === workspace.workspaceId)
|
||||
.map((agent) => ({ workspaceId: agent.workspaceId, cwd: agent.cwd }));
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toEqual([{ workspaceId: workspace.workspaceId, cwd: workspace.repoPath }]);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user