1import { test, expect } from '@playwright/test';
2import path from 'node:path';
3
4const root = process.cwd();
5
6/** @param {number} width @param {number} height @param {string} text */
7function snapshot(width, height, text) {
8 const cells = Array.from({ length: width }, (_, column) => ({ column, width: 1, text, style: 0 }));
9 return {
10 schemaVersion: 2,
11 width,
12 height,
13 snapshot: true,
14 styles: [{ foreground: { kind: 0 }, background: { kind: 0 }, underlineColor: { kind: 0 } }],
15 rows: Array.from({ length: height }, (_, row) => ({ row, cells })),
16 cursor: { row: 0, column: 0, visible: false, style: 0, styleSet: false }
17 };
18}
19
20test('keeps sustained snapshot and row replacement work bounded', async ({ page, browserName }, testInfo) => {
21 test.skip(browserName !== 'chromium', 'reference performance budget uses pinned Chromium');
22 await page.setContent('<div id="terminal"></div>');
23 await page.addStyleTag({ path: path.join(root, 'html/terminal.css') });
24 await page.addScriptTag({ path: path.join(root, 'html/browser/terminal.js') });
25
26 const metrics = await page.evaluate(async ({ first, second, large }) => {
27 const terminal = VevTerminal.mount(document.querySelector('#terminal'), { label: 'Performance terminal' });
28 const settle = async () => {
29 document.querySelector('.vev-terminal__viewport').getBoundingClientRect();
30 await new Promise(requestAnimationFrame);
31 };
32 terminal.apply(first);
33 await settle();
34 const snapshots = [];
35 for (let index = 0; index < 10; index += 1) {
36 const start = performance.now();
37 terminal.apply(index % 2 === 0 ? second : first);
38 await settle();
39 snapshots.push(performance.now() - start);
40 }
41
42 const rows = [];
43 for (let index = 0; index < 50; index += 1) {
44 // Alternate both rows so every timed apply performs a real replacement
45 // instead of re-applying the same row through the no-op reuse path.
46 const rowUpdate = {
47 ...(index % 2 === 0 ? first : second),
48 snapshot: false,
49 rows: [(index % 2 === 0 ? second : first).rows[20]]
50 };
51 const start = performance.now();
52 terminal.apply(rowUpdate);
53 await settle();
54 rows.push(performance.now() - start);
55 }
56 const percentile = (values, ratio) => [...values].sort((a, b) => a - b)[Math.ceil(values.length * ratio) - 1];
57 const largeStart = performance.now();
58 terminal.apply(large);
59 await settle();
60 const largeApply = performance.now() - largeStart;
61 return {
62 snapshotP95: percentile(snapshots, 0.95),
63 rowP95: percentile(rows, 0.95),
64 largeApply,
65 cells: document.querySelectorAll('.vev-terminal__cell').length,
66 rows: document.querySelectorAll('.vev-terminal__row').length
67 };
68 }, { first: snapshot(120, 40, 'A'), second: snapshot(120, 40, 'B'), large: snapshot(240, 80, 'C') });
69
70 testInfo.annotations.push({ type: 'performance', description: JSON.stringify(metrics) });
71 expect(metrics.snapshotP95).toBeLessThan(100);
72 expect(metrics.rowP95).toBeLessThan(33);
73 expect(metrics.largeApply).toBeLessThan(500);
74 expect(metrics.cells).toBe(240 * 80);
75 expect(metrics.rows).toBe(80);
76});