fix(e2e): parametrize reply line count and improve scroll behavior

This commit is contained in:
Mohamed Boudra
2026-03-13 13:43:34 +07:00
parent 7c18616160
commit 049edbfcaf
2 changed files with 33 additions and 39 deletions
+2 -1
View File
@@ -104,6 +104,7 @@ test("sticky mode stays pinned through composer growth and viewport resize, but
cwd: repo.path, cwd: repo.path,
title: `bottom-anchor-sticky-${Date.now()}`, title: `bottom-anchor-sticky-${Date.now()}`,
turnCount: 10, turnCount: 10,
lineCount: 24,
}); });
await page.setViewportSize({ width: 1320, height: 920 }); await page.setViewportSize({ width: 1320, height: 920 });
@@ -123,7 +124,7 @@ test("sticky mode stays pinned through composer growth and viewport resize, but
await expectNearBottom(page); await expectNearBottom(page);
await expect(page.getByTestId("scroll-to-bottom-button")).toHaveCount(0); await expect(page.getByTestId("scroll-to-bottom-button")).toHaveCount(0);
await page.setViewportSize({ width: 820, height: 760 }); await page.setViewportSize({ width: 1040, height: 760 });
await expect(page.getByTestId(`workspace-tab-agent_${agent.id}`).first()).toBeVisible({ await expect(page.getByTestId(`workspace-tab-agent_${agent.id}`).first()).toBeVisible({
timeout: 30000, timeout: 30000,
}); });
+31 -38
View File
@@ -66,17 +66,17 @@ function buildReplyBlock(label: string, lineCount = 14): string {
}).join("\n"); }).join("\n");
} }
function buildProtocolMessage(label: string): string { function buildProtocolMessage(label: string, lineCount = 14): string {
return [ return [
"For every message in this chat, reply with exactly the text after the final line `REPLY:`.", "For every message in this chat, reply with exactly the text after the final line `REPLY:`.",
"Do not add extra words, bullets, markdown fences, or tool calls.", "Do not add extra words, bullets, markdown fences, or tool calls.",
"REPLY:", "REPLY:",
buildReplyBlock(label), buildReplyBlock(label, lineCount),
].join("\n"); ].join("\n");
} }
function buildReplyMessage(label: string): string { function buildReplyMessage(label: string, lineCount = 14): string {
return ["REPLY:", buildReplyBlock(label)].join("\n"); return ["REPLY:", buildReplyBlock(label, lineCount)].join("\n");
} }
export function createReplyTurn(label: string): { export function createReplyTurn(label: string): {
@@ -124,9 +124,11 @@ export async function seedBottomAnchorAgent(input: {
cwd: string; cwd: string;
title?: string; title?: string;
turnCount?: number; turnCount?: number;
lineCount?: number;
}): Promise<SeededAgent> { }): Promise<SeededAgent> {
const title = input.title ?? `bottom-anchor-${Date.now()}`; const title = input.title ?? `bottom-anchor-${Date.now()}`;
const turnCount = Math.max(3, input.turnCount ?? 5); const turnCount = Math.max(3, input.turnCount ?? 5);
const lineCount = Math.max(14, input.lineCount ?? 14);
const created = await input.client.createAgent({ const created = await input.client.createAgent({
provider: "codex", provider: "codex",
model: "gpt-5.1-codex-mini", model: "gpt-5.1-codex-mini",
@@ -134,7 +136,7 @@ export async function seedBottomAnchorAgent(input: {
modeId: "full-access", modeId: "full-access",
cwd: input.cwd, cwd: input.cwd,
title, title,
initialPrompt: buildProtocolMessage(`${title}-turn-00`), initialPrompt: buildProtocolMessage(`${title}-turn-00`, lineCount),
}); });
const initialFinish = await input.client.waitForFinish(created.id, 120000); const initialFinish = await input.client.waitForFinish(created.id, 120000);
if (initialFinish.status !== "idle") { if (initialFinish.status !== "idle") {
@@ -143,11 +145,11 @@ export async function seedBottomAnchorAgent(input: {
); );
} }
let expectedTailText = buildReplyBlock(`${title}-turn-00`); let expectedTailText = buildReplyBlock(`${title}-turn-00`, lineCount);
for (let index = 1; index < turnCount; index += 1) { for (let index = 1; index < turnCount; index += 1) {
const label = `${title}-turn-${index.toString().padStart(2, "0")}`; const label = `${title}-turn-${index.toString().padStart(2, "0")}`;
expectedTailText = buildReplyBlock(label); expectedTailText = buildReplyBlock(label, lineCount);
await input.client.sendAgentMessage(created.id, buildReplyMessage(label)); await input.client.sendAgentMessage(created.id, buildReplyMessage(label, lineCount));
const finish = await input.client.waitForFinish(created.id, 120000); const finish = await input.client.waitForFinish(created.id, 120000);
if (finish.status !== "idle") { if (finish.status !== "idle") {
throw new Error( throw new Error(
@@ -209,36 +211,27 @@ export async function readScrollMetrics(page: Page): Promise<ScrollMetrics> {
export async function scrollUpFromBottom(page: Page, pixels: number): Promise<void> { export async function scrollUpFromBottom(page: Page, pixels: number): Promise<void> {
const scrollViewport = getVisibleChatScroll(page); const scrollViewport = getVisibleChatScroll(page);
await expect(scrollViewport).toHaveCount(1, { timeout: 30000 }); await expect(scrollViewport).toHaveCount(1, { timeout: 30000 });
const targetDistanceFromBottom = Math.max( let remaining = Math.max(0, pixels);
NEAR_BOTTOM_THRESHOLD_PX + 24, while (remaining > 0) {
Math.max(0, pixels) const delta = Math.min(240, remaining);
); await scrollViewport.evaluate((element: Element, step: number) => {
await scrollViewport.evaluate( const scrollContainer = element as HTMLElement;
(root: Element, input: { targetDistanceFromBottom: number }) => { scrollContainer.dispatchEvent(
const candidates = [root, ...Array.from(root.querySelectorAll("*"))] new WheelEvent("wheel", {
.filter((element): element is HTMLElement => element instanceof HTMLElement) deltaY: -step,
.filter((element) => { bubbles: true,
const tagName = element.tagName.toLowerCase(); cancelable: true,
const isEditable = })
tagName === "textarea" || );
tagName === "input" || scrollContainer.scrollTop = Math.max(0, scrollContainer.scrollTop - step);
element.getAttribute("contenteditable") === "true"; scrollContainer.dispatchEvent(new Event("scroll", { bubbles: true }));
return !isEditable && element.scrollHeight - element.clientHeight > 1; }, delta);
}); remaining -= delta;
const scrollElement =
candidates.sort( if ((await readScrollMetrics(page)).distanceFromBottom > NEAR_BOTTOM_THRESHOLD_PX) {
(left, right) => return;
right.scrollHeight - }
right.clientHeight - }
(left.scrollHeight - left.clientHeight)
)[0] ?? (root as HTMLElement);
const maxOffset = Math.max(0, scrollElement.scrollHeight - scrollElement.clientHeight);
const nextOffset = Math.max(0, maxOffset - input.targetDistanceFromBottom);
scrollElement.scrollTop = nextOffset;
scrollElement.dispatchEvent(new Event("scroll", { bubbles: true }));
},
{ targetDistanceFromBottom }
);
} }
export async function waitForAgentReady(page: Page, expectedTailText?: string): Promise<void> { export async function waitForAgentReady(page: Page, expectedTailText?: string): Promise<void> {