Files
paseo-bot[bot]androyenheart 5dc350034e Keep large Import Session lists responsive on Web (#7872)
* Virtualize long Import Session Web lists while preserving native and short-list scrolling

* fix(app): measure import list headers through the existing layout event

Keep the virtualized import list usable in the existing component harness without introducing an observer mock. Import React for the unit JSX transform.

Co-authored-by: royenheart <royenheart@outlook.com>

---------

Co-authored-by: royenheart <royenheart@outlook.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-25 15:05:12 +00:00

187 lines
8.3 KiB
TypeScript

import { expect, type Page } from "@playwright/test";
import { buildHostWorkspaceRoute } from "@/utils/host-routes";
import { gotoAppShell } from "./app";
import { openCommandCenter } from "./command-center";
import { openGlobalNewWorkspaceComposer } from "./new-workspace";
import { getServerId } from "./server-id";
import { expectMobileAgentSidebarVisible, openMobileAgentSidebar } from "./sidebar";
const rowTestId = (id: string) => `import-session-session-claude-${id}`;
const rowFolderTestId = (id: string) => `import-session-row-folder-claude-${id}`;
export class ImportSessionFlow {
constructor(private readonly page: Page) {}
async openWorkspace(workspaceId: string, viewport: { width: number; height: number }) {
await this.page.setViewportSize(viewport);
await gotoAppShell(this.page);
await this.page.goto(buildHostWorkspaceRoute(getServerId(), workspaceId));
await expect(this.page.getByRole("button", { name: "Workspace actions" })).toBeVisible({
timeout: 30_000,
});
}
/** The new workspace screen's Import session button, reached from the sidebar. */
async revealNewWorkspaceEntryPoint() {
const compact = (this.page.viewportSize()?.width ?? 0) < 768;
if (compact) {
await openMobileAgentSidebar(this.page);
await expectMobileAgentSidebarVisible(this.page);
}
await openGlobalNewWorkspaceComposer(this.page);
const button = this.page.getByTestId("new-workspace-import-session").filter({ visible: true });
await expect(button).toHaveAccessibleName("Import session");
await expect(button).toBeInViewport();
}
async openGlobally() {
await this.revealNewWorkspaceEntryPoint();
await this.openFromNewWorkspace();
}
async openFromNewWorkspace() {
await this.page.getByTestId("new-workspace-import-session").filter({ visible: true }).click();
await this.expectSheetReady();
}
async openFromWorkspaceHeader() {
await this.page.getByRole("button", { name: "Workspace actions" }).click();
await this.page.getByTestId("workspace-header-import-agent").click();
await this.expectScope("This workspace");
await expect(this.page.getByTestId("import-session-show-all")).toBeVisible();
await expect(this.page.getByText("Workspace actions", { exact: true })).not.toBeVisible();
}
async expectScope(text: string, exact = true) {
const scope = this.page.getByTestId("import-session-scope");
if (exact) await expect(scope).toHaveText(text);
else await expect(scope).toContainText(text);
await expect(scope).toBeVisible();
}
async expectRows(input: {
first?: string[];
before?: [string, string];
folders?: Array<[string, string]>;
}) {
for (const id of input.first ?? input.before ?? []) {
await expect(this.page.getByTestId(rowTestId(id))).toHaveCount(1);
}
const ids = await this.rowIds();
if (input.first) expect(ids.slice(0, input.first.length)).toEqual(input.first.map(rowTestId));
if (input.before) {
expect(ids.indexOf(rowTestId(input.before[0]))).toBeGreaterThanOrEqual(0);
expect(ids.indexOf(rowTestId(input.before[0]))).toBeLessThan(
ids.indexOf(rowTestId(input.before[1])),
);
}
for (const [id, folder] of input.folders ?? []) {
await expect(this.page.getByTestId(rowFolderTestId(id))).toHaveText(folder);
}
}
async expectProviderError(label: string) {
await expect(this.page.getByTestId("import-session-provider-errors")).toContainText(
`Could not load ${label} sessions`,
);
}
async expectProviderFilterFits(width: number) {
await expect(this.page.getByTestId("import-session-scope")).toBeInViewport();
const filter = this.page.getByRole("button", { name: "Filter: All" });
await expect(filter).toBeInViewport();
const bounds = await filter.boundingBox();
expect(bounds?.x ?? width).toBeGreaterThanOrEqual(0);
expect((bounds?.x ?? width) + (bounds?.width ?? 1)).toBeLessThanOrEqual(width);
}
async revealSession(id: string) {
const target = this.page.getByTestId(rowTestId(id));
if ((await target.count()) === 0) {
await this.scrollList(false);
await expect(async () => {
if ((await target.count()) === 0) await this.scrollList(true);
await expect(target).toHaveCount(1, { timeout: 100 });
}).toPass({ timeout: 10_000 });
}
await target.scrollIntoViewIfNeeded();
await expect(target).toBeInViewport();
}
private async scrollList(forward: boolean) {
await this.page
.locator('[data-testid^="import-session-session-"]')
.first()
.evaluate((row, advance) => {
let element = row.parentElement;
while (element && !["auto", "scroll"].includes(getComputedStyle(element).overflowY))
element = element.parentElement;
if (!element) throw new Error("Import list has no scroll owner");
element.scrollTop = advance ? element.scrollTop + element.clientHeight * 0.8 : 0;
element.dispatchEvent(new Event("scroll"));
}, forward);
}
async search(query: string) {
await this.page.getByTestId("import-session-search").fill(query);
await expect(this.page.getByText("Invoice migration plan", { exact: true })).toBeVisible();
await expect(this.page.getByText("Root session 01", { exact: true })).toHaveCount(0);
await expect(this.page.getByTestId("import-session-load-more")).toHaveCount(0);
}
async resetSearch() {
await this.page.getByTestId("import-session-search").fill("");
await expect(this.page.getByTestId("import-session-load-more")).toBeVisible();
}
async loadMore() {
await this.page.getByTestId("import-session-load-more").click();
await this.revealSession("fixture-root-20");
await expect(this.page.getByText("Root session 20", { exact: true })).toBeInViewport();
await expect(this.page.getByTestId("import-session-load-more")).toHaveCount(0);
}
async retryProvider(provider: string, label: string) {
await this.expectProviderError(label);
await this.page.getByTestId(`import-session-retry-${provider}`).click();
await expect(this.page.getByTestId(`import-session-retry-${provider}`)).toBeEnabled();
await this.expectProviderError(label);
await expect(this.page.getByRole("progressbar")).toHaveCount(0);
}
async importSession(id: string) {
await this.revealSession(id);
await this.page.getByTestId(rowTestId(id)).click();
await expect(this.page.getByTestId("import-session-sheet")).toHaveCount(0, {
timeout: 30_000,
});
}
async expectTranscript(userText: string, assistantText: string) {
await expect(this.page.getByTestId("user-message").filter({ visible: true })).toContainText(
userText,
);
await expect(
this.page.getByTestId("assistant-message").filter({ visible: true }),
).toContainText(assistantText);
}
async showAll() {
await this.page.getByTestId("import-session-show-all").click();
await expect(this.page.getByTestId("import-session-scope")).toContainText("Sessions on");
}
async expectImportedIntoWorkspace(workspaceId: string, userText: string) {
await expect(this.page).toHaveURL(buildHostWorkspaceRoute(getServerId(), workspaceId), {
timeout: 30_000,
});
const workspace = this.page.getByTestId(`workspace-deck-entry-${getServerId()}:${workspaceId}`);
await expect(workspace.getByTestId("user-message").filter({ visible: true })).toContainText(
userText,
);
}
async close() {
await this.page.getByRole("button", { name: "Close", exact: true }).last().click();
await expect(this.page.getByTestId("import-session-sheet")).toHaveCount(0);
}
async expectCommandCenterMatch() {
const panel = await openCommandCenter(this.page);
await panel.getByTestId("command-center-input").fill("import");
await expect(panel.getByText("Import session", { exact: true })).toBeVisible();
await expect(panel.getByText("Home", { exact: true })).toHaveCount(0);
}
private async expectSheetReady() {
const sheet = this.page.getByTestId("import-session-sheet");
await expect(sheet).toBeVisible({ timeout: 30_000 });
await expect(sheet.getByText("Loading sessions...", { exact: true })).toHaveCount(0, {
timeout: 30_000,
});
}
private async rowIds() {
return await this.page
.locator('[data-testid^="import-session-session-claude-"]')
.evaluateAll((nodes) => nodes.map((node) => node.getAttribute("data-testid")));
}
}