mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 07:33:18 +00:00
Keep compact sidebars open while users inspect Search, Import session, Add project, or file sheets. Reveal the resolved destination when navigation commits, and place compact workspace sheets above both sidebars while preserving shared editing and cache state.
274 lines
11 KiB
TypeScript
274 lines
11 KiB
TypeScript
import type { Page } from "@playwright/test";
|
|
import { test as base, expect } from "../support/fixtures";
|
|
import { gotoAppShell } from "../support/helpers/app";
|
|
import {
|
|
openAgentRoute,
|
|
seedMockAgentWorkspace,
|
|
type MockAgentWorkspace,
|
|
} from "../support/helpers/mock-agent";
|
|
import { expectOpenTabCount } from "../support/helpers/sheet";
|
|
import { getServerId } from "../support/helpers/server-id";
|
|
import { closeMobileAgentSidebar, openMobileAgentSidebar } from "../support/helpers/sidebar";
|
|
|
|
interface FinishedWorkspaces {
|
|
subject: MockAgentWorkspace;
|
|
other: MockAgentWorkspace;
|
|
}
|
|
|
|
const test = base.extend<{ workspaces: FinishedWorkspaces }>({
|
|
workspaces: async ({ browserName: _browserName }, provide) => {
|
|
const seeded: MockAgentWorkspace[] = [];
|
|
async function finishedWorkspace(title: string) {
|
|
const workspace = await seedMockAgentWorkspace({
|
|
repoPrefix: "workspace-mark-unread-",
|
|
title,
|
|
initialPrompt: "Finish this turn.",
|
|
});
|
|
seeded.push(workspace);
|
|
await workspace.client.waitForFinish(workspace.agentId, 20_000);
|
|
await workspace.client.clearWorkspaceAttention(workspace.workspaceId);
|
|
return workspace;
|
|
}
|
|
try {
|
|
await provide({
|
|
subject: await finishedWorkspace("Unread subject"),
|
|
other: await finishedWorkspace("Other workspace"),
|
|
});
|
|
} finally {
|
|
for (const workspace of seeded) await workspace.cleanup();
|
|
}
|
|
},
|
|
});
|
|
|
|
function workspaceRow(page: Page, workspaceId: string) {
|
|
return page.getByTestId(`sidebar-workspace-row-${getServerId()}:${workspaceId}`);
|
|
}
|
|
|
|
async function openWorkspace(page: Page, workspaceId: string) {
|
|
await workspaceRow(page, workspaceId).click();
|
|
await expect(page).toHaveURL(new RegExp(`/workspace/${workspaceId}`));
|
|
}
|
|
|
|
async function chooseReadAction(page: Page, workspaceId: string, action: "read" | "unread") {
|
|
await workspaceRow(page, workspaceId).hover();
|
|
await page.getByTestId(`sidebar-workspace-kebab-${getServerId()}:${workspaceId}`).click();
|
|
const item = page.getByRole("menuitem", { name: `Mark as ${action}`, exact: true });
|
|
await expect(item).toBeVisible();
|
|
await item.click();
|
|
}
|
|
|
|
async function markAsUnread(page: Page, workspaceId: string) {
|
|
await chooseReadAction(page, workspaceId, "unread");
|
|
await expectStatus(page, workspaceId, "attention");
|
|
}
|
|
|
|
async function markAsRead(page: Page, workspaceId: string) {
|
|
await chooseReadAction(page, workspaceId, "read");
|
|
await expectStatus(page, workspaceId, "done");
|
|
}
|
|
|
|
async function expectStatus(page: Page, workspaceId: string, status: "done" | "attention") {
|
|
await expect(
|
|
workspaceRow(page, workspaceId).getByTestId(`workspace-status-indicator-${status}`),
|
|
).toBeVisible();
|
|
}
|
|
|
|
async function markBackgroundWorkspaceAndReopen(page: Page, workspaceId: string) {
|
|
await test.step("background workspace gains green dot and clears when clicked", async () => {
|
|
await markAsUnread(page, workspaceId);
|
|
await openWorkspace(page, workspaceId);
|
|
await expectStatus(page, workspaceId, "done");
|
|
});
|
|
}
|
|
|
|
async function leaveMarkedWorkspaceAndRead(page: Page, { subject, other }: FinishedWorkspaces) {
|
|
await test.step("leaving preserves manual unread until Mark as read", async () => {
|
|
await markAsUnread(page, subject.workspaceId);
|
|
await openWorkspace(page, other.workspaceId);
|
|
await markAsRead(page, subject.workspaceId);
|
|
await openWorkspace(page, subject.workspaceId);
|
|
});
|
|
}
|
|
|
|
async function leaveMarkedWorkspaceAndReopen(page: Page, { subject, other }: FinishedWorkspaces) {
|
|
await test.step("leaving preserves manual unread until reopening", async () => {
|
|
await markAsUnread(page, subject.workspaceId);
|
|
await openWorkspace(page, other.workspaceId);
|
|
await expectStatus(page, subject.workspaceId, "attention");
|
|
await openWorkspace(page, subject.workspaceId);
|
|
await expectStatus(page, subject.workspaceId, "done");
|
|
});
|
|
}
|
|
|
|
async function completeTurnAndLeave(page: Page, { subject, other }: FinishedWorkspaces) {
|
|
await test.step("ordinary completion still clears on departure", async () => {
|
|
await subject.client.sendAgentMessage(subject.agentId, "Finish another turn.");
|
|
await subject.client.waitForFinish(subject.agentId, 20_000);
|
|
await expectStatus(page, subject.workspaceId, "attention");
|
|
await openWorkspace(page, other.workspaceId);
|
|
await expectStatus(page, subject.workspaceId, "done");
|
|
});
|
|
}
|
|
|
|
async function openWorkspaceOnCompact(page: Page, workspaceId: string) {
|
|
await openWorkspace(page, workspaceId);
|
|
await openMobileAgentSidebar(page);
|
|
}
|
|
|
|
async function leaveMarkedWorkspaceAndReadOnCompact(
|
|
page: Page,
|
|
{ subject, other }: FinishedWorkspaces,
|
|
) {
|
|
await test.step("leaving preserves manual unread until Mark as read", async () => {
|
|
await markAsUnread(page, subject.workspaceId);
|
|
await openWorkspaceOnCompact(page, other.workspaceId);
|
|
await markAsRead(page, subject.workspaceId);
|
|
await openWorkspaceOnCompact(page, subject.workspaceId);
|
|
});
|
|
}
|
|
|
|
async function leaveMarkedWorkspaceAndReopenOnCompact(
|
|
page: Page,
|
|
{ subject, other }: FinishedWorkspaces,
|
|
) {
|
|
await test.step("leaving preserves manual unread until reopening", async () => {
|
|
await markAsUnread(page, subject.workspaceId);
|
|
await openWorkspaceOnCompact(page, other.workspaceId);
|
|
await expectStatus(page, subject.workspaceId, "attention");
|
|
await openWorkspaceOnCompact(page, subject.workspaceId);
|
|
await expectStatus(page, subject.workspaceId, "done");
|
|
});
|
|
}
|
|
|
|
async function completeTurnAndLeaveOnCompact(page: Page, { subject, other }: FinishedWorkspaces) {
|
|
await test.step("ordinary completion still clears on departure", async () => {
|
|
await closeMobileAgentSidebar(page);
|
|
await subject.client.sendAgentMessage(subject.agentId, "Finish another turn.");
|
|
await subject.client.waitForFinish(subject.agentId, 20_000);
|
|
await openMobileAgentSidebar(page);
|
|
await expectStatus(page, subject.workspaceId, "attention");
|
|
await openWorkspaceOnCompact(page, other.workspaceId);
|
|
await expectStatus(page, subject.workspaceId, "done");
|
|
});
|
|
}
|
|
|
|
async function markBackgroundWorkspaceAndRead(page: Page, workspaceId: string) {
|
|
await test.step("explicit Mark as read clears a background workspace", async () => {
|
|
await markAsUnread(page, workspaceId);
|
|
await markAsRead(page, workspaceId);
|
|
});
|
|
}
|
|
|
|
async function expectSelectedAgent(page: Page, agentId: string) {
|
|
await expect(page.getByTestId(`workspace-tab-agent_${agentId}`).first()).toHaveAttribute(
|
|
"aria-selected",
|
|
"true",
|
|
);
|
|
}
|
|
|
|
async function addFinishedAgent(workspace: MockAgentWorkspace) {
|
|
return workspace.client.createAgent({
|
|
provider: "mock",
|
|
cwd: workspace.cwd,
|
|
workspaceId: workspace.workspaceId,
|
|
title: "Newest agent",
|
|
modeId: "load-test",
|
|
model: "e2e-fast-stream",
|
|
});
|
|
}
|
|
|
|
async function openCompactWorkspace(page: Page, workspace: MockAgentWorkspace) {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await openAgentRoute(page, workspace);
|
|
await expect(page.getByText("Finish this turn.", { exact: true })).toBeVisible();
|
|
await openMobileAgentSidebar(page);
|
|
}
|
|
|
|
async function markUnreadThenResumeChat(page: Page, workspaceId: string) {
|
|
await test.step("interacting with the visible chat clears manual unread", async () => {
|
|
await markAsUnread(page, workspaceId);
|
|
await closeMobileAgentSidebar(page);
|
|
await page.getByRole("textbox", { name: "Message agent..." }).click();
|
|
await openMobileAgentSidebar(page);
|
|
await expectStatus(page, workspaceId, "done");
|
|
});
|
|
}
|
|
|
|
test("manual unread survives departure and clears on reopening without changing normal completions", async ({
|
|
page,
|
|
workspaces,
|
|
}) => {
|
|
await gotoAppShell(page);
|
|
await openWorkspace(page, workspaces.other.workspaceId);
|
|
await markBackgroundWorkspaceAndReopen(page, workspaces.subject.workspaceId);
|
|
await leaveMarkedWorkspaceAndRead(page, workspaces);
|
|
await leaveMarkedWorkspaceAndReopen(page, workspaces);
|
|
await completeTurnAndLeave(page, workspaces);
|
|
await markBackgroundWorkspaceAndRead(page, workspaces.subject.workspaceId);
|
|
});
|
|
|
|
test("clicking a multi-agent workspace reveals and clears its marked agent", async ({
|
|
page,
|
|
workspaces,
|
|
}) => {
|
|
await openAgentRoute(page, workspaces.subject);
|
|
await expectSelectedAgent(page, workspaces.subject.agentId);
|
|
await openWorkspace(page, workspaces.other.workspaceId);
|
|
const newest = await addFinishedAgent(workspaces.subject);
|
|
await markBackgroundWorkspaceAndReopen(page, workspaces.subject.workspaceId);
|
|
await expectSelectedAgent(page, newest.id);
|
|
});
|
|
|
|
test("manual unread survives leaving the current workspace on compact layout", async ({
|
|
page,
|
|
workspaces,
|
|
}) => {
|
|
await openCompactWorkspace(page, workspaces.subject);
|
|
await leaveMarkedWorkspaceAndReadOnCompact(page, workspaces);
|
|
await leaveMarkedWorkspaceAndReopenOnCompact(page, workspaces);
|
|
await markUnreadThenResumeChat(page, workspaces.subject.workspaceId);
|
|
await completeTurnAndLeaveOnCompact(page, workspaces);
|
|
});
|
|
|
|
async function reopenMarkedWorkspaceAfterBackgroundTurn(page: Page, workspace: MockAgentWorkspace) {
|
|
const route = page.url();
|
|
// End this app session before the turn, so its cached transcript cannot contain the new prompt.
|
|
await page.goto("about:blank");
|
|
await workspace.client.sendAgentMessage(
|
|
workspace.agentId,
|
|
"A turn completed while the app was closed.",
|
|
);
|
|
await workspace.client.waitForFinish(workspace.agentId, 20_000);
|
|
// Reopen the ordinary workspace route, without an agent open intent that would repair focus.
|
|
await page.goto(route);
|
|
await expect(page.getByRole("button", { name: /Switch tabs/ })).toBeVisible();
|
|
}
|
|
|
|
async function expectRestoredCompactChat(page: Page) {
|
|
await expectOpenTabCount(page, 1);
|
|
await expect(
|
|
page.getByText("A turn completed while the app was closed.", { exact: true }),
|
|
).toBeVisible();
|
|
}
|
|
|
|
test("reopening a marked unread workspace restores only its agent tab and current history on compact layout", async ({
|
|
page,
|
|
workspaces,
|
|
}) => {
|
|
test.setTimeout(120_000);
|
|
await test.step("open a finished chat with one main tab", async () => {
|
|
await openCompactWorkspace(page, workspaces.subject);
|
|
await closeMobileAgentSidebar(page);
|
|
await expectOpenTabCount(page, 1);
|
|
await expect(page.getByText("Finish this turn.", { exact: true })).toBeVisible();
|
|
});
|
|
await test.step("mark the current workspace unread, then close and reopen the app", async () => {
|
|
await openMobileAgentSidebar(page);
|
|
await markAsUnread(page, workspaces.subject.workspaceId);
|
|
await reopenMarkedWorkspaceAfterBackgroundTurn(page, workspaces.subject);
|
|
});
|
|
await test.step("restore the main tab without Explorer entries and load its latest history", async () => {
|
|
await expectRestoredCompactChat(page);
|
|
});
|
|
});
|