test: repair native acceptance entry portability T01-T03
This commit is contained in:
1 parent
9724fc70bc
commit
f3be60bf2d
6 files changed
+335
-58
No files matched your search
@@ -4,11 +4,10 @@
|
||||
*/
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createRequire } from 'node:module'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { createHash } from 'node:crypto'
|
||||
const require = createRequire(import.meta.url), module = require('electron')
|
||||
const require = createRequire(import.meta.url)
|
||||
if (process.versions.electron) throw new Error('Launch this fixture builder with Node, not Electron')
|
||||
const base = path.resolve('.tmp')
|
||||
fs.mkdirSync(base, { recursive: true })
|
||||
const file = 'src/renderer/src/workbuddy/composer/ModeChip.tsx'
|
||||
@@ -27,51 +26,51 @@ function Fixture(){const [mode,setMode]=useState<any>('plan'),[disabled,setDisab
|
||||
await require('esbuild').build({ entryPoints: [path.join(output, 'fixture.tsx')], bundle: true, platform: 'browser', jsx: 'automatic', outfile: path.join(output, 'fixture.js'), logLevel: 'warning' })
|
||||
fs.writeFileSync(path.join(output, 'index.html'), '<html data-theme="light"><head><meta charset="utf-8"><link rel="stylesheet" href="fixture.css"></head><body><div id="root"></div><script src="fixture.js"></script></body></html>')
|
||||
}
|
||||
if (!process.versions.electron) {
|
||||
const output = fs.mkdtempSync(path.join(base, 'mode-chip-native-')), env = { ...process.env }
|
||||
delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL
|
||||
if (process.argv.includes('--build-only')) {
|
||||
await prepareFixture(output); console.log(JSON.stringify({ output, fixtureBuilt: true, nativeExecuted: false }))
|
||||
} else {
|
||||
const child = spawn(module, [path.resolve('scripts/test-mode-chip-native.mjs'), output], { env, windowsHide: true, stdio: 'inherit' })
|
||||
const timeout = setTimeout(() => child.kill(), 60000)
|
||||
child.on('error', error => { clearTimeout(timeout); console.error(error); process.exitCode = 1 })
|
||||
child.on('exit', code => { clearTimeout(timeout); process.exitCode = code ?? 1; console.log(JSON.stringify({ output, exitCode: code })) })
|
||||
}
|
||||
} else {
|
||||
const { app, BrowserWindow } = module, output = path.resolve(process.argv.at(-1))
|
||||
assert.ok(path.dirname(output) === base && path.basename(output).startsWith('mode-chip-native-'))
|
||||
fs.mkdirSync(path.join(output, 'data')); app.setPath('userData', path.join(output, 'data')); app.disableHardwareAcceleration()
|
||||
let win; const checks = [], samples = [], errors = []
|
||||
try {
|
||||
await prepareFixture(output)
|
||||
await app.whenReady(); win = new BrowserWindow({ width: 800, height: 300, show: false, webPreferences: { contextIsolation: true, nodeIntegration: false, backgroundThrottling: false } })
|
||||
win.webContents.on('console-message', event => { if (event.level === 'error' || event.level >= 3) errors.push(event.message) })
|
||||
await win.loadFile(path.join(output, 'index.html')); win.showInactive()
|
||||
const read = code => win.webContents.executeJavaScript(code), pause = () => new Promise(resolve => setTimeout(resolve, 80))
|
||||
const check = (label, actual, expected = true) => { assert.deepEqual(actual, expected, label); checks.push(label) }
|
||||
const key = async keyCode => { win.webContents.sendInputEvent({ type: 'keyDown', keyCode }); win.webContents.sendInputEvent({ type: 'keyUp', keyCode }); await pause() }
|
||||
const sample = () => read(`(()=>{const c=document.querySelector('.yc-mode-chip'),i=c.querySelector('.yc-mode-chip__mode-icon'),b=c.querySelector('button'),s=getComputedStyle(c),r=e=>{const x=e.getBoundingClientRect();return{x:x.x,y:x.y,width:x.width,height:x.height}};return{chip:r(c),icon:r(i),button:b?r(b):null,background:s.backgroundColor,radius:s.borderRadius,color:s.color,expectedColor:s.getPropertyValue('--cr-text-secondary').trim(),font:s.fontSize,weight:s.fontWeight,line:s.lineHeight,padding:s.paddingLeft,iconOpacity:getComputedStyle(i).opacity,clearOpacity:b?getComputedStyle(b).opacity:null}})()`)
|
||||
await pause()
|
||||
for (const theme of ['light', 'dark']) for (const mode of ['plan', 'ask']) {
|
||||
await read(`document.documentElement.dataset.theme='${theme}'; document.querySelector('.chat-pane').dataset.crTheme='${theme}'; qa.setMode('${mode}');qa.setDisabled(false);document.querySelector('#before').focus()`)
|
||||
win.webContents.sendInputEvent({ type: 'mouseMove', x: 1, y: 1 }); await pause()
|
||||
const normal = await sample(); samples.push({ theme, mode, state: 'normal', ...normal })
|
||||
check('transparent', normal.background, 'rgba(0, 0, 0, 0)'); check('pill', parseFloat(normal.radius) >= normal.chip.height / 2)
|
||||
check('height', normal.chip.height, 32); check('font', normal.font, '13px'); check('weight', normal.weight, '500'); check('line', normal.line, '20px'); check('padding', normal.padding, '8px')
|
||||
check('same slot', normal.icon, normal.button); check('icon width', normal.icon.width, 16); check('default icon', normal.iconOpacity, '1'); check('default close', normal.clearOpacity, '0')
|
||||
check('secondary color', await read(`(()=>{const c=document.querySelector('.yc-mode-chip'),p=document.createElement('span');p.style.color='var(--cr-text-secondary)';c.append(p);const ok=getComputedStyle(p).color===getComputedStyle(c).color;p.remove();return ok})()`))
|
||||
win.webContents.sendInputEvent({ type: 'mouseMove', x: Math.round(normal.chip.x + normal.chip.width / 2), y: Math.round(normal.chip.y + normal.chip.height / 2) }); await pause()
|
||||
const hover = await sample(); samples.push({ theme, mode, state: 'hover', ...hover }); check('hover icon hidden', hover.iconOpacity, '0'); check('hover X', hover.clearOpacity, '1'); check('no hover layout shift', hover.chip, normal.chip)
|
||||
win.webContents.sendInputEvent({ type: 'mouseMove', x: 1, y: 1 }); await read(`document.querySelector('.yc-mode-chip').focus()`); await pause()
|
||||
const focus = await sample(); check('focus icon hidden', focus.iconOpacity, '0'); check('focus X', focus.clearOpacity, '1'); check('no focus layout shift', focus.chip, normal.chip)
|
||||
await fs.promises.writeFile(path.join(output, `${theme}-${mode}-focus.png`), (await win.webContents.capturePage()).toPNG())
|
||||
await key('Tab'); check('Tab reaches native clear', await read(`document.activeElement.matches('.yc-mode-chip__clear')`))
|
||||
const before = await read('qa.clears'); await key(mode === 'plan' ? 'Enter' : 'Space'); check('native keyboard exit', await read('qa.clears'), before + 1); check('craft hidden', await read(`!document.querySelector('.yc-mode-chip')`))
|
||||
await read(`qa.setMode('${mode}');qa.setDisabled(true)`); await pause(); check('disabled has no clear', await read(`!document.querySelector('.yc-mode-chip button')`)); check('disabled not tabbable', await read(`document.querySelector('.yc-mode-chip').tabIndex`), -1)
|
||||
}
|
||||
win.setSize(320, 300); await read(`qa.setMode('ask');qa.setDisabled(false)`); await pause(); const narrow = await sample(); check('narrow slot preserved', narrow.icon, narrow.button); check('narrow no overflow', narrow.chip.x + narrow.chip.width <= 320)
|
||||
await fs.promises.writeFile(path.join(output, 'narrow.png'), (await win.webContents.capturePage()).toPNG()); check('no renderer errors', errors, [])
|
||||
fs.writeFileSync(path.join(output, 'report.json'), JSON.stringify({ checks, samples, errors, sourceSha256: createHash('sha256').update(fs.readFileSync(file)).digest('hex'), scope: 'Own-YCode isolated component browser test, not full app/store integration or original-app pixel acceptance' }, null, 2)); app.exit(0)
|
||||
} catch (error) { fs.writeFileSync(path.join(output, 'failure.txt'), String(error.stack || error)); console.error(error); app.exit(1) }
|
||||
const output = fs.mkdtempSync(path.join(base, 'mode-chip-native-'))
|
||||
function phase(name) {
|
||||
const detail = { phase: name, time: new Date().toISOString(), output }
|
||||
fs.writeFileSync(path.join(output, 'runner-phase.json'), JSON.stringify(detail, null, 2))
|
||||
console.log(JSON.stringify(detail))
|
||||
}
|
||||
function fail(error) {
|
||||
const detail = String(error.stack || error)
|
||||
fs.writeFileSync(path.join(output, 'runner-failure.txt'), detail)
|
||||
console.error(detail); process.exitCode = 1
|
||||
}
|
||||
phase('prepare-fixture')
|
||||
const buildTimeout = setTimeout(() => {
|
||||
fail(new Error('Fixture preparation timed out after 30000ms'))
|
||||
process.exit(1)
|
||||
}, 30000)
|
||||
try {
|
||||
// All esbuild work finishes under Node, before starting Electron's CJS entry.
|
||||
await prepareFixture(output)
|
||||
clearTimeout(buildTimeout)
|
||||
phase('fixture-built')
|
||||
if (process.argv.includes('--build-only')) {
|
||||
console.log(JSON.stringify({ output, fixtureBuilt: true, nativeExecuted: false }))
|
||||
} else {
|
||||
const env = { ...process.env }
|
||||
delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL
|
||||
phase('launch-electron')
|
||||
const child = spawn(require('electron'), [path.resolve('scripts/test-mode-chip-native-electron.cjs'), output], { env, windowsHide: true, stdio: 'inherit' })
|
||||
let timedOut = false, spawnFailed = false
|
||||
const timeout = setTimeout(() => {
|
||||
timedOut = true
|
||||
let lastPhase = 'no Electron phase recorded'
|
||||
try { lastPhase = fs.readFileSync(path.join(output, 'phase.json'), 'utf8') } catch {}
|
||||
fail(new Error(`Electron timed out after 60000ms; last phase: ${lastPhase}`))
|
||||
// Only terminate the child created by this runner, never an existing app/dev.
|
||||
child.kill('SIGKILL')
|
||||
}, 60000)
|
||||
child.once('error', error => { spawnFailed = true; clearTimeout(timeout); fail(error) })
|
||||
child.once('close', (code, signal) => {
|
||||
clearTimeout(timeout)
|
||||
const passed = !timedOut && !spawnFailed && code === 0 && fs.existsSync(path.join(output, 'report.json'))
|
||||
process.exitCode = passed ? 0 : 1
|
||||
if (!passed && !timedOut && !spawnFailed) fail(new Error(`Electron did not complete: exitCode=${code}, signal=${signal}; see phase.json / failure.txt`))
|
||||
phase(passed ? 'complete' : 'failed')
|
||||
console.log(JSON.stringify({ output, exitCode: code, signal, timedOut, nativeExecuted: passed }))
|
||||
})
|
||||
}
|
||||
} catch (error) { clearTimeout(buildTimeout); fail(error) }
|
||||
Reference in new issue
Block a user