Files
NetMesh/scripts/terminal-background-rendering.live.test.cjs
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

143 lines
7.9 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);
};
const 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,
define: { "import.meta.env.DEV": "false", "import.meta.env.PROD": "true", "import.meta": "{}" },
}).outputFiles[0].text;
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);});
}