* Add npm release track invariant and beta dist-tag procedure * Revert "Keep older chat history and image previews stable (#2490)" The timeline optimistic/pagination rework needs more work than main releases can wait for. Reverted together with #2484; both are reapplied on integration/timeline-optimistic-rework. * Revert "Stop completed turns from appearing stuck (#2484)" See the #2490 revert. #2490 built on this submission-authority change, so the two revert and reapply as a pair.
140 lines
4.5 KiB
TypeScript
140 lines
4.5 KiB
TypeScript
import { expect, type Page } from "@playwright/test";
|
|
import { buildAgentRoute, seedMockAgentWorkspace, type MockAgentWorkspace } from "./mock-agent";
|
|
import {
|
|
delayAgentOlderTimelineResponse,
|
|
type AgentTimelineResponseGate,
|
|
} from "./agent-timeline-gate";
|
|
|
|
interface LongTimelineAgentOptions {
|
|
turns: number;
|
|
}
|
|
|
|
interface LongTimelineAgent extends MockAgentWorkspace {
|
|
oldestPrompt: string;
|
|
newestPrompt: string;
|
|
}
|
|
|
|
const PROMPT_PREFIX = "timeline-pagination-turn";
|
|
|
|
function promptForTurn(index: number): string {
|
|
return `${PROMPT_PREFIX}-${index}: emit 1 coalesced agent stream updates`;
|
|
}
|
|
|
|
export async function seedLongMockAgentTimeline(
|
|
options: LongTimelineAgentOptions,
|
|
): Promise<LongTimelineAgent> {
|
|
const agent = await seedMockAgentWorkspace({
|
|
repoPrefix: "timeline-pagination-",
|
|
title: "Timeline pagination regression",
|
|
model: "ten-second-stream",
|
|
});
|
|
|
|
for (let index = 0; index < options.turns; index += 1) {
|
|
await agent.client.sendAgentMessage(agent.agentId, promptForTurn(index));
|
|
await agent.client.waitForFinish(agent.agentId, 15_000);
|
|
}
|
|
|
|
return {
|
|
...agent,
|
|
oldestPrompt: promptForTurn(0),
|
|
newestPrompt: promptForTurn(options.turns - 1),
|
|
};
|
|
}
|
|
|
|
export async function openAgentTimeline(page: Page, agent: LongTimelineAgent): Promise<void> {
|
|
await page.goto(buildAgentRoute(agent.workspaceId, agent.agentId));
|
|
await page.waitForURL(
|
|
(url) => url.pathname.includes("/workspace/") && !url.searchParams.has("open"),
|
|
{ timeout: 60_000 },
|
|
);
|
|
}
|
|
|
|
export async function expectTimelinePromptVisible(page: Page, prompt: string): Promise<void> {
|
|
await expect(page.getByText(prompt, { exact: true })).toBeVisible({ timeout: 30_000 });
|
|
}
|
|
|
|
export async function expectTimelinePromptNotMounted(page: Page, prompt: string): Promise<void> {
|
|
await expect(page.getByText(prompt, { exact: true })).toHaveCount(0);
|
|
}
|
|
|
|
export async function makeLoadedTimelineFitViewport(page: Page): Promise<void> {
|
|
await page.setViewportSize({ width: 1280, height: 8_000 });
|
|
}
|
|
|
|
export async function expectLoadedTimelineDoesNotScroll(page: Page): Promise<void> {
|
|
const scroll = page.locator('[data-testid="agent-chat-scroll"]:visible').first();
|
|
await expect
|
|
.poll(async () =>
|
|
scroll.evaluate((element) => {
|
|
if (!(element instanceof HTMLElement)) {
|
|
throw new Error("Agent chat scroll element is not an HTMLElement");
|
|
}
|
|
return element.scrollHeight <= element.clientHeight;
|
|
}),
|
|
)
|
|
.toBe(true);
|
|
}
|
|
|
|
export async function holdNextOlderTimelinePage(
|
|
page: Page,
|
|
agent: LongTimelineAgent,
|
|
): Promise<AgentTimelineResponseGate & { expectLoading(): Promise<void> }> {
|
|
const gate = await delayAgentOlderTimelineResponse(page, agent.agentId);
|
|
return {
|
|
...gate,
|
|
async expectLoading() {
|
|
await gate.waitForDelayedResponse();
|
|
await expect(page.getByTestId("load-older-history-spinner")).toBeVisible();
|
|
await expectTimelinePromptNotMounted(page, agent.oldestPrompt);
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function scrollTimelineToOldestLoadedEdge(page: Page): Promise<void> {
|
|
const scroll = page.locator('[data-testid="agent-chat-scroll"]:visible').first();
|
|
await scroll.hover();
|
|
await page.mouse.wheel(0, -20_000);
|
|
await page.evaluate(
|
|
() =>
|
|
new Promise<void>((resolve) => {
|
|
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
|
}),
|
|
);
|
|
await scroll.evaluate((element) => {
|
|
if (!(element instanceof HTMLElement)) {
|
|
throw new Error("Agent chat scroll element is not an HTMLElement");
|
|
}
|
|
element.scrollTop = 0;
|
|
element.dispatchEvent(new Event("scroll", { bubbles: true }));
|
|
});
|
|
}
|
|
|
|
export async function scrollTimelineUntilOlderHistoryIsReachable(page: Page): Promise<void> {
|
|
const scroll = page.locator('[data-testid="agent-chat-scroll"]:visible').first();
|
|
const previousHeight = await scroll.evaluate((element) => {
|
|
if (!(element instanceof HTMLElement)) {
|
|
throw new Error("Agent chat scroll element is not an HTMLElement");
|
|
}
|
|
return element.scrollHeight;
|
|
});
|
|
|
|
await page.evaluate(
|
|
() =>
|
|
new Promise<void>((resolve) => {
|
|
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
|
}),
|
|
);
|
|
await scrollTimelineToOldestLoadedEdge(page);
|
|
await expect
|
|
.poll(async () =>
|
|
scroll.evaluate((element) => {
|
|
if (!(element instanceof HTMLElement)) {
|
|
throw new Error("Agent chat scroll element is not an HTMLElement");
|
|
}
|
|
return element.scrollHeight;
|
|
}),
|
|
)
|
|
.toBeGreaterThan(previousHeight);
|
|
await scrollTimelineToOldestLoadedEdge(page);
|
|
}
|