Files
xterm.js/addons/addon-webgl/test/WebglSingleTermStress.test.ts
T

100 lines
4.1 KiB
TypeScript

/**
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
* @license MIT
*
* Single-terminal WebGL atlas stress: try to reproduce the "garbled glyph"
* corruption from microsoft/vscode#322756 in ONE terminal (no shared atlas),
* via heavy unique-glyph churn that forces atlas page merges, both with content
* pinned on screen (TUI-style) and mid-scroll. Chromium/SwiftShader only.
*/
import test, { expect } from '@playwright/test';
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
import {
captureRowSignatures,
expectSignatureMatches,
floodDistinctGlyphs,
getAtlasPageCount,
loadWebglStrict,
setMaxAtlasPages,
waitForRender,
writeAndWaitForRender
} from '../../../test/playwright/RendererTestUtils';
import { generateUniqueGlyphFlood } from '../../../test/playwright/SyntheticTui';
let ctx: ITestContext;
test.beforeAll(async ({ browser }) => {
ctx = await createTestContext(browser);
});
test.afterAll(async () => await ctx.page.close());
test.describe('WebGL single-terminal heavy render + merge', () => {
test.skip(({ browserName }) => browserName !== 'chromium', 'WebGL atlas tests run on Chromium/SwiftShader only');
test.describe.configure({ timeout: 30000 });
test.beforeEach(async () => {
await openTerminal(ctx, { cols: 80, rows: 24, scrollback: 5000 });
await loadWebglStrict(ctx);
});
test('heavy merge churn must not crash the GlyphRenderer (xtermjs#322756)', async () => {
// EXPECTED RED: with a small page cap, atlas merges shrink the pages array
// while GlyphRenderer.render still indexes the old page count -> throws
// "Cannot read properties of undefined (reading 'version')" and the terminal
// garbles. Capture the crash via pageerror.
const errors: string[] = [];
const onError = (e: Error): void => { errors.push(e.message); };
ctx.page.on('pageerror', onError);
try {
await setMaxAtlasPages(ctx, 4);
await floodDistinctGlyphs(ctx, 10);
// Force a GPU read (screenshot) mid-churn so a render lands during/after a
// merge, then churn more — this reliably exposes the version crash.
await captureRowSignatures(ctx, 1, [1, 2, 3]);
const pages = await floodDistinctGlyphs(ctx, 10);
expect(pages, 'should have allocated/merged pages').toBeGreaterThan(1);
await ctx.page.waitForTimeout(300);
expect(errors, `renderer crashed during atlas merges: ${errors[0] ?? ''}`).toEqual([]);
} finally {
ctx.page.off('pageerror', onError);
}
});
test('pinned header stays correct while body churns many pages', async () => {
const cols = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
await writeAndWaitForRender(ctx, 'HEADER_REF_0123456789');
const reference = await captureRowSignatures(ctx, 1, cols);
// Body churns unique glyphs (forces merges) while header line stays on screen.
const pages = await floodDistinctGlyphs(ctx, 8);
expect(pages, 'should have allocated/merged pages').toBeGreaterThan(1);
await ctx.proxy.scrollToTop();
await waitForRender(ctx);
const after = await captureRowSignatures(ctx, 1, cols);
for (let i = 0; i < cols.length; i++) {
expectSignatureMatches(after[i], reference[i], 14, `header glyph col ${cols[i]} corrupted by single-terminal merges`);
}
});
test('visible rows match content while scrolling (no merge)', async () => {
// Big scrollback of distinct glyphs across multiple pages (no tiny page cap,
// so no merge), then scroll up/down repeatedly.
for (let c = 0; c < 8; c++) {
await writeAndWaitForRender(ctx, generateUniqueGlyphFlood(24 * 40, 80, c * 24 * 40));
}
expect(await getAtlasPageCount(ctx)).toBeGreaterThan(1);
await ctx.proxy.scrollToTop();
await waitForRender(ctx);
const top = await captureRowSignatures(ctx, 1, [1, 2, 3, 4, 5]);
await ctx.proxy.scrollToBottom();
await ctx.proxy.scrollToTop();
await waitForRender(ctx);
const topAgain = await captureRowSignatures(ctx, 1, [1, 2, 3, 4, 5]);
for (let i = 0; i < top.length; i++) {
expectSignatureMatches(topAgain[i], top[i], 14, `row1 col${i + 1} changed after scroll round-trip`);
}
});
});