153 lines
6.6 KiB
TypeScript
153 lines
6.6 KiB
TypeScript
/**
|
|
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
|
* @license MIT
|
|
*
|
|
* WebGL "heavy TUI" stress tests: large scrollback + scrolling, alternate-buffer
|
|
* full-screen redraws (like an agent CLI animating output), high-volume
|
|
* rendering and replay of recorded real-world TUI sessions (vim, colored ls).
|
|
*
|
|
* The shared invariant: a known reference row of glyphs, captured in a fresh
|
|
* atlas, must still render identically after the stress workload. If the WebGL
|
|
* texture atlas is corrupted by the workload the reference glyphs change and the
|
|
* test fails. See microsoft/vscode#322756.
|
|
*
|
|
* Chromium + SwiftShader only (additive scope; Firefox/WebKit WebGL are broken
|
|
* in CI per xtermjs#5854).
|
|
*/
|
|
|
|
import { readFileSync } from 'fs';
|
|
import test, { expect } from '@playwright/test';
|
|
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
|
|
import {
|
|
assertTextureAtlasPresent,
|
|
captureRowSignatures,
|
|
expectSignatureMatches,
|
|
loadWebglStrict,
|
|
waitForRender,
|
|
writeAndWaitForRender
|
|
} from '../../../test/playwright/RendererTestUtils';
|
|
import { generateMixedGlyphBlock, generateSyntheticTuiFrames, generateUniqueGlyphFlood } from '../../../test/playwright/SyntheticTui';
|
|
|
|
const REFERENCE_COLS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
const REFERENCE_LINE = 'Ref0123456789ABCDEF';
|
|
|
|
let ctx: ITestContext;
|
|
test.beforeAll(async ({ browser }) => {
|
|
ctx = await createTestContext(browser);
|
|
});
|
|
test.afterAll(async () => await ctx.page.close());
|
|
|
|
test.describe('WebGL heavy TUI / scrolling stress', () => {
|
|
test.skip(({ browserName }) => browserName !== 'chromium', 'WebGL stress tests run on Chromium/SwiftShader only');
|
|
|
|
test.beforeEach(async () => {
|
|
await openTerminal(ctx, { cols: 80, rows: 24, scrollback: 5000 });
|
|
await loadWebglStrict(ctx);
|
|
});
|
|
|
|
test('reference glyphs survive heavy scrolling through large scrollback', async () => {
|
|
await writeAndWaitForRender(ctx, `${REFERENCE_LINE}\r\n`);
|
|
const reference = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
|
|
|
// Push the reference far up into scrollback with thousands of varied glyphs.
|
|
for (let i = 0; i < 20; i++) {
|
|
await ctx.proxy.write(generateMixedGlyphBlock(i + 1, 30, 70));
|
|
}
|
|
await waitForRender(ctx);
|
|
|
|
// Scroll around, then back to the very top where the reference lives.
|
|
await ctx.proxy.scrollToTop();
|
|
await waitForRender(ctx);
|
|
await ctx.proxy.scrollPages(3);
|
|
await waitForRender(ctx);
|
|
await ctx.proxy.scrollToTop();
|
|
await waitForRender(ctx);
|
|
|
|
const after = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
|
for (let i = 0; i < REFERENCE_COLS.length; i++) {
|
|
expectSignatureMatches(after[i], reference[i], 14, `reference glyph at col ${REFERENCE_COLS[i]} corrupted after heavy scrolling`);
|
|
}
|
|
});
|
|
|
|
test('alternate-buffer TUI redraws do not corrupt the restored primary buffer', async () => {
|
|
// Reference content lives in the PRIMARY buffer.
|
|
await writeAndWaitForRender(ctx, `${REFERENCE_LINE}`);
|
|
const reference = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
|
|
|
// Animate a synthetic agent TUI in the alternate buffer.
|
|
const frames = generateSyntheticTuiFrames({ seed: 7, frames: 30, cols: 80, rows: 24, useAltBuffer: true });
|
|
for (const frame of frames) {
|
|
await ctx.proxy.write(frame);
|
|
}
|
|
await waitForRender(ctx);
|
|
await assertTextureAtlasPresent(ctx);
|
|
|
|
// Back in the primary buffer the reference glyphs must be intact.
|
|
await ctx.proxy.refresh(0, (await ctx.proxy.rows) - 1);
|
|
await waitForRender(ctx);
|
|
const after = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
|
for (let i = 0; i < REFERENCE_COLS.length; i++) {
|
|
expectSignatureMatches(after[i], reference[i], 14, `primary-buffer glyph at col ${REFERENCE_COLS[i]} corrupted by alt-buffer TUI redraws`);
|
|
}
|
|
});
|
|
|
|
test('reference glyphs survive a heavy mixed render + unique-glyph churn', async () => {
|
|
// Capture the reference rendering of a known line in a fresh atlas.
|
|
await writeAndWaitForRender(ctx, `${REFERENCE_LINE}`);
|
|
const reference = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
|
|
|
// High-volume rendering: synthetic TUI frames (which clear/redraw the screen)
|
|
// plus a large unique-glyph flood to churn the atlas, mimicking a long agent
|
|
// CLI session.
|
|
const frames = generateSyntheticTuiFrames({ seed: 3, frames: 40, cols: 80, rows: 24 });
|
|
for (const frame of frames) {
|
|
await ctx.proxy.write(frame);
|
|
}
|
|
await ctx.proxy.write(generateUniqueGlyphFlood(3000, 80));
|
|
await waitForRender(ctx);
|
|
|
|
// The synthetic frames erase the screen, so re-render the reference line and
|
|
// assert the (heavily churned) atlas still draws those glyphs correctly.
|
|
await writeAndWaitForRender(ctx, `\x1b[2J\x1b[H${REFERENCE_LINE}`);
|
|
const after = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
|
for (let i = 0; i < REFERENCE_COLS.length; i++) {
|
|
expectSignatureMatches(after[i], reference[i], 14, `reference glyph at col ${REFERENCE_COLS[i]} corrupted after heavy render`);
|
|
}
|
|
});
|
|
|
|
test('replaying recorded TUI sessions renders without errors or atlas corruption', async () => {
|
|
const pageErrors: string[] = [];
|
|
const onError = (err: Error): void => {
|
|
pageErrors.push(err.message);
|
|
};
|
|
ctx.page.on('pageerror', onError);
|
|
try {
|
|
const fixtures = [
|
|
'./test/fixtures/escape_sequence_files/t0090-alt_screen.in',
|
|
'./test/fixtures/escape_sequence_files/t0502-bash_ls_color.in',
|
|
'./test/fixtures/escape_sequence_files/t0500-bash_long_line.in',
|
|
'./test/fixtures/escape_sequence_files/t0504-vim.in'
|
|
];
|
|
for (const fixture of fixtures) {
|
|
const data = readFileSync(fixture, { encoding: 'latin1' });
|
|
await ctx.proxy.write(data);
|
|
}
|
|
await waitForRender(ctx);
|
|
await assertTextureAtlasPresent(ctx);
|
|
|
|
// Re-rendering the final frame must be idempotent (self-consistent): if the
|
|
// atlas were corrupted, a forced redraw would differ from what's shown.
|
|
const before = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
|
await ctx.proxy.refresh(0, (await ctx.proxy.rows) - 1);
|
|
await waitForRender(ctx);
|
|
const after = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
|
for (let i = 0; i < REFERENCE_COLS.length; i++) {
|
|
expectSignatureMatches(after[i], before[i], 14, `fixture replay frame not self-consistent at col ${REFERENCE_COLS[i]}`);
|
|
}
|
|
expect(pageErrors, `no page errors during fixture replay, got: ${pageErrors.join(', ')}`).toEqual([]);
|
|
} finally {
|
|
ctx.page.off('pageerror', onError);
|
|
}
|
|
});
|
|
});
|