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
143 lines
7.9 KiB
JavaScript
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);});
|
|
}
|