Files
paseo/packages/app/e2e/support/helpers/workspace-ui.ts
T
Mohamed BoudraandGitHub c1f7ed3198 Improve reconnect status feedback (#2931)
* fix(app): debounce reconnect status toast

Keep reconnect transport immediate while delaying the visible status toast so brief outages stay quiet. Scope the toast to the active retained panel, keep its copy and status dot consistent across locales, and add a manual real relay deployment journey that verifies streaming recovery without user action.

* fix(app): isolate manual relay deployment check

Keep the deployment journey out of the provider-wide real test project so it runs only when its local relay prerequisites are explicitly requested. Also keep the relay teardown active when packaged daemon startup or later cleanup fails.

* fix(app): preserve reconnect toast across tabs

Keep reconnect presentation timing at the host level so switching between retained panels does not restart the debounce or flash the toast. Add a browser regression covering a visible outage across retained tab switches.

* fix(app): anchor reconnect debounce to host status

Record host connection status transitions independently of mounted agent panels and preserve the outage start across reconnecting states. Use that host-level timestamp when a visible agent panel appears, with runtime coverage for the transition lifecycle.

* fix(app): reset reconnect toast state per outage

* test(app): build relay journey prerequisites
2026-08-06 13:45:34 +02:00

155 lines
5.2 KiB
TypeScript

import { expect, type Page } from "@playwright/test";
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
import { gotoHome } from "./app";
import { expectAppRoute } from "./route-assertions";
export async function openNewAgentComposer(page: Page): Promise<void> {
await gotoHome(page);
}
/**
* Wait for the sidebar to show at least one project row. Use this after a spec
* seeds a workspace/project; zero-project flows need their own assertion.
*/
export async function waitForSidebarHydration(page: Page, timeout = 60_000): Promise<void> {
await page
.locator('[data-testid^="sidebar-project-row-"]')
.first()
.waitFor({ state: "visible", timeout });
}
export async function waitForNoProjectsInSidebar(page: Page, timeout = 60_000): Promise<void> {
await page.getByTestId("sidebar-project-empty-state").waitFor({ state: "visible", timeout });
}
function workspaceRowLocator(page: Page, serverId: string, workspaceId: string) {
return page.getByTestId(`sidebar-workspace-row-${serverId}:${workspaceId}`).first();
}
export async function expectSidebarWorkspaceSelected(input: {
page: Page;
serverId: string;
workspaceId: string;
selected?: boolean;
}): Promise<void> {
const row = workspaceRowLocator(input.page, input.serverId, input.workspaceId);
await expect(row).toBeVisible({ timeout: 30_000 });
const expected = input.selected === false ? "false" : "true";
const hasDataSelected = await row.getAttribute("data-selected");
if (hasDataSelected !== null) {
await expect(row).toHaveAttribute("data-selected", expected, {
timeout: 30_000,
});
return;
}
await expect(row).toHaveAttribute("aria-selected", expected, {
timeout: 30_000,
});
}
export async function switchWorkspaceViaSidebar(input: {
page: Page;
serverId: string;
workspaceId: string;
}): Promise<void> {
const row = workspaceRowLocator(input.page, input.serverId, input.workspaceId);
await expect(row).toBeVisible({ timeout: 30_000 });
await row.click();
const targetWorkspaceRoute = buildHostWorkspaceRoute(input.serverId, input.workspaceId);
await expectAppRoute(input.page, targetWorkspaceRoute, { timeout: 30_000 });
}
/**
* Wait for a workspace's sidebar row to appear, confirming the workspace
* descriptor has been hydrated into the session store.
*/
export async function waitForWorkspaceInSidebar(
page: Page,
input: { serverId: string; workspaceId: string },
): Promise<void> {
await workspaceRowLocator(page, input.serverId, input.workspaceId).waitFor({
state: "visible",
timeout: 60_000,
});
}
export async function expectWorkspaceHeader(
page: Page,
input: { title: string; subtitle: string },
): Promise<void> {
const titleLocator = page.getByTestId("workspace-header-title").filter({ visible: true });
const subtitleLocator = page.getByTestId("workspace-header-subtitle").filter({ visible: true });
await expect(titleLocator.first()).toHaveText(input.title, {
timeout: 30_000,
});
await expect(subtitleLocator.first()).toHaveText(input.subtitle, {
timeout: 30_000,
});
}
export async function expectReconnectingToastVisible(
page: Page,
options?: { timeout?: number },
): Promise<void> {
const toast = page.getByTestId("agent-reconnecting-toast");
await expect(toast).toBeVisible({
timeout: options?.timeout ?? 30_000,
});
await expect(toast).toHaveText("Reconnecting");
await expect(toast.getByTestId("agent-reconnecting-status-dot")).toBeVisible();
}
export async function expectReconnectingToastGone(
page: Page,
options?: { timeout?: number },
): Promise<void> {
await expect(page.getByTestId("agent-reconnecting-toast")).toHaveCount(0, {
timeout: options?.timeout ?? 30_000,
});
}
export async function expectReconnectingToastDebounced(
page: Page,
options?: { durationMs?: number },
): Promise<void> {
await page.waitForTimeout(options?.durationMs ?? 750);
await expect(page.getByTestId("agent-reconnecting-toast")).toHaveCount(0, { timeout: 100 });
}
export async function expectHostConnectingOrOffline(
page: Page,
options?: { timeout?: number },
): Promise<void> {
await expect(
page.getByText(/^Connecting$|localhost is offline|Cannot reach localhost/i),
).toBeVisible({ timeout: options?.timeout ?? 30_000 });
}
export async function expectMenuButtonVisible(page: Page): Promise<void> {
await expect(page.getByTestId("menu-button")).toBeVisible();
}
export async function expectWorkspaceHeaderAbsent(page: Page): Promise<void> {
await expect(page.getByTestId("workspace-header-title")).toHaveCount(0);
}
export function workspaceDeckEntryLocator(page: Page, serverId: string, workspaceId: string) {
return page.getByTestId(`workspace-deck-entry-${serverId}:${workspaceId}`);
}
export async function expectWorkspaceDeckEntryCount(page: Page, count: number): Promise<void> {
await expect(page.locator('[data-testid^="workspace-deck-entry-"]')).toHaveCount(count);
}
export async function seedWorkspaceActivity(page: Page, marker: string): Promise<void> {
const input = page.getByRole("textbox", { name: "Message agent..." });
await expect(input).toBeEditable({ timeout: 30_000 });
await input.fill(marker);
await input.press("Enter");
await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 });
}