chore: establish YCode replication baseline and TODO
This commit is contained in:
commit
6dc59f89ba
1118 files changed
+196002
No files matched your search
@@ -0,0 +1,100 @@
|
||||
// Real xterm renderer, Electron IPC, and Windows ConPTY in a temporary workspace.
|
||||
const path = require('node:path')
|
||||
const fs = require('node:fs/promises')
|
||||
const assert = require('node:assert/strict')
|
||||
const directory = path.resolve('.tmp/terminal-ui-regression')
|
||||
if (!process.versions.electron) {
|
||||
;(async () => {
|
||||
await fs.mkdir(directory, { recursive: true })
|
||||
await require('esbuild').build({ entryPoints: [path.resolve('src/main/terminal-host.ts')], outfile: path.join(directory, 'host.cjs'), bundle: true,
|
||||
platform: 'node', format: 'cjs', external: ['electron', 'node-pty'], logLevel: 'silent' })
|
||||
const entry = `
|
||||
import React from 'react';import {createRoot} from 'react-dom/client';
|
||||
import {NativeTerminalPane} from './src/renderer/src/workbuddy/workbench/NativeTerminalPane';
|
||||
import './src/renderer/src/workbuddy/typography/tokens.css';import './src/renderer/src/styles/tokens.css';import './src/renderer/src/styles/app.css';
|
||||
const ipc=window.require('electron').ipcRenderer;
|
||||
window.ycode={terminal:{get:(...args)=>ipc.invoke('qa-terminal:get',...args),write:(...args)=>ipc.invoke('qa-terminal:write',...args),resize:(...args)=>ipc.invoke('qa-terminal:resize',...args),clear:(...args)=>ipc.invoke('qa-terminal:clear',...args),
|
||||
onEvent:listener=>{const fn=(_e,event)=>listener(event);ipc.on('terminal:event',fn);return()=>ipc.removeListener('terminal:event',fn)}}};
|
||||
const root=createRoot(document.getElementById('stage'));const id=new URLSearchParams(location.search).get('id');
|
||||
window.qa={creates:0,selection:'',mount:()=>root.render(<NativeTerminalPane id={id} sessionId="ui-session" onAddSelectionToChat={text=>window.qa.selection=text} onCreateTerminal={()=>{window.qa.creates++}}/>),unmount:()=>root.render(null)};
|
||||
window.qa.mount();
|
||||
`
|
||||
await require('esbuild').build({ stdin: { contents: entry, resolveDir: process.cwd(), loader: 'tsx' }, outfile: path.join(directory, 'fixture.js'), bundle: true,
|
||||
platform: 'browser', format: 'iife', jsx: 'automatic', logLevel: 'silent' })
|
||||
await fs.writeFile(path.join(directory, 'index.html'), '<!doctype html><html data-theme="light"><meta charset="utf-8"><link rel="stylesheet" href="fixture.css"><style>#stage{width:640px;height:420px}</style><div id="stage"></div><script src="fixture.js"></script></html>')
|
||||
const env = { ...process.env }; delete env.ELECTRON_RUN_AS_NODE
|
||||
const result = require('node:child_process').spawnSync(require('electron'), [__filename], { env, windowsHide: true, stdio: 'inherit', timeout: 60000 })
|
||||
process.exitCode = result.status ?? 1
|
||||
})().catch(error => { console.error(error); process.exitCode = 1 })
|
||||
} else {
|
||||
const { app, BrowserWindow, ipcMain } = require('electron')
|
||||
app.setPath('userData', path.join(directory, 'user-data'))
|
||||
app.disableHardwareAcceleration()
|
||||
const host = require(path.join(directory, 'host.cjs'))
|
||||
let win, view
|
||||
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
const read = code => win.webContents.executeJavaScript(code)
|
||||
async function until(test, label) {
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < 12000) { if (await test()) return; await pause(25) }
|
||||
throw Error('Timed out: ' + label)
|
||||
}
|
||||
const visibleText = () => read('document.querySelector(".xterm-screen")?.textContent ?? ""')
|
||||
const context = () => read('document.querySelector(".native-terminal__screen").dispatchEvent(new MouseEvent("contextmenu",{clientX:400,clientY:120,bubbles:true,cancelable:true}))')
|
||||
const click = selector => read(`document.querySelector(${JSON.stringify(selector)}).click()`)
|
||||
const ownerOf = event => {
|
||||
const owner = BrowserWindow.fromWebContents(event.sender)
|
||||
if (!owner) throw Error('No terminal owner')
|
||||
return owner
|
||||
}
|
||||
for (const [channel, operation] of Object.entries({ get: host.getTerminal, write: host.writeTerminal, resize: host.resizeTerminal, clear: host.clearTerminal })) {
|
||||
ipcMain.handle('qa-terminal:' + channel, (event, ...args) => operation(ownerOf(event), ...args))
|
||||
}
|
||||
app.whenReady().then(async () => {
|
||||
const cwd = path.join(directory, 'workspace')
|
||||
await fs.mkdir(cwd, { recursive: true })
|
||||
win = new BrowserWindow({ width: 900, height: 600, useContentSize: true, show: false,
|
||||
webPreferences: { contextIsolation: false, nodeIntegration: true, offscreen: true } })
|
||||
const rendererErrors = []
|
||||
win.webContents.on('console-message', event => { if (event.level === 'error') rendererErrors.push(event.message) })
|
||||
view = await host.createTerminal(win, cwd, 'ui-session')
|
||||
await win.loadFile(path.join(directory, 'index.html'), { query: { id: view.id } })
|
||||
await until(async () => (await read('!!document.querySelector(".xterm-helper-textarea")')) && host.getTerminal(win, view.id, 'ui-session').cols !== 80, 'xterm opened and FitAddon resized native PTY')
|
||||
host.writeTerminal(win, view.id, "Write-Output ('__UI_'+'READY__')\r", 'ui-session')
|
||||
await until(async () => (await visibleText()).includes('__UI_READY__'), 'native PTY output renders in actual xterm rows')
|
||||
const dimensions = await read('(()=>{const e=document.querySelector(".native-terminal"),s=getComputedStyle(e);return{block:s.paddingBlockStart,start:s.paddingInlineStart,end:s.paddingInlineEnd,height:e.getBoundingClientRect().height}})()')
|
||||
assert.deepEqual(dimensions, { block: '20px', start: '20px', end: '6px', height: 420 })
|
||||
await read('document.querySelector(".xterm-helper-textarea").focus()')
|
||||
await win.webContents.insertText("Write-Output ('__USER_'+'INPUT__')")
|
||||
win.webContents.sendInputEvent({ type: 'keyDown', keyCode: 'Return' }); win.webContents.sendInputEvent({ type: 'keyUp', keyCode: 'Return' })
|
||||
await until(async () => (await visibleText()).includes('__USER_INPUT__'), 'native keyboard/textarea input reaches PowerShell')
|
||||
const beforeCols = host.getTerminal(win, view.id, 'ui-session').cols
|
||||
await read('document.getElementById("stage").style.width="420px"')
|
||||
await until(() => host.getTerminal(win, view.id, 'ui-session').cols < beforeCols, 'ResizeObserver fits and resizes actual PTY')
|
||||
const narrowedCols = host.getTerminal(win, view.id, 'ui-session').cols
|
||||
await read('window.qa.unmount()')
|
||||
await until(async () => !(await read('!!document.querySelector(".native-terminal")')), 'terminal view unmounted without closing host')
|
||||
host.writeTerminal(win, view.id, "Write-Output ('__HIDDEN_'+'OUTPUT__')\r", 'ui-session')
|
||||
await until(() => host.getTerminal(win, view.id, 'ui-session').buffer.includes('__HIDDEN_OUTPUT__'), 'detached terminal keeps running')
|
||||
await read('window.qa.mount()')
|
||||
await until(async () => (await visibleText()).includes('__HIDDEN_OUTPUT__'), 'output replays on remount')
|
||||
await context()
|
||||
await until(async () => await read('document.querySelectorAll(".terminal-context-menu>button").length===4'), 'source four-item terminal context menu')
|
||||
assert.equal(await read('document.querySelector(".terminal-context-menu").getBoundingClientRect().width'), 160)
|
||||
await click('.terminal-context-menu button:last-child')
|
||||
assert.equal(await read('window.qa.creates'), 1)
|
||||
await read('document.documentElement.dataset.theme="dark"')
|
||||
await pause(150)
|
||||
const background = await read('getComputedStyle(document.querySelector(".xterm-scrollable-element")).backgroundColor')
|
||||
assert.equal(background, 'rgb(31, 31, 31)', 'source dark xterm theme is applied')
|
||||
await fs.writeFile(path.join(directory, 'terminal-dark.png'), (await win.webContents.capturePage()).toPNG())
|
||||
host.writeTerminal(win, view.id, 'exit 3\r', 'ui-session')
|
||||
await until(async () => (await read('document.querySelector(".native-terminal__status")?.textContent')) === 'Terminal process exited', 'exit state reaches terminal UI')
|
||||
assert.deepEqual(rendererErrors, [], 'xterm renderer must not log runtime errors')
|
||||
const report = { result: 'PASS native terminal UI', electron: process.versions.electron, beforeCols, narrowedCols, sourceDimensions: dimensions,
|
||||
checks: ['real xterm + native PTY output', 'actual native text/Enter input', 'fit resize', 'unmount keeps process', 'geometry-aware replay', 'source context menu', 'new-terminal callback', 'source dark theme', 'exit event'] }
|
||||
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2))
|
||||
console.log(JSON.stringify(report))
|
||||
host.disposeTerminalHosts(); win.destroy(); app.exit(0)
|
||||
}).catch(error => { console.error(error); host.disposeTerminalHosts(); if (win && !win.isDestroyed()) win.destroy(); app.exit(1) })
|
||||
}
|
||||
Reference in new issue
Block a user