






















1-import { chromium, type Browser } from "playwright";
1+import { chromium, type Browser, type Page } from "playwright";
22import { afterAll, beforeAll, describe, expect, it } from "vitest";
33import {
44canRunPlaywrightChromium,
@@ -49,6 +49,13 @@ async function waitForRequests(
4949throw new Error(`Timed out waiting for ${count} ${method} requests`);
5050}
515152+async function chatThreadDistanceFromBottom(page: Page): Promise<number> {
53+return page.locator(".chat-thread").evaluate((element) => {
54+const thread = element as HTMLElement;
55+return Math.round(thread.scrollHeight - thread.scrollTop - thread.clientHeight);
56+});
57+}
58+5259function chatSessionListResponse() {
5360return {
5461count: 2,
@@ -322,6 +329,63 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
322329}
323330});
324331332+it("scrolls a delayed pending send into view before the ACK resolves", async () => {
333+const context = await browser.newContext({
334+locale: "en-US",
335+serviceWorkers: "block",
336+viewport: { height: 900, width: 1280 },
337+});
338+const page = await context.newPage();
339+const baseTs = Date.now() - 100_000;
340+const historyMessages = Array.from({ length: 50 }, (_, index) => ({
341+content: [
342+{
343+text: `History message ${index}\n${"extra transcript line\n".repeat(4)}`,
344+type: "text",
345+},
346+],
347+role: index % 2 === 0 ? "assistant" : "user",
348+timestamp: baseTs + index,
349+}));
350+const gateway = await installMockGateway(page, { historyMessages });
351+352+try {
353+await page.goto(`${server.baseUrl}chat`);
354+await page.getByText("History message 49").waitFor({ timeout: 10_000 });
355+await expect
356+.poll(() => chatThreadDistanceFromBottom(page), { timeout: 10_000 })
357+.toBeLessThanOrEqual(4);
358+359+await page.locator(".chat-thread").evaluate((element) => {
360+(element as HTMLElement).scrollTop = 0;
361+});
362+await expect
363+.poll(() => chatThreadDistanceFromBottom(page), { timeout: 10_000 })
364+.toBeGreaterThan(200);
365+366+await gateway.deferNext("chat.send");
367+368+const prompt = `pending send should scroll before ack\n${"visible now\n".repeat(6)}`;
369+await page.locator(".agent-chat__composer-combobox textarea").fill(prompt);
370+await page.getByRole("button", { name: "Send message" }).click();
371+372+const sendRequest = await gateway.waitForRequest("chat.send");
373+const params = requireRecord(sendRequest.params);
374+const runId = requireString(params.idempotencyKey, "chat send idempotency key");
375+376+await page.locator(".chat-thread").getByText("pending send should scroll").waitFor({
377+timeout: 10_000,
378+});
379+await expect
380+.poll(() => chatThreadDistanceFromBottom(page), { timeout: 10_000 })
381+.toBeLessThanOrEqual(4);
382+383+await gateway.resolveDeferred("chat.send", { runId, status: "started" });
384+} finally {
385+await context.close();
386+}
387+});
388+325389it("keeps rejected pre-ACK sends visible and restores the draft", async () => {
326390const context = await browser.newContext({
327391locale: "en-US",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。