webgl stress + glyph-integrity tests for atlas merges; repro vscode#322756 single-terminal garble
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* WebGL device-pixel-ratio (HiDPI) and font-size ("resize") atlas tests.
|
||||
*
|
||||
* A devicePixelRatio > 1 and font-size changes both change the device cell
|
||||
* dimensions, which key the texture atlas cache and force the atlas to be
|
||||
* rebuilt. Historically these paths have produced blank/oversized/garbled glyphs
|
||||
* (e.g. xtermjs#4728, #5915/#5929). These tests assert glyphs render and stay
|
||||
* correct across those transitions.
|
||||
*
|
||||
* Chromium + SwiftShader only (additive scope; Firefox/WebKit WebGL broken in CI
|
||||
* per xtermjs#5854).
|
||||
*/
|
||||
|
||||
import test, { Browser, expect } from '@playwright/test';
|
||||
import { ITestContext, TerminalProxy, openTerminal } from '../../../test/playwright/TestUtils';
|
||||
import {
|
||||
assertTextureAtlasPresent,
|
||||
captureRowSignatures,
|
||||
expectSignatureMatches,
|
||||
loadWebglStrict,
|
||||
waitForRender,
|
||||
writeAndWaitForRender
|
||||
} from '../../../test/playwright/RendererTestUtils';
|
||||
|
||||
const REFERENCE_COLS = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
|
||||
/**
|
||||
* Creates a test context whose page uses a specific devicePixelRatio. The shared
|
||||
* `createTestContext` helper always uses DPR 1 (`browser.newPage()`), so HiDPI
|
||||
* tests build their own context here.
|
||||
*/
|
||||
async function createDprContext(browser: Browser, deviceScaleFactor: number): Promise<ITestContext> {
|
||||
const context = await browser.newContext({ deviceScaleFactor });
|
||||
const page = await context.newPage();
|
||||
page.on('pageerror', e => console.error(`[dpr ${deviceScaleFactor}]`, e));
|
||||
await page.goto('/test');
|
||||
const proxy = new TerminalProxy(page);
|
||||
proxy.initPage();
|
||||
return {
|
||||
browser,
|
||||
page,
|
||||
termHandle: await page.evaluateHandle('window.term'),
|
||||
proxy
|
||||
};
|
||||
}
|
||||
|
||||
test.describe('WebGL HiDPI (devicePixelRatio) rendering', () => {
|
||||
test.skip(({ browserName }) => browserName !== 'chromium', 'WebGL atlas tests run on Chromium/SwiftShader only');
|
||||
|
||||
test('initializes the WebGL renderer and scales to device pixels at devicePixelRatio 2', async ({ browser }) => {
|
||||
const ctx = await createDprContext(browser, 2);
|
||||
try {
|
||||
await openTerminal(ctx, { cols: 80, rows: 24 });
|
||||
// GPU must be genuinely active at HiDPI (asserted inside loadWebglStrict).
|
||||
await loadWebglStrict(ctx);
|
||||
await writeAndWaitForRender(ctx, 'A W .');
|
||||
await assertTextureAtlasPresent(ctx);
|
||||
|
||||
// The WebGL renderer must render at the device resolution: device cell
|
||||
// dimensions should be ~2x the CSS cell dimensions. This catches the
|
||||
// DPR-scaling class of bugs (e.g. xtermjs#4728) without relying on
|
||||
// screenshotting a WebGL canvas at a non-1 device scale factor.
|
||||
const dims = await ctx.proxy.core.renderDimensions;
|
||||
expect(dims.device.cell.width).toBeGreaterThan(dims.css.cell.width * 1.8);
|
||||
expect(dims.device.cell.width).toBeLessThan(dims.css.cell.width * 2.2);
|
||||
expect(dims.device.cell.height).toBeGreaterThan(dims.css.cell.height * 1.8);
|
||||
expect(dims.device.cell.height).toBeLessThan(dims.css.cell.height * 2.2);
|
||||
} finally {
|
||||
await ctx.page.context().close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('WebGL font-size change atlas integrity', () => {
|
||||
let ctx: ITestContext;
|
||||
test.skip(({ browserName }) => browserName !== 'chromium', 'WebGL atlas tests run on Chromium/SwiftShader only');
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
ctx = await createDprContext(browser, 1);
|
||||
});
|
||||
test.afterAll(async () => await ctx.page.close());
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await openTerminal(ctx, { cols: 80, rows: 24, fontSize: 15 });
|
||||
await loadWebglStrict(ctx);
|
||||
});
|
||||
|
||||
test('glyphs stay correct after cycling font size (atlas rebuilds)', async () => {
|
||||
await writeAndWaitForRender(ctx, 'FontRef0123456789');
|
||||
const reference = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
||||
|
||||
// Changing font size changes device cell dimensions -> the atlas is rebuilt.
|
||||
for (const fontSize of [12, 18, 21, 15]) {
|
||||
await ctx.proxy.setOption('fontSize', fontSize);
|
||||
await writeAndWaitForRender(ctx, '\r\nchurn');
|
||||
}
|
||||
|
||||
// Back at the original font size the reference glyphs must be unchanged.
|
||||
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], 16, `reference glyph at col ${REFERENCE_COLS[i]} corrupted after font-size cycling`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user