136 lines
4.6 KiB
TypeScript
136 lines
4.6 KiB
TypeScript
import type { Page } from "@playwright/test";
|
|
import { test, expect } from "./fixtures";
|
|
import { TerminalE2EHarness, withTerminalInApp } from "./helpers/terminal-dsl";
|
|
import {
|
|
installTerminalRenderProbe,
|
|
readTerminalRenderProbe,
|
|
resetTerminalRenderProbe,
|
|
startTerminalFrameSampling,
|
|
summarizeTerminalRenderProbe,
|
|
} from "./helpers/terminal-probes";
|
|
import { getTerminalBufferText, waitForTerminalContent } from "./helpers/terminal-perf";
|
|
|
|
async function waitForAlternateScreenExit(page: Page, afterAlt: string, timeout: number) {
|
|
let lastBufferText = "";
|
|
let lastProbe = await readTerminalRenderProbe(page);
|
|
|
|
try {
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
lastBufferText = await getTerminalBufferText(page);
|
|
lastProbe = await readTerminalRenderProbe(page);
|
|
return (
|
|
lastProbe.altEnterWrites > 0 &&
|
|
lastProbe.altExitWrites > 0 &&
|
|
lastBufferText.includes(afterAlt)
|
|
);
|
|
},
|
|
{
|
|
intervals: [50],
|
|
message: `wait for alternate-screen exit and ${afterAlt} output`,
|
|
timeout,
|
|
},
|
|
)
|
|
.toBe(true);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
throw new Error(
|
|
`Timed out waiting for alternate-screen exit: ${message}\n${JSON.stringify(
|
|
{
|
|
afterAlt,
|
|
probe: summarizeTerminalRenderProbe(lastProbe),
|
|
bufferTextTail: lastBufferText.slice(-500),
|
|
},
|
|
null,
|
|
2,
|
|
)}`,
|
|
);
|
|
}
|
|
|
|
return lastProbe;
|
|
}
|
|
|
|
test.describe("Terminal alternate-screen transitions", () => {
|
|
let harness: TerminalE2EHarness;
|
|
|
|
test.beforeAll(async () => {
|
|
harness = await TerminalE2EHarness.create({ tempPrefix: "terminal-alt-" });
|
|
});
|
|
|
|
test.afterAll(async () => {
|
|
await harness?.cleanup();
|
|
});
|
|
|
|
test("restores the normal screen after full-screen alternate buffer exit without remounting", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
test.setTimeout(60_000);
|
|
|
|
await installTerminalRenderProbe(page);
|
|
|
|
await withTerminalInApp(page, harness, { name: "alternate-screen" }, async () => {
|
|
await harness.setupPrompt(page);
|
|
|
|
const terminal = harness.terminalSurface(page);
|
|
const historyReady = `HISTORY_READY_${Date.now()}`;
|
|
await terminal.pressSequentially(
|
|
`for i in $(seq 1 80); do echo HISTORY_$i; done; echo ${historyReady}\n`,
|
|
{ delay: 0 },
|
|
);
|
|
await waitForTerminalContent(page, (text) => text.includes(historyReady), 10_000);
|
|
|
|
await resetTerminalRenderProbe(page);
|
|
await page.waitForTimeout(500);
|
|
const settledProbe = await readTerminalRenderProbe(page);
|
|
expect(settledProbe.resetWrites, "terminal should be idle before alternate-screen act").toBe(
|
|
0,
|
|
);
|
|
await resetTerminalRenderProbe(page);
|
|
|
|
const afterAlt = `AFTER_ALT_${Date.now()}`;
|
|
await startTerminalFrameSampling(page);
|
|
await terminal.pressSequentially(
|
|
`printf '\\033[?1049h\\033[2J\\033[HALT_SCREEN_TOP\\n'; sleep 0.25; printf '\\033[?1049l'; echo ${afterAlt}\n`,
|
|
{ delay: 0 },
|
|
);
|
|
const probe = await waitForAlternateScreenExit(page, afterAlt, 10_000);
|
|
const probeSummary = summarizeTerminalRenderProbe(probe);
|
|
|
|
await testInfo.attach("alternate-screen-probe", {
|
|
body: JSON.stringify({ summary: probeSummary, probe }, null, 2),
|
|
contentType: "application/json",
|
|
});
|
|
|
|
expect(probe.setCount, "terminal instance should not be replaced after attach").toBe(0);
|
|
expect(probe.unsetCount, "terminal instance should not be unset after attach").toBe(0);
|
|
expect(
|
|
probe.altEnterWrites,
|
|
"test command should enter the alternate screen",
|
|
).toBeGreaterThan(0);
|
|
expect(probe.altExitWrites, "test command should exit the alternate screen").toBeGreaterThan(
|
|
0,
|
|
);
|
|
expect(probe.resetWrites, "alternate-screen exit should not replay a snapshot reset").toBe(0);
|
|
|
|
const finalBufferText = await getTerminalBufferText(page);
|
|
expect(finalBufferText).toContain(historyReady);
|
|
expect(finalBufferText).toContain(afterAlt);
|
|
|
|
const suspiciousFrames = probe.frames.filter(
|
|
(frame) =>
|
|
frame.text.includes("$") &&
|
|
!frame.text.includes(historyReady) &&
|
|
!frame.text.includes(afterAlt) &&
|
|
frame.nonEmptyRows <= 2 &&
|
|
(frame.firstNonEmptyRow ?? Number.POSITIVE_INFINITY) <= 1,
|
|
);
|
|
|
|
expect(
|
|
suspiciousFrames,
|
|
"normal-screen restore should not flash to a mostly blank prompt-at-top frame",
|
|
).toEqual([]);
|
|
});
|
|
});
|
|
});
|