refactor: unify workspace tab management and add runtime metrics

This commit is contained in:
Mohamed Boudra
2026-03-05 14:21:16 +07:00
parent 6dc49ccbc7
commit 8371ea0c7d
32 changed files with 1064 additions and 461 deletions
+9 -27
View File
@@ -1,44 +1,26 @@
import { test, expect } from './fixtures';
import { createAgent, ensureHostSelected, gotoHome, setWorkingDirectory } from './helpers/app';
import { createAgentInRepo } from './helpers/app';
import { createTempGitRepo } from './helpers/workspace';
function parseAgentUrl(url: string): { serverId: string; agentId: string } {
const parsed = new URL(url);
const match = parsed.pathname.match(/\/h\/([^/]+)\/agent\/([^/?#]+)/);
if (!match) {
throw new Error(`Expected /h/:serverId/agent/:agentId URL, got ${url}`);
}
return {
serverId: decodeURIComponent(match[1]),
agentId: decodeURIComponent(match[2]),
};
}
test('create agent in a temp repo', async ({ page }) => {
const repo = await createTempGitRepo();
const prompt = "Respond with exactly: Hello";
try {
await gotoHome(page);
await setWorkingDirectory(page, repo.path);
await ensureHostSelected(page);
await createAgent(page, prompt);
await createAgentInRepo(page, { directory: repo.path, prompt });
// Verify user message is shown in the stream
await expect(page.getByText(prompt, { exact: true })).toBeVisible();
// Verify we used the seeded fast model (do not fall back to other defaults).
await page.getByTestId('agent-overflow-menu').click();
await expect(page.getByText('Model', { exact: true })).toBeVisible();
await expect(
page.getByTestId('agent-overflow-content').getByText(/gpt-5\.1-codex-mini/i)
).toBeVisible();
const modelPicker = page.getByRole("button", { name: /select agent model/i }).first();
await expect(modelPicker).toBeVisible({ timeout: 30000 });
await expect(modelPicker).toContainText(/gpt-5\.1-codex-mini/i);
// Verify the created agent's title reflects the response.
const { serverId, agentId } = parseAgentUrl(page.url());
const agentRow = page.getByTestId(`agent-row-${serverId}-${agentId}`).first();
await expect(agentRow).not.toContainText(/new agent/i, { timeout: 30000 });
await expect(agentRow).toContainText(/hello|greet|response/i, { timeout: 30000 });
// Verify the assistant response is rendered.
await expect(page.getByText("Hello", { exact: true }).first()).toBeVisible({
timeout: 30000,
});
} finally {
await repo.cleanup();
}
+16 -1
View File
@@ -158,7 +158,12 @@ export const gotoHome = async (page: Page) => {
await page.goto('/');
await ensureE2EStorageSeeded(page);
await expect(page.getByText('New agent', { exact: true }).first()).toBeVisible();
await expect(page.getByRole('textbox', { name: 'Message agent...' })).toBeVisible();
const composer = page.getByRole('textbox', { name: 'Message agent...' });
if (!(await composer.first().isVisible().catch(() => false))) {
const newAgentButton = page.getByText('New agent', { exact: true }).first();
await newAgentButton.click();
}
await expect(composer.first()).toBeVisible({ timeout: 30000 });
};
export const openSettings = async (page: Page) => {
@@ -585,6 +590,16 @@ export const createAgentWithConfig = async (page: Page, config: AgentConfig) =>
await createAgent(page, config.prompt);
};
export const createAgentInRepo = async (
page: Page,
config: Pick<AgentConfig, 'directory' | 'prompt'>
) => {
await gotoHome(page);
await ensureHostSelected(page);
await setWorkingDirectory(page, config.directory);
await createAgent(page, config.prompt);
};
export const waitForPermissionPrompt = async (page: Page, timeout = 30000) => {
const promptText = page.getByTestId('permission-request-question').first();
await expect(promptText).toBeVisible({ timeout });
@@ -0,0 +1,52 @@
import { expect, type Page } from "@playwright/test";
export async function getWorkspaceTabTestIds(page: Page): Promise<string[]> {
const tabs = page.locator('[data-testid^="workspace-tab-"]');
const count = await tabs.count();
const ids: string[] = [];
for (let index = 0; index < count; index += 1) {
const testId = await tabs.nth(index).getAttribute("data-testid");
if (testId && !ids.includes(testId)) {
ids.push(testId);
}
}
return ids;
}
export async function waitForWorkspaceTabsVisible(page: Page): Promise<void> {
await expect(page.getByTestId("workspace-tabs-row").first()).toBeVisible({
timeout: 30_000,
});
await expect(page.getByTestId("workspace-new-agent-tab").first()).toBeVisible({
timeout: 30_000,
});
}
export async function ensureWorkspaceAgentPaneVisible(page: Page): Promise<void> {
const toggle = page.getByTestId("workspace-explorer-toggle").first();
if (!(await toggle.isVisible().catch(() => false))) {
return;
}
const isExpanded = (await toggle.getAttribute("aria-expanded")) === "true";
if (isExpanded) {
await toggle.click();
await expect(toggle).toHaveAttribute("aria-expanded", "false", {
timeout: 10_000,
});
}
}
export async function sampleWorkspaceTabIds(
page: Page,
options: { durationMs?: number; intervalMs?: number } = {}
): Promise<string[][]> {
const durationMs = options.durationMs ?? 2_500;
const intervalMs = options.intervalMs ?? 50;
const snapshots: string[][] = [];
const start = Date.now();
while (Date.now() - start <= durationMs) {
snapshots.push(await getWorkspaceTabTestIds(page));
await page.waitForTimeout(intervalMs);
}
return snapshots;
}
+60 -10
View File
@@ -1,24 +1,74 @@
import { expect, type Page } from "@playwright/test";
import { expect, type Page } from '@playwright/test';
import { buildHostWorkspaceRoute } from '@/utils/host-routes';
import { gotoHome } from './app';
export async function openNewAgentComposer(page: Page): Promise<void> {
await page.goto("/");
await gotoHome(page);
}
const sidebarNewAgent = page.getByTestId("sidebar-new-agent").first();
if (await sidebarNewAgent.isVisible().catch(() => false)) {
await sidebarNewAgent.click();
} else {
await page.getByText("New agent", { exact: true }).first().click();
export function workspaceLabelFromPath(value: string): string {
const normalized = value.replace(/\\/g, '/').replace(/\/+$/, '');
const parts = normalized.split('/').filter(Boolean);
return parts[parts.length - 1] ?? normalized;
}
function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function candidateWorkspaceIds(inputPath: string): string[] {
const trimmed = inputPath.replace(/\/+$/, '');
const candidates = new Set<string>([trimmed]);
if (trimmed.startsWith('/var/')) {
candidates.add(`/private${trimmed}`);
}
if (trimmed.startsWith('/private/var/')) {
candidates.add(trimmed.replace(/^\/private/, ''));
}
return Array.from(candidates);
}
await expect(page.getByRole("textbox", { name: "Message agent..." })).toBeVisible({
function workspaceRowLocator(page: Page, serverId: string, workspacePath: string) {
const ids = candidateWorkspaceIds(workspacePath).map(
(id) => `[data-testid="sidebar-workspace-row-${serverId}:${id}"]`
);
return page.locator(ids.join(',')).first();
}
export async function switchWorkspaceViaSidebar(input: {
page: Page;
serverId: string;
targetWorkspacePath: string;
}): Promise<void> {
const row = workspaceRowLocator(input.page, input.serverId, input.targetWorkspacePath);
await expect(row).toBeVisible({ timeout: 30_000 });
await row.click();
const targetWorkspaceRoute = buildHostWorkspaceRoute(input.serverId, input.targetWorkspacePath);
await expect(input.page).toHaveURL(new RegExp(escapeRegex(targetWorkspaceRoute)), {
timeout: 30_000,
});
}
export async function expectWorkspaceHeader(
page: Page,
input: { title: string; subtitle: string }
): Promise<void> {
const titleLocator = page.getByTestId('workspace-header-title');
const subtitleLocator = page.getByTestId('workspace-header-subtitle');
await expect(titleLocator.first()).toHaveText(input.title, {
timeout: 30_000,
});
await expect(subtitleLocator.first()).toHaveText(input.subtitle, {
timeout: 30_000,
});
}
export async function seedWorkspaceActivity(page: Page, marker: string): Promise<void> {
const input = page.getByRole("textbox", { name: "Message agent..." });
const input = page.getByRole('textbox', { name: 'Message agent...' });
await expect(input).toBeEditable({ timeout: 30_000 });
await input.fill(marker);
await input.press("Enter");
await input.press('Enter');
await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 });
}
+8 -1
View File
@@ -65,6 +65,13 @@ test('new agent respects serverId in the URL', async ({ page }) => {
await page.goto(`/?serverId=${encodeURIComponent(serverId)}`);
await expect(page.getByText('New agent', { exact: true }).first()).toBeVisible();
const newAgentButton = page.getByTestId('sidebar-new-agent').first();
if (await newAgentButton.isVisible().catch(() => false)) {
await newAgentButton.click();
} else {
await page.getByText('New agent', { exact: true }).first().click();
}
const input = page.getByRole('textbox', { name: 'Message agent...' });
await expect(input).toBeEditable({ timeout: 30000 });
});
@@ -95,7 +102,7 @@ test('new agent auto-selects first online host when no preference is stored', as
updatedAt: nowIso,
};
await page.goto('/');
await gotoHome(page);
await page.evaluate(
({ daemon }) => {
const nonce = localStorage.getItem('@paseo:e2e-seed-nonce') ?? '1';
@@ -1,5 +1,6 @@
import { test, expect } from './fixtures';
import { Buffer } from 'node:buffer';
import { gotoHome, openSettings } from './helpers/app';
function encodeBase64Url(input: string): string {
return Buffer.from(input, 'utf8')
@@ -20,7 +21,8 @@ test('pairing flow accepts #offer=ConnectionOfferV2 and stores relay-only host',
}
// Override the default fixture seeding for this test.
await page.goto('/settings');
await gotoHome(page);
await openSettings(page);
await page.evaluate(() => {
const nonce = localStorage.getItem('@paseo:e2e-seed-nonce') ?? '1';
localStorage.setItem('@paseo:e2e-disable-default-seed-once', nonce);
@@ -1,4 +1,5 @@
import { test, expect } from './fixtures';
import { gotoHome, openSettings } from './helpers/app';
test('connects via relay when direct endpoints fail', async ({ page }) => {
const relayPort = process.env.E2E_RELAY_PORT;
@@ -26,7 +27,8 @@ test('connects via relay when direct endpoints fail', async ({ page }) => {
};
// Override the default fixture seeding for this test.
await page.goto('/settings');
await gotoHome(page);
await openSettings(page);
await page.evaluate((daemon) => {
const nonce = localStorage.getItem('@paseo:e2e-seed-nonce') ?? '1';
localStorage.setItem('@paseo:e2e-disable-default-seed-once', nonce);
+7 -2
View File
@@ -1,4 +1,5 @@
import { test, expect } from './fixtures';
import { gotoHome, openSettings } from './helpers/app';
test('relay connection stays stable across multiple tabs', async ({ page }) => {
const relayPort = process.env.E2E_RELAY_PORT;
@@ -26,7 +27,8 @@ test('relay connection stays stable across multiple tabs', async ({ page }) => {
};
// Use relay by making the direct endpoint intentionally fail.
await page.goto('/settings');
await gotoHome(page);
await openSettings(page);
await page.evaluate((daemon) => {
const nonce = localStorage.getItem('@paseo:e2e-seed-nonce') ?? '1';
localStorage.setItem('@paseo:e2e-disable-default-seed-once', nonce);
@@ -45,7 +47,10 @@ test('relay connection stays stable across multiple tabs', async ({ page }) => {
await page2.routeWebSocket(/:(6767)\b/, async (ws) => {
await ws.close({ code: 1008, reason: 'Blocked connection to localhost:6767 during e2e.' });
});
await page2.goto('/settings');
await page2.goto('/');
const settingsButton2 = page2.locator('[data-testid="sidebar-settings"]:visible').first();
await expect(settingsButton2).toBeVisible({ timeout: 20000 });
await settingsButton2.click();
const card2 = page2.getByTestId(`daemon-card-${serverId}`);
await expect(card2.getByText('Relay', { exact: true })).toBeVisible({ timeout: 20000 });
await expect(card2.getByText('Online', { exact: true })).toBeVisible({ timeout: 20000 });
-3
View File
@@ -75,7 +75,6 @@ async function openNewAgentDraft(page: Page): Promise<void> {
const newAgentButton = page.getByTestId("sidebar-new-agent").first();
await expect(newAgentButton).toBeVisible({ timeout: 30000 });
await newAgentButton.click();
await expect(page).toHaveURL(/\/h\/[^/]+\/agent(\?|$)/, { timeout: 30000 });
await expect(
page.locator('[data-testid="working-directory-select"]:visible').first()
).toBeVisible({
@@ -385,9 +384,7 @@ test("mobile terminal tab switch keeps command input routed to the selected tab"
await setWorkingDirectory(page, repo.path);
await ensureHostSelected(page);
await createAgent(page, "Reply with exactly: terminal routing");
const createdAgentUrl = page.url();
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(createdAgentUrl);
await ensureExplorerTabsVisible(page);
const terminalsTab = visibleTestId(page, "explorer-tab-terminals");
@@ -1,27 +1,13 @@
import { test, expect } from "./fixtures";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { test, expect, type Page } from "./fixtures";
import { setWorkingDirectory } from "./helpers/app";
import { createTempGitRepo } from "./helpers/workspace";
import { openNewAgentComposer, seedWorkspaceActivity } from "./helpers/workspace-ui";
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
function buildWorkspaceRoute(serverId: string, workspacePath: string): string {
return buildHostWorkspaceRoute(serverId, workspacePath);
}
async function openWorkspace(page: Page, workspacePath: string): Promise<void> {
const serverId = process.env.E2E_SERVER_ID;
if (!serverId) {
throw new Error("E2E_SERVER_ID is not set.");
}
await page.goto(buildWorkspaceRoute(serverId, workspacePath));
await expect(page).toHaveURL(new RegExp(`/h/${encodeURIComponent(serverId)}/workspace/`), {
timeout: 30_000,
});
await expect(page.getByTestId("workspace-new-terminal-tab").first()).toBeVisible({
timeout: 30_000,
});
}
import {
openNewAgentComposer,
seedWorkspaceActivity,
switchWorkspaceViaSidebar,
} from "./helpers/workspace-ui";
function percentile(values: number[], p: number): number {
if (values.length === 0) {
@@ -58,6 +44,7 @@ function summarize(values: number[]) {
function buildStressCommand(doneMarker: string): string {
// Deterministic synthetic "TUI-like" redraw loop: alternate screen + cursor-home repaint.
const markerFile = ".paseo-terminal-benchmark-marker";
return [
"i=1",
"printf '\\033[?1049h\\033[2J'",
@@ -72,9 +59,19 @@ function buildStressCommand(doneMarker: string): string {
"i=$((i+1))",
"done",
`printf '\\033[?1049l\\n${doneMarker}\\n'`,
`printf '${doneMarker}\\n' > '${markerFile}'`,
].join("; ");
}
async function markerFileContains(filePath: string, marker: string): Promise<boolean> {
try {
const text = await readFile(filePath, "utf8");
return text.includes(marker);
} catch {
return false;
}
}
async function toggleExplorerAndMeasureLatency(page: Page): Promise<number> {
const toggle = page.getByTestId("workspace-explorer-toggle").first();
await expect(toggle).toBeVisible({ timeout: 30_000 });
@@ -92,13 +89,22 @@ test("workspace terminal responsiveness benchmark (report-only, single stress pr
}, testInfo) => {
test.setTimeout(180_000);
const repo = await createTempGitRepo("paseo-e2e-terminal-benchmark-");
const markerFilePath = path.join(repo.path, ".paseo-terminal-benchmark-marker");
try {
const serverId = process.env.E2E_SERVER_ID;
if (!serverId) {
throw new Error("E2E_SERVER_ID is not set.");
}
await openNewAgentComposer(page);
await setWorkingDirectory(page, repo.path);
await seedWorkspaceActivity(page, `terminal benchmark seed ${Date.now()}`);
await openWorkspace(page, repo.path);
await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: repo.path });
await expect(page.getByTestId("workspace-new-terminal-tab").first()).toBeVisible({
timeout: 30_000,
});
const newTerminalButton = page.getByTestId("workspace-new-terminal-tab").first();
await expect(newTerminalButton).toBeVisible({ timeout: 30_000 });
@@ -163,11 +169,11 @@ test("workspace terminal responsiveness benchmark (report-only, single stress pr
});
await surface.click({ force: true });
await page.keyboard.type(`echo ${postMarker}`, { delay: 0 });
await page.keyboard.type(`echo ${postMarker} >> .paseo-terminal-benchmark-marker`, { delay: 0 });
await page.keyboard.press("Enter");
await expect(page.getByText(postMarker).first()).toBeVisible({
await expect.poll(async () => await markerFileContains(markerFilePath, postMarker), {
timeout: 120_000,
});
}).toBe(true);
const diagnostics = await page.evaluate(async () => {
const debug = (
@@ -205,9 +211,7 @@ test("workspace terminal responsiveness benchmark (report-only, single stress pr
frameSleepMs: 10,
doneMarker,
postMarker,
doneMarkerObserved:
(await page.getByText(doneMarker).first().isVisible().catch(() => false)) ||
(await page.getByText(doneMarker).count()) > 0,
doneMarkerObserved: await markerFileContains(markerFilePath, doneMarker),
},
frameGapMs: {
...summarize(frameGapsMs),
@@ -0,0 +1,125 @@
import { test, expect } from "./fixtures";
import type { Page } from "@playwright/test";
import { createAgentInRepo } from "./helpers/app";
import { createTempGitRepo } from "./helpers/workspace";
import {
ensureWorkspaceAgentPaneVisible,
getWorkspaceTabTestIds,
sampleWorkspaceTabIds,
waitForWorkspaceTabsVisible,
} from "./helpers/workspace-tabs";
import { switchWorkspaceViaSidebar } from "./helpers/workspace-ui";
async function expectComposerFocused(page: Page) {
const composer = page.getByRole("textbox", { name: "Message agent..." }).first();
await expect(composer).toBeEditable({ timeout: 30_000 });
await expect
.poll(async () => {
return await composer.evaluate(
(element) => document.activeElement === element
);
})
.toBe(true);
}
test("workspace draft submit retargets tab in place without transient extra tabs", async ({ page }) => {
const serverId = process.env.E2E_SERVER_ID;
if (!serverId) {
throw new Error("E2E_SERVER_ID is not set.");
}
const repo = await createTempGitRepo("paseo-e2e-draft-retarget-");
const seedPrompt = `seed prompt ${Date.now()}`;
const createPrompt = `retarget prompt ${Date.now()}`;
try {
await createAgentInRepo(page, { directory: repo.path, prompt: seedPrompt });
await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: repo.path });
await waitForWorkspaceTabsVisible(page);
await ensureWorkspaceAgentPaneVisible(page);
const beforeDraftIds = await getWorkspaceTabTestIds(page);
await page.getByTestId("workspace-new-agent-tab").first().click();
await ensureWorkspaceAgentPaneVisible(page);
await expect(page.getByRole("textbox", { name: "Message agent..." })).toBeEditable();
const withDraftIds = await getWorkspaceTabTestIds(page);
expect(withDraftIds.length).toBe(beforeDraftIds.length + 1);
const draftTabTestId = withDraftIds.find((id) => !beforeDraftIds.includes(id));
expect(draftTabTestId).toBeTruthy();
const draftId = draftTabTestId!.replace("workspace-tab-", "");
const draftCloseButton = page.getByTestId(`workspace-draft-close-${draftId}`).first();
await expect(draftCloseButton).toBeVisible({ timeout: 30_000 });
const samplingPromise = sampleWorkspaceTabIds(page, { durationMs: 3_000, intervalMs: 40 });
const input = page.getByRole("textbox", { name: "Message agent..." });
await input.fill(createPrompt);
await input.press("Enter");
await expect(page.getByText(createPrompt, { exact: true }).first()).toBeVisible({
timeout: 30_000,
});
const snapshots = await samplingPromise;
const maxObservedCount = snapshots.reduce((max, ids) => Math.max(max, ids.length), 0);
expect(maxObservedCount).toBe(withDraftIds.length);
const finalIds = await getWorkspaceTabTestIds(page);
expect(finalIds.length).toBe(withDraftIds.length);
expect(finalIds).toContain(draftTabTestId!);
await expect(draftCloseButton).not.toBeVisible({ timeout: 30_000 });
} finally {
await repo.cleanup();
}
});
test("workspace agent tab switch focuses composer on desktop web", async ({ page }) => {
const serverId = process.env.E2E_SERVER_ID;
if (!serverId) {
throw new Error("E2E_SERVER_ID is not set.");
}
const repo = await createTempGitRepo("paseo-e2e-tab-focus-");
const firstPrompt = `first tab prompt ${Date.now()}`;
const secondPrompt = `second tab prompt ${Date.now()}`;
try {
await createAgentInRepo(page, { directory: repo.path, prompt: firstPrompt });
await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: repo.path });
await waitForWorkspaceTabsVisible(page);
await ensureWorkspaceAgentPaneVisible(page);
const beforeSecondAgentIds = await getWorkspaceTabTestIds(page);
await page.getByTestId("workspace-new-agent-tab").first().click();
await expectComposerFocused(page);
const composer = page.getByRole("textbox", { name: "Message agent..." }).first();
await composer.fill(secondPrompt);
await composer.press("Enter");
await expect(page.getByText(secondPrompt, { exact: true }).first()).toBeVisible({
timeout: 30_000,
});
const withSecondAgentIds = await getWorkspaceTabTestIds(page);
const secondAgentTabTestId = withSecondAgentIds.find(
(id) => !beforeSecondAgentIds.includes(id)
);
if (!secondAgentTabTestId) {
throw new Error("Expected second agent tab to be created.");
}
const firstAgentTabTestId = beforeSecondAgentIds[0];
if (!firstAgentTabTestId) {
throw new Error("Expected first agent tab to exist.");
}
await page.getByTestId(firstAgentTabTestId).first().click();
await expectComposerFocused(page);
await page.getByTestId(secondAgentTabTestId).first().click();
await expectComposerFocused(page);
} finally {
await repo.cleanup();
}
});
@@ -1,16 +1,7 @@
import { test, expect, type Page } from "./fixtures";
import {
createAgent,
ensureHostSelected,
gotoHome,
setWorkingDirectory,
} from "./helpers/app";
import { createAgentInRepo } from "./helpers/app";
import { createTempGitRepo } from "./helpers/workspace";
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
function buildWorkspaceRoute(serverId: string, workspacePath: string): string {
return buildHostWorkspaceRoute(serverId, workspacePath);
}
import { switchWorkspaceViaSidebar } from "./helpers/workspace-ui";
async function openWorkspaceWithAgent(page: Page, workspacePath: string): Promise<void> {
const serverId = process.env.E2E_SERVER_ID;
@@ -18,15 +9,11 @@ async function openWorkspaceWithAgent(page: Page, workspacePath: string): Promis
throw new Error("E2E_SERVER_ID is not set.");
}
await gotoHome(page);
await ensureHostSelected(page);
await setWorkingDirectory(page, workspacePath);
await createAgent(page, `workspace header restore ${Date.now()}`);
await page.goto(buildWorkspaceRoute(serverId, workspacePath));
await expect(page).toHaveURL(new RegExp(`/h/${encodeURIComponent(serverId)}/workspace/`), {
timeout: 30000,
await createAgentInRepo(page, {
directory: workspacePath,
prompt: `workspace header restore ${Date.now()}`,
});
await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: workspacePath });
await expect(page.getByTestId("workspace-new-agent-tab").first()).toBeVisible({
timeout: 30000,
});
@@ -49,15 +36,17 @@ test("workspace new-tab buttons stay on-screen during horizontal scroll", async
await expect(tabsScroll).toBeVisible({ timeout: 30000 });
// Create enough terminal tabs to ensure the tabs row has overflow to scroll.
const terminalTabs = page.locator('[data-testid^="workspace-tab-terminal:"]');
const initialTerminalCount = await terminalTabs.count();
const targetTerminalCount = initialTerminalCount + 8;
const workspaceTabs = page.locator(
'[data-testid^="workspace-tab-"]:not([data-testid^="workspace-tab-context-"])'
);
const initialTabCount = await workspaceTabs.count();
const targetTabCount = initialTabCount + 8;
for (let attempt = initialTerminalCount; attempt < targetTerminalCount; attempt += 1) {
for (let attempt = initialTabCount; attempt < targetTabCount; attempt += 1) {
await expect(terminalButton).toBeEnabled({ timeout: 30000 });
await terminalButton.click();
await expect
.poll(async () => await terminalTabs.count(), { timeout: 30000 })
.poll(async () => await workspaceTabs.count(), { timeout: 30000 })
.toBeGreaterThanOrEqual(attempt + 1);
}
@@ -1,68 +1,14 @@
import { execSync } from 'node:child_process';
import { test, expect, type Page } from './fixtures';
import { test } from './fixtures';
import { setWorkingDirectory } from './helpers/app';
import { createTempGitRepo } from './helpers/workspace';
import { openNewAgentComposer, seedWorkspaceActivity } from './helpers/workspace-ui';
import { buildHostWorkspaceRoute } from '@/utils/host-routes';
function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function workspaceLabelFromPath(value: string): string {
const normalized = value.replace(/\\/g, '/').replace(/\/+$/, '');
const parts = normalized.split('/').filter(Boolean);
return parts[parts.length - 1] ?? normalized;
}
function candidateWorkspaceIds(inputPath: string): string[] {
const trimmed = inputPath.replace(/\/+$/, '');
const candidates = new Set<string>([trimmed]);
if (trimmed.startsWith('/var/')) {
candidates.add(`/private${trimmed}`);
}
if (trimmed.startsWith('/private/var/')) {
candidates.add(trimmed.replace(/^\/private/, ''));
}
return Array.from(candidates);
}
function workspaceRowLocator(page: Page, serverId: string, workspacePath: string) {
const ids = candidateWorkspaceIds(workspacePath).map(
(id) => `[data-testid="sidebar-workspace-row-${serverId}:${id}"]`
);
return page.locator(ids.join(',')).first();
}
async function switchViaSidebar(input: {
page: Page;
serverId: string;
targetWorkspacePath: string;
}) {
const row = workspaceRowLocator(input.page, input.serverId, input.targetWorkspacePath);
await expect(row).toBeVisible({ timeout: 30000 });
await row.click();
const targetWorkspaceRoute = buildHostWorkspaceRoute(input.serverId, input.targetWorkspacePath);
await expect(input.page).toHaveURL(new RegExp(escapeRegex(targetWorkspaceRoute)), {
timeout: 30000,
});
}
async function expectWorkspaceHeader(
page: Page,
input: { title: string; subtitle: string }
): Promise<void> {
const titleLocator = page.getByTestId('workspace-header-title');
const subtitleLocator = page.getByTestId('workspace-header-subtitle');
await expect(titleLocator.first()).toHaveText(input.title, {
timeout: 30000,
});
await expect(subtitleLocator.first()).toHaveText(input.subtitle, {
timeout: 30000,
});
}
import {
expectWorkspaceHeader,
openNewAgentComposer,
seedWorkspaceActivity,
switchWorkspaceViaSidebar,
workspaceLabelFromPath,
} from './helpers/workspace-ui';
test('sidebar workspace switch keeps visible content in sync with selected workspace', async ({ page }) => {
const serverId = process.env.E2E_SERVER_ID;
@@ -88,20 +34,19 @@ test('sidebar workspace switch keeps visible content in sync with selected works
await setWorkingDirectory(page, repoB.path);
await seedWorkspaceActivity(page, tokenB);
await page.goto(buildHostWorkspaceRoute(serverId, repoA.path));
await expect(page).toHaveURL(new RegExp('/workspace/'), { timeout: 30000 });
await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: repoA.path });
await expectWorkspaceHeader(page, {
title: 'sync-a-branch',
subtitle: workspaceLabelFromPath(repoA.path),
});
await switchViaSidebar({ page, serverId, targetWorkspacePath: repoB.path });
await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: repoB.path });
await expectWorkspaceHeader(page, {
title: 'sync-b-branch',
subtitle: workspaceLabelFromPath(repoB.path),
});
await switchViaSidebar({ page, serverId, targetWorkspacePath: repoA.path });
await switchWorkspaceViaSidebar({ page, serverId, targetWorkspacePath: repoA.path });
await expectWorkspaceHeader(page, {
title: 'sync-a-branch',
subtitle: workspaceLabelFromPath(repoA.path),