Files
kandev/apps/web/e2e/tests/chat/message-pagination-helpers.ts
T
2026-08-31 09:13:50 +00:00

267 lines
9.8 KiB
TypeScript

import type { Locator, Page } from "@playwright/test";
import type { SeedData } from "../../fixtures/test-base";
import type { ApiClient } from "../../helpers/api-client";
export const TASK_DESCRIPTION_MARKER = "TASK-DESCRIPTION-FALLBACK-9R4M";
export const INITIAL_PROMPT_MARKER = "INITIAL-PROMPT-MARKER-7Q2X";
export const RECENT_AGENT_MARKER = "RECENT-AGENT-MARKER-4K8P";
export const PRE_PROMPT_MARKER = "HIDDEN-PRE-PROMPT-MARKER-6N3V";
export const EAGER_HISTORY_PROMPT_MARKER = "EAGER-HISTORY-PROMPT-MARKER-3J6W";
export const VISIBLE_PAGE_MARKER = "VISIBLE-PAGE-MARKER-8D5H";
export const SHORT_PAGE_BOUNDARY_MARKER = "SHORT-PAGE-BOUNDARY-MARKER-5T1C";
export const DEEP_PROMPT_MARKER = "DEEP-PROMPT-MARKER-2P7N";
export const LONG_HISTORY_TAIL_MARKER = "LONG-HISTORY-TAIL-MARKER-6V4R";
/** Seeds an older prompt followed by a tool-only newest window. */
export async function seedToolHeavyOpeningHistory(
apiClient: ApiClient,
seedData: SeedData,
title: string,
): Promise<{ taskId: string; sessionId: string }> {
const task = await apiClient.createTask(seedData.workspaceId, title, {
description: TASK_DESCRIPTION_MARKER,
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
const { session_id: sessionId } = await apiClient.seedTaskSession(task.id, {
state: "IDLE",
repositoryId: seedData.repositoryId,
});
await apiClient.seedSessionMessage(sessionId, {
type: "message",
content: EAGER_HISTORY_PROMPT_MARKER,
authorType: "user",
});
await apiClient.seedTaskSession(task.id, {
sessionId,
state: "IDLE",
commandCount: 150,
});
return { taskId: task.id, sessionId };
}
/** Seeds a prompt more than twenty older pages behind a bounded tool-only
* newest window. All newer activity shares one turn so collapsed rendering
* keeps the sentinel in preload while the cursor pages are committed. */
export async function seedLongMessageHistory(
apiClient: ApiClient,
seedData: SeedData,
title: string,
): Promise<{ taskId: string; sessionId: string }> {
const task = await apiClient.createTask(seedData.workspaceId, title, {
description: TASK_DESCRIPTION_MARKER,
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
const { session_id: sessionId } = await apiClient.seedTaskSession(task.id, {
state: "IDLE",
repositoryId: seedData.repositoryId,
});
await apiClient.seedSessionMessage(sessionId, {
type: "message",
content: INITIAL_PROMPT_MARKER,
authorType: "user",
});
await apiClient.seedSessionMessage(sessionId, {
type: "message",
content: DEEP_PROMPT_MARKER,
authorType: "user",
});
await apiClient.seedToolCallMessages(sessionId, 520, { status: "complete" });
await apiClient.seedSessionMessage(sessionId, {
type: "message",
content: LONG_HISTORY_TAIL_MARKER,
});
// Keep the initial bounded window scrollable while the collapsed tool group
// above the anchor remains height-stable across older-page commits.
await apiClient.seedAgentMessages(sessionId, 20, "LONG-HISTORY-VISIBLE-TAIL");
return { taskId: task.id, sessionId };
}
/** Captures older-page requests made after this watcher is installed. */
export function watchOlderMessageRequests(page: Page, sessionId: string): string[] {
const requests: string[] = [];
page.on("request", (request) => {
const url = request.url();
if (url.includes(`/task-sessions/${sessionId}/messages?`) && url.includes("before=")) {
requests.push(url);
}
});
return requests;
}
/** Seeds collapsed history around the visible prompt boundary. */
export async function seedCollapsedMessageHistory(
apiClient: ApiClient,
seedData: SeedData,
title: string,
options?: { promptOutsideInitialWindow?: boolean },
): Promise<{ taskId: string; sessionId: string }> {
const task = await apiClient.createTask(seedData.workspaceId, title, {
description: TASK_DESCRIPTION_MARKER,
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
const { session_id: sessionId } = await apiClient.seedTaskSession(task.id, {
state: "IDLE",
repositoryId: seedData.repositoryId,
});
if (options?.promptOutsideInitialWindow) {
await apiClient.seedSessionMessage(sessionId, {
type: "message",
content: INITIAL_PROMPT_MARKER,
authorType: "user",
});
await apiClient.seedTaskSession(task.id, {
sessionId,
state: "IDLE",
});
await apiClient.seedToolCallMessages(sessionId, 140, { status: "complete" });
} else {
for (let i = 0; i < 20; i += 1) {
await apiClient.seedSessionMessage(sessionId, {
type: "tool_call",
content: `${PRE_PROMPT_MARKER} ${i + 1}`,
});
}
await apiClient.seedSessionMessage(sessionId, {
type: "message",
content: INITIAL_PROMPT_MARKER,
authorType: "user",
});
await apiClient.seedTaskSession(task.id, {
sessionId,
state: "IDLE",
commandCount: 0,
});
// Keep the prompt at the oldest edge of the initial 100-message window:
// 98 collapsed rows plus the recent agent row follow it, while the 20
// pre-prompt rows remain on the next backend page.
await apiClient.seedToolCallMessages(sessionId, 98);
}
await apiClient.seedSessionMessage(sessionId, {
type: "message",
content: RECENT_AGENT_MARKER,
});
if (options?.promptOutsideInitialWindow) {
await apiClient.seedAgentMessages(sessionId, 20, "RECENT-VISIBLE-TAIL");
}
return { taskId: task.id, sessionId };
}
/** Seeds several backend pages of standalone messages behind the newest window. */
export async function seedVisibleMessageHistory(
apiClient: ApiClient,
seedData: SeedData,
title: string,
): Promise<{ taskId: string; sessionId: string }> {
const task = await apiClient.createTask(seedData.workspaceId, title, {
description: TASK_DESCRIPTION_MARKER,
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
const { session_id: sessionId } = await apiClient.seedTaskSession(task.id, {
state: "IDLE",
repositoryId: seedData.repositoryId,
});
await apiClient.seedSessionMessage(sessionId, {
type: "message",
content: INITIAL_PROMPT_MARKER,
authorType: "user",
});
await apiClient.seedAgentMessages(sessionId, 140, VISIBLE_PAGE_MARKER);
return { taskId: task.id, sessionId };
}
/** Seeds a short standalone boundary page followed by a collapsed page. The
* first older page remains inside the sentinel preload margin, so the native
* transcript must continue once before the second page moves it out. */
export async function seedShortBoundaryPageHistory(
apiClient: ApiClient,
seedData: SeedData,
title: string,
): Promise<{ taskId: string; sessionId: string }> {
const task = await apiClient.createTask(seedData.workspaceId, title, {
description: TASK_DESCRIPTION_MARKER,
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
const { session_id: sessionId } = await apiClient.seedTaskSession(task.id, {
state: "IDLE",
repositoryId: seedData.repositoryId,
});
await apiClient.seedAgentMessages(sessionId, 20, "SHORT-PAGE-OLDER-FILLER");
await apiClient.seedSessionMessage(sessionId, {
type: "message",
content: SHORT_PAGE_BOUNDARY_MARKER,
});
for (let index = 0; index < 19; index += 1) {
await apiClient.seedSessionMessage(sessionId, {
type: "tool_call",
content: `short-page completed tool ${index + 1}`,
metadata: { status: "complete" },
});
}
await apiClient.seedAgentMessages(sessionId, 100, VISIBLE_PAGE_MARKER);
return { taskId: task.id, sessionId };
}
/** Reads a rendered standalone message's viewport position from the list. */
export async function readStandaloneMessageTop(list: Locator, marker: string): Promise<number> {
return list.evaluate((element, messageMarker) => {
const row = Array.from(element.querySelectorAll<HTMLElement>("[id^='msg-']")).find(
(candidate) => candidate.textContent?.includes(messageMarker),
);
return row?.getBoundingClientRect().top ?? Number.NaN;
}, marker);
}
/** Reads one already-rendered message row by its stable DOM id. */
export async function readMessageRowTopById(list: Locator, rowId: string): Promise<number> {
return list.evaluate((element, id) => {
const row = Array.from(element.querySelectorAll<HTMLElement>("[id^='msg-']")).find(
(candidate) => candidate.id === id,
);
return row?.getBoundingClientRect().top ?? Number.NaN;
}, rowId);
}
/** Scrolls the native transcript to the oldest loaded edge and captures the
* anchored row's position before the next older-page request can prepend. */
export async function scrollToOldestLoadedEdge(
list: Locator,
marker: string,
): Promise<{ rowId: string | null; rowTop: number; scrollHeight: number }> {
return list.evaluate((element, messageMarker) => {
element.scrollTop = 0;
element.dispatchEvent(new Event("scroll", { bubbles: true }));
const row = Array.from(element.querySelectorAll<HTMLElement>("[id^='msg-']")).find(
(candidate) => candidate.textContent?.includes(messageMarker),
);
return {
rowId: row?.id ?? null,
rowTop: row?.getBoundingClientRect().top ?? Number.NaN,
scrollHeight: element.scrollHeight,
};
}, marker);
}
/** Applies a small upward movement after prepend restoration. */
export async function scrollUpSlightly(list: Locator): Promise<number> {
return list.evaluate((element) => {
const previous = element.scrollTop;
element.scrollTop = Math.max(0, previous - 24);
element.dispatchEvent(new Event("scroll", { bubbles: true }));
return previous - element.scrollTop;
});
}