Files
paseo/packages/app/e2e/support/helpers/workspace-tabs.ts
Mohamed Boudra d3c76be9c1 Unify Explorer tabs and refine launch controls (#5942)
* feat(app): unify Explorer tabs and refine interface styling

Give Explorer the workspace tab controls while keeping launcher choices separate from drag-and-drop support.

Upgrade Lucide and set a shared icon stroke width for consistent styling. Reduce the desktop composer bottom inset to align the action row with its corners.

* fix(app): show active Explorer tabs in foreground

The shared row coupled foreground styling to keyboard shortcut ownership. Separate those concerns so Explorer can highlight its active tab while Cmd+T remains with the workspace pane.

* fix(app): refine Explorer close controls and launch options

* test(app): respect singleton Files in Explorer plugin workflow

* test(server): trim unused setup from no-track checkout regression

* test(app): count visible main tab rows after pane moves
2026-10-03 01:20:53 +01:00

247 lines
8.8 KiB
TypeScript

import { expect, type Locator, 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;
}
function setupTabTestId(workspaceId: string): string {
return `workspace-tab-setup_${workspaceId}`;
}
async function waitForSetupToReachWorkspace(page: Page): Promise<void> {
const actionsButton = page.getByTestId("workspace-header-menu-trigger");
await expect(actionsButton).toBeVisible({ timeout: 30_000 });
await actionsButton.click();
await expect(page.getByTestId("workspace-header-show-setup")).toBeVisible({ timeout: 30_000 });
await page.keyboard.press("Escape");
}
export async function expectSetupTabNotSeeded(page: Page, workspaceId: string): Promise<void> {
await waitForSetupToReachWorkspace(page);
const tab = page.getByTestId(setupTabTestId(workspaceId));
for (let attempt = 0; attempt < 5; attempt += 1) {
await expect(tab).toHaveCount(0);
await page.waitForTimeout(100);
}
}
export async function expectFailedSetupTabSeededInMainPane(
page: Page,
workspaceId: string,
): Promise<void> {
const tabId = setupTabTestId(workspaceId);
await expect(page.getByTestId(tabId).filter({ visible: true }).first()).toBeVisible({
timeout: 30_000,
});
const explorer = await ensureExplorerSidebar(page);
await expect(explorer.getByTestId(tabId)).toHaveCount(0);
}
export async function closeSetupTab(page: Page, workspaceId: string): Promise<void> {
const tabId = setupTabTestId(workspaceId);
await page.getByTestId(tabId).filter({ visible: true }).first().click({ button: "right" });
await page.getByTestId(`workspace-tab-context-setup_${workspaceId}-close`).click();
await expect(page.getByTestId(tabId)).toHaveCount(0);
}
function visibleTestId(page: Page, testId: string) {
return page.getByTestId(testId).filter({ visible: true });
}
function explorerSidebar(page: Page) {
return visibleTestId(page, "workspace-explorer-sidebar").first();
}
async function selectWorkspaceTab(tab: Locator): Promise<void> {
if ((await tab.getAttribute("aria-selected")) !== "true") {
// The close action overlays the chip's trailing edge on hover. Click the
// leading icon area so Playwright does not target that separate control.
await tab.click({ position: { x: 12, y: 13 } });
}
await expect(tab).toHaveAttribute("aria-selected", "true");
}
/** Reveal the Explorer sidebar without changing its selected view. */
export async function ensureExplorerSidebar(page: Page): Promise<Locator> {
const toggle = page.getByTestId("workspace-explorer-toggle").first();
await expect(toggle).toBeVisible({ timeout: 30_000 });
const explorer = explorerSidebar(page);
if ((await explorer.count()) === 0) {
await toggle.click();
}
await expect(explorer).toBeVisible({ timeout: 30_000 });
return explorer;
}
/** Reveals the Explorer sidebar and selects one of its fixed navigation views. */
async function openExplorerView(
page: Page,
view: { tabTestId: string; contentTestId: string; timeout?: number },
): Promise<void> {
const explorer = await ensureExplorerSidebar(page);
const tab = explorer.getByTestId(view.tabTestId);
await selectWorkspaceTab(tab);
await expect(visibleTestId(page, view.contentTestId).first()).toBeVisible({
timeout: view.timeout ?? 30_000,
});
}
export async function openChangesTreePanel(page: Page): Promise<void> {
await openExplorerView(page, {
tabTestId: "workspace-tab-changes_tree",
contentTestId: "changes-tree-panel",
});
}
export async function openChangesPanel(page: Page, timeout = 30_000): Promise<void> {
await openChangesTreePanel(page);
const changedFile = page
.locator('[data-testid^="diff-tree-file-"][data-testid$="-toggle"]')
.filter({ visible: true })
.first();
await expect(changedFile).toBeVisible({ timeout });
await changedFile.click();
await expect(visibleTestId(page, "working-diff-panel").first()).toBeVisible({
timeout,
});
}
export async function openFilesPanel(page: Page): Promise<void> {
await openExplorerView(page, {
tabTestId: "workspace-tab-files",
contentTestId: "file-explorer-tree-scroll",
});
}
export async function openPullRequestPanel(page: Page): Promise<void> {
const existingTab = visibleTestId(page, "workspace-tab-pull_request").first();
if ((await existingTab.count()) > 0) {
await selectWorkspaceTab(existingTab);
await expect(visibleTestId(page, "pr-pane").first()).toBeVisible({ timeout: 15_000 });
return;
}
const trigger = visibleTestId(page, "workspace-new-tab-button").first();
await trigger.click();
await visibleTestId(page, "workspace-new-tab-menu-pull-request").first().click();
await expect(visibleTestId(page, "pr-pane").first()).toBeVisible({ timeout: 15_000 });
}
export async function waitForWorkspaceTabsVisible(page: Page): Promise<void> {
await expect(visibleTestId(page, "workspace-tabs-row").first()).toBeVisible({
timeout: 30_000,
});
await expect(visibleTestId(page, "workspace-new-tab-button").first()).toBeVisible({
timeout: 30_000,
});
}
/** Open the pane-local `+` menu and pick Agent. */
export async function createAgentTabFromMenu(page: Page): Promise<void> {
const trigger = visibleTestId(page, "workspace-new-tab-button").first();
await expect(trigger).toBeVisible({ timeout: 10_000 });
await trigger.click();
const item = visibleTestId(page, "workspace-new-tab-menu-agent").first();
await expect(item).toBeVisible({ timeout: 10_000 });
await item.click();
}
export async function getVisibleWorkspaceAgentTabIds(page: Page): Promise<string[]> {
const tabs = page.locator('[data-testid^="workspace-tab-agent_"]').filter({ visible: true });
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 expectOnlyWorkspaceAgentTabsVisible(
page: Page,
expectedAgentIds: string[],
): Promise<void> {
const expected = new Set(expectedAgentIds.map((id) => `workspace-tab-agent_${id}`));
const visible = await getVisibleWorkspaceAgentTabIds(page);
const unexpected = visible.filter((id) => !expected.has(id));
expect(unexpected).toEqual([]);
expect(visible.length).toBe(expected.size);
for (const expectedId of expectedAgentIds) {
await expect(visibleTestId(page, `workspace-tab-agent_${expectedId}`).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 expectWorkspaceTabsAbsent(page: Page): Promise<void> {
await expect(page.getByTestId("workspace-tabs-row")).toHaveCount(0);
}
export async function expectNoTerminalTabs(page: Page): Promise<void> {
await expect(page.locator('[data-testid^="workspace-tab-terminal_"]')).toHaveCount(0);
}
export async function clickFirstTerminalTab(
page: Page,
options?: { timeout?: number },
): Promise<void> {
const tab = page.locator('[data-testid^="workspace-tab-terminal_"]').first();
await expect(tab).toBeVisible({ timeout: options?.timeout ?? 30_000 });
await tab.click();
}
export async function expectFirstTerminalTabContains(page: Page, text: string): Promise<void> {
await expect(page.locator('[data-testid^="workspace-tab-terminal_"]').first()).toContainText(
text,
);
}
export async function expectTerminalTabOpen(
page: Page,
options?: { timeout?: number },
): Promise<void> {
await expect(
page.locator('[data-testid^="workspace-tab-terminal_"]').filter({ visible: true }).first(),
).toBeVisible({ timeout: options?.timeout ?? 30_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;
}