Files
Netcatty/scripts/terminal-background-rendering.live.test.cjs
T
陈大猫 bd8476fb34 fix(terminal): enable font ligatures from the terminal setting (#3557)
fontLigatures already defaulted to on, but the terminal never loaded
the ligatures addon, so Fira Code and Maple Mono kept drawing != as two
cells. Activate the addon after open and before WebGL, and rebuild the
WebGL atlas when the setting changes so glyphs are not rasterized
without the calt feature.

The addon bundles lru-cache's node:diagnostics_channel import. Alias
that builtin to a no-op channel in the Vite renderer build and in the
Electron live-test bundles.

Closes #3543
2026-09-29 17:34:00 +08:00

155 lines
8.4 KiB
JavaScript

"use strict";
/* global process, __dirname, console */
if (!process.versions.electron) {
require("node:test")("background terminals stop painting without stopping output", {
skip: "run with Electron to exercise real terminal visibility and rendering",
}, () => {});
} else {
const { app, BrowserWindow } = require("electron");
const fs = require("node:fs");
const path = require("node:path");
const esbuild = require("esbuild");
const temp = require("../electron/bridges/tempDirBridge.cjs");
const root = path.resolve(__dirname, "..");
const userData = fs.mkdtempSync(`${temp.getTempFilePath("background-rendering")}-`);
app.setPath("userData", userData);
app.on("window-all-closed", () => {});
let win;
const cleanup = (code) => {
win?.destroy();
fs.rmSync(userData, { recursive: true, force: true });
app.exit(code);
};
let bundle;
try {
bundle = esbuild.buildSync({
stdin: {
contents: [
'export {createXTermRuntime} from "./components/terminal/runtime/createXTermRuntime";',
'export {DEFAULT_TERMINAL_SETTINGS} from "./domain/models/terminal";',
'export {resolveInactiveTerminalPaneStyle} from "./components/terminalPaneVisibility";',
].join("\n"),
loader: "ts", resolveDir: root,
},
bundle: true, format: "cjs", platform: "browser", target: "chrome148", write: false,
// addon-ligatures bundles lru-cache's node:diagnostics_channel import.
alias: {
"node:diagnostics_channel": path.join(root, "infrastructure/shims/nodeDiagnosticsChannel.ts"),
"diagnostics_channel": path.join(root, "infrastructure/shims/nodeDiagnosticsChannel.ts"),
},
define: { "import.meta.env.DEV": "false", "import.meta.env.PROD": "true", "import.meta": "{}" },
}).outputFiles[0].text;
} catch (error) {
console.error(error);
fs.rmSync(userData, { recursive: true, force: true });
process.exit(1);
}
void app.whenReady().then(async () => {
win = new BrowserWindow({
show: true, width: 1100, height: 740,
webPreferences: { nodeIntegration: true, contextIsolation: false, sandbox: false, backgroundThrottling: false },
});
await win.loadURL("data:text/html,<body style='margin:0;background:%23111'><div id='root'></div>");
await win.webContents.insertCSS(fs.readFileSync(require.resolve("@xterm/xterm/css/xterm.css"), "utf8"));
const result = await win.webContents.executeJavaScript(`(async () => {
const assert = require('node:assert/strict');
const loaded = {exports:{}};
((module,exports)=>{${bundle}})(loaded,loaded.exports);
const {createXTermRuntime,DEFAULT_TERMINAL_SETTINGS,resolveInactiveTerminalPaneStyle} = loaded.exports;
const wait = ms => new Promise(resolve=>setTimeout(resolve,ms));
const ref = current => ({current});
const panes = [];
const root = document.getElementById('root');
root.style.cssText = 'position:relative;width:1050px;height:660px;overflow:hidden';
const visibleStyle = {left:'0px',top:'0px',width:'1050px',height:'660px'};
const apply = (pane, visible, layout = visibleStyle) => {
const style = visible ? layout : resolveInactiveTerminalPaneStyle(layout,{width:1050,height:660},false,true);
pane.el.style.cssText = 'position:absolute;background:#111';
Object.assign(pane.el.style,style);
pane.el.style.zIndex = visible ? '10' : '0';
};
for(let index=0;index<5;index++) {
const el=document.createElement('div');root.appendChild(el);
const pane={el,frames:0};apply(pane,true);
const r=createXTermRuntime({
container:el,host:{id:'test-'+index,label:'test',hostname:'localhost',protocol:'ssh'},
fontFamilyId:'jetbrains-mono',resolvedFontFamily:'monospace',fontSize:14,
terminalTheme:{colors:{background:'#111111',foreground:'#eeeeee',cursor:'#ffffff',selection:'#444444'}},
terminalSettingsRef:ref({...DEFAULT_TERMINAL_SETTINGS,cursorBlink:false}),
terminalBackend:{write:()=>{},resize:()=>{},openExternalAvailable:false},
sessionRef:ref('test-'+index),hotkeySchemeRef:ref('disabled'),disableTerminalFontZoomRef:ref(false),
keyBindingsRef:ref([]),onHotkeyActionRef:ref(undefined),isBroadcastEnabledRef:ref(false),
onBroadcastInputRef:ref(undefined),sessionId:'test-'+index,statusRef:ref('connected'),
commandBufferRef:ref(''),requestSearchFocus:()=>{},
});
r.fitAddon.fit();r.term.onRender(()=>pane.frames++);
pane.r=r;panes.push(pane);
await new Promise(resolve=>r.term.write('ready\\r\\n',resolve));
}
assert.ok(root.querySelector('canvas'),'exercise WebGL, not only a fake renderer');
const write=(pane,data)=>new Promise(resolve=>pane.r.term.write(data,resolve));
const size=pane=>[pane.r.term.cols,pane.r.term.rows];
const beforeSizes=panes.map(size);
for(let i=1;i<5;i++)apply(panes[i],false);
await wait(200);
const before=panes.map(p=>p.frames);
for(let tick=0;tick<12;tick++) {
await Promise.all(panes.map(p=>write(p,'background-'+tick+'\\r\\n')));
await wait(30);
}
const hiddenFrames=panes.map((p,i)=>p.frames-before[i]);
assert.ok(hiddenFrames[0]>0,'visible terminal still paints');
assert.deepEqual(hiddenFrames.slice(1),[0,0,0,0],'background terminals must not paint');
assert.deepEqual(panes.map(size),beforeSizes,'hiding preserves terminal dimensions');
for(const pane of panes) {
const b=pane.r.term.buffer.active;
assert.equal(b.getLine(b.baseY+b.cursorY-1).translateToString(true),'background-11','hidden output is already parsed');
}
// Cached right-hand split offsets can exceed a restored window's width.
apply(panes[4],false,{...visibleStyle,left:'1600px'});
await wait(150);
const offsetFrames=panes[4].frames;
await write(panes[4],'cached-offset\\r\\n');await wait(100);
assert.equal(panes[4].frames,offsetFrames,'cached split offsets must not leave hidden panes painting');
assert.ok(panes[4].el.getBoundingClientRect().right<=0,'park the entire cached split pane');
assert.deepEqual(size(panes[4]),beforeSizes[4]);
// No zero-width fitting during a window resize while tabs are parked.
root.style.width='850px';
assert.equal(panes[1].el.clientWidth,1050);
for(let tick=0;tick<12;tick++) {
const pane=panes[1+tick%4];
await write(pane,'switch-'+tick+'\\r\\n');
const start=pane.frames;apply(pane,true);await wait(100);
assert.ok(pane.frames>start,'reveal must repaint');
assert.deepEqual(size(pane),beforeSizes[1]);apply(pane,false);await wait(30);
}
// A full-screen program must keep interpreting cursor operations while hidden.
const tui=panes[2];
await write(tui,'\\x1b[?1049h\\x1b[2J\\x1b[HOLD');
await wait(100);const tuiBefore=tui.frames;
await write(tui,'\\x1b[HNEW\\x1b[2;1Hsecond row');await wait(100);
assert.equal(tui.frames,tuiBefore);
assert.equal(tui.r.term.buffer.active.getLine(0).translateToString(true),'NEW');
apply(tui,true,{...visibleStyle,width:'850px'});tui.r.fitAddon.fit();await wait(100);
assert.ok(tui.frames>tuiBefore);
assert.equal(tui.r.term.buffer.active.getLine(0).translateToString(true),'NEW');
await write(tui,'\\x1b[?1049l');
// Two visible split panes paint; parked panes continue parsing without painting.
for(const pane of panes)apply(pane,false);
apply(panes[0],true,{left:'0px',top:'0px',width:'425px',height:'660px'});
apply(panes[1],true,{left:'425px',top:'0px',width:'425px',height:'660px'});
panes[0].r.fitAddon.fit();panes[1].r.fitAddon.fit();await wait(150);
const splitBefore=panes.map(p=>p.frames);
await Promise.all(panes.map(p=>write(p,'split-final\\r\\n')));await wait(100);
assert.ok(panes[0].frames>splitBefore[0] && panes[1].frames>splitBefore[1]);
assert.deepEqual(panes.slice(2).map((p,i)=>p.frames-splitBefore[i+2]),[0,0,0]);
const summary={hiddenFrames,rapidSwitches:12,alternateScreen:true,split:true,sizes:beforeSizes};
panes.forEach(p=>p.r.dispose());root.replaceChildren();
assert.equal(document.querySelectorAll('.xterm').length,0);
return summary;
})()`);
console.log("TERMINAL_BACKGROUND_RENDERING_OK", JSON.stringify(result));
cleanup(0);
}).catch(error=>{console.error(error);cleanup(1);});
}