Files
paseo/packages/app/e2e/browser/terminal-retained-tab-stream.spec.ts
T
Mohamed Boudra 9c926ed126 fix(terminal): preserve 10,000 rows with direct ANSI replay (#8282)
Restore configured terminal history on reconnect, retaining 10,000 rows plus the screen. Serialize the worker-owned xterm buffer directly to ANSI and forward negotiated bounded replay chunks while preserving ANSI-era clients and per-viewer live ordering.

Preserve captured geometry through replay, bound pressure recovery to 500 rows, and keep the omitted visible-history default at 200. Remove the full cell-grid copy and obsolete JSON rendering.

All 21 CI checks pass. Real browser, native Debug emulator/simulator and Linux Electron coverage and platform limits are documented in the PR. The Android Debug invalid native delegate dispatch also reproduces on upstream main; its cause and any change in frequency remain unproven.

Credit Manav Dhindsa (DrakenKor), #6139 and #6148, and Lennart Schoch, #6743, for prior ANSI replay and retention work informing this change.

Closes #6137.
Closes #6147.
2026-10-31 10:51:45 +07:00

129 lines
5.0 KiB
TypeScript

import type { Page } from "@playwright/test";
import { expect, test } from "../support/fixtures";
import {
expectTerminalCursor,
getTerminalBufferText,
waitForTerminalContent,
} from "../support/helpers/terminal-perf";
import { TerminalE2EHarness } from "../support/helpers/terminal-dsl";
interface AttachOverlayProbeWindow extends Window {
__terminalAttachOverlaySeen?: boolean;
__terminalAttachOverlayObserver?: MutationObserver;
}
async function watchForTerminalAttachOverlay(page: Page): Promise<void> {
await page.evaluate(() => {
const win = window as AttachOverlayProbeWindow;
win.__terminalAttachOverlaySeen = false;
win.__terminalAttachOverlayObserver?.disconnect();
win.__terminalAttachOverlayObserver = new MutationObserver(() => {
if (document.querySelector('[data-testid="terminal-attach-loading"]')) {
win.__terminalAttachOverlaySeen = true;
}
});
win.__terminalAttachOverlayObserver.observe(document.body, {
childList: true,
subtree: true,
});
});
}
async function terminalAttachOverlayWasSeen(page: Page): Promise<boolean> {
return page.evaluate(() => {
const win = window as AttachOverlayProbeWindow;
win.__terminalAttachOverlayObserver?.disconnect();
return win.__terminalAttachOverlaySeen === true;
});
}
test.describe("retained terminal tab streams", () => {
let harness: TerminalE2EHarness;
test.beforeEach(async () => {
harness = await TerminalE2EHarness.create({ tempPrefix: "terminal-retained-tab-stream-" });
});
test.afterEach(async () => {
await harness.cleanup();
});
test("switching back to a retained terminal does not reattach its stream", async ({ page }) => {
const first = await harness.createTerminal({ name: "retained-first" });
const second = await harness.createTerminal({ name: "retained-second" });
await harness.openTerminal(page, { terminalId: first.id });
const secondTab = page.getByTestId(`workspace-tab-terminal_${second.id}`).first();
await secondTab.click();
await expect(page.getByTestId("terminal-attach-loading")).toBeHidden({ timeout: 10_000 });
await watchForTerminalAttachOverlay(page);
await page.getByTestId(`workspace-tab-terminal_${first.id}`).first().click();
await expect(page.getByTestId("terminal-surface").filter({ visible: true })).toHaveCount(1);
await page.waitForTimeout(100);
expect(await terminalAttachOverlayWasSeen(page)).toBe(false);
});
for (const { count, retainedMarker } of [
{ count: 3000, retainedMarker: "history-0001界🙂" },
{ count: 12050, retainedMarker: "history-3000界🙂" },
]) {
test(`reattach restores retained styled Unicode history from ${count} lines`, async ({
page,
}) => {
await page.addInitScript(() => {
const key = "@paseo:app-settings";
const settings = JSON.parse(localStorage.getItem(key) ?? "{}");
localStorage.setItem(key, JSON.stringify({ ...settings, terminalScrollbackLines: 10000 }));
});
const terminal = await harness.createTerminal({
name: `history-${count}`,
command: "/bin/sh",
args: [
"-lc",
`i=1; while [ $i -le ${count} ]; do printf '\\033[31mhistory-%04d界🙂 payload-abcdefghijklmnopqrstuvwxyz0123456789\\033[0m\\n' $i; i=$((i+1)); done; sleep 120`,
],
});
const last = `history-${String(count).padStart(4, "0")}界🙂`;
await harness.openTerminal(page, { terminalId: terminal.id });
await waitForTerminalContent(page, (text) => text.includes(last), 10000);
expect(await getTerminalBufferText(page)).toContain(retainedMarker);
await page.reload();
await waitForTerminalContent(page, (text) => text.includes(last), 10000);
const restored = await getTerminalBufferText(page);
expect(restored).toContain(retainedMarker);
expect(restored.match(/history-\d{4,5}/g)?.length).toBeGreaterThanOrEqual(
Math.min(count, 10000),
);
if (count > 10000) expect(restored).not.toContain("history-0001界🙂");
});
}
for (const input of [
{ name: "client blinking bar", presentation: "", shape: "bar" as const, blinking: true },
{
name: "program blinking underline",
presentation: "\x1b[3 q",
shape: "underline" as const,
blinking: true,
},
]) {
test(`reattach preserves the ${input.name} cursor`, async ({ page }) => {
const terminal = await harness.createTerminal({
name: "cursor-presentation",
command: process.execPath,
args: [
"-e",
`process.stdout.write(${JSON.stringify(input.presentation + "CURSOR-READY\r\n")});setInterval(()=>{},1000)`,
],
});
await harness.openTerminal(page, { terminalId: terminal.id });
await waitForTerminalContent(page, (text) => text.includes("CURSOR-READY"), 10000);
await expectTerminalCursor(page, input);
await page.reload();
await waitForTerminalContent(page, (text) => text.includes("CURSOR-READY"), 10000);
await expectTerminalCursor(page, input);
});
}
});