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,166 @@
|
||||
// Production isolated native Electron windows, real preload/IPC, no messages or upstream calls.
|
||||
// WorkBuddy reference is a fixed REA PNG; debugger operations below target YCode only.
|
||||
const fs = require('node:fs/promises')
|
||||
const path = require('node:path')
|
||||
const assert = require('node:assert/strict')
|
||||
const { createHash } = require('node:crypto')
|
||||
const hash = bytes => createHash('sha256').update(bytes).digest('hex')
|
||||
const referencePath = path.resolve('.tmp/rea-home-static-20261016/workbuddy.png')
|
||||
const referenceSha256 = '360b076811ef19381f0670c314b24b06252310bf24fdc245f529f4894cfd7241'
|
||||
const assetPath = path.resolve('src/renderer/src/components/source-home-assets/workbuddy-buddy-thumbnail-2713ed5b.png')
|
||||
const assetSha256 = '2713ed5bbf1288d56ebeaca49bfb0d622c967914ab9c98955cc995932049e7e8'
|
||||
if (!process.versions.electron) {
|
||||
;(async () => {
|
||||
assert.equal(hash(await fs.readFile(referencePath)), referenceSha256, 'REA reference PNG changed')
|
||||
assert.equal(hash(await fs.readFile(assetPath)), assetSha256, 'REA robot asset changed')
|
||||
const directory = await fs.mkdtemp(path.resolve('.tmp/desktop-appearance-'))
|
||||
await Promise.all(['data', 'home'].map(name => fs.mkdir(path.join(directory, name))))
|
||||
const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: path.join(directory, 'data'), YCODE_OMP_HOME: path.join(directory, 'home'), YCODE_QA_OFFSCREEN: '0' }
|
||||
delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL; delete env.YCODE_DISABLE_GPU; delete env.YCODE_DISABLE_GPU_SANDBOX
|
||||
const child = require('node:child_process').spawn(require('electron'), ['--no-sandbox', '--disable-gpu-sandbox', '--use-angle=swiftshader', '--disable-gpu-compositing', __filename],
|
||||
{ env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
let output = ''
|
||||
child.stdout.on('data', bytes => { output += bytes; if (bytes.toString().includes('APPEARANCE_')) process.stdout.write(bytes) })
|
||||
child.stderr.on('data', bytes => { output += bytes })
|
||||
const timer = setTimeout(() => child.kill(), 90000)
|
||||
child.once('exit', async code => {
|
||||
clearTimeout(timer); await fs.writeFile(path.join(directory, 'process.log'), output)
|
||||
console.log(JSON.stringify({ directory, exitCode: code })); if (code) console.error(output.slice(-6000)); process.exitCode = code ?? 1
|
||||
})
|
||||
})().catch(error => { console.error(error); process.exitCode = 1 })
|
||||
} else {
|
||||
const { app, BrowserWindow, nativeImage } = require('electron')
|
||||
// Exactly the established visible-swiftshader capture mode; no --disable-gpu request.
|
||||
app.disableHardwareAcceleration()
|
||||
app.commandLine.appendSwitch('use-angle', 'swiftshader'); app.commandLine.appendSwitch('disable-gpu-compositing')
|
||||
const { pathToFileURL } = require('node:url')
|
||||
const directory = process.env.YCODE_QA_DIRECTORY, data = path.join(directory, 'data'), home = path.join(directory, 'home')
|
||||
const relative = path.relative(path.resolve('.tmp'), directory)
|
||||
assert.ok(relative.startsWith('desktop-appearance-') && !relative.includes('..') && !path.isAbsolute(relative))
|
||||
app.setPath('userData', data); app.setPath('sessionData', data); app.setPath('home', home)
|
||||
let main, settings, phase = 'boot'
|
||||
const checks = [], themes = [], rendererErrors = [], screenshots = []
|
||||
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
async function read(source, win = main) { let timer; try { return await Promise.race([win.webContents.executeJavaScript(source), new Promise((_, reject) => { timer = setTimeout(() => reject(Error('Renderer timeout')), 12000) })]) } catch(error) { throw Error(error.message + ' — ' + source.slice(0, 180)) } finally { clearTimeout(timer) } }
|
||||
async function wait(source, label, win = main) { for (let i = 0; i < 250; i++) { const value = await read(source, win); if (value) return value; await pause(25) } throw Error('Timeout: ' + label) }
|
||||
async function step(label) { phase = label; await fs.writeFile(path.join(directory, 'phase.txt'), label) }
|
||||
function check(label, actual, expected) { const pass = typeof expected === 'function' ? expected(actual) : JSON.stringify(actual) === JSON.stringify(expected); checks.push({ label, pass, actual, expected: typeof expected === 'function' ? expected.toString() : expected }) }
|
||||
async function click(selector, win = main) { await read(`(()=>{const n=document.querySelector(${JSON.stringify(selector)});if(!n)throw Error('Missing clickable');n.click()})()`, win); await pause(60) }
|
||||
async function clickText(selector, text, win = main) { await read(`(()=>{const n=[...document.querySelectorAll(${JSON.stringify(selector)})].find(n=>n.textContent.trim()===${JSON.stringify(text)});if(!n)throw Error('Missing text button');n.click()})()`, win); await pause(60) }
|
||||
function observe(win) { win.webContents.on('console-message', event => { if (event.level === 'error' || event.level >= 3) rendererErrors.push({ windowId: win.id, message: event.message }) }) }
|
||||
async function shot(name, win = main) {
|
||||
if (!win.webContents.debugger.isAttached()) win.webContents.debugger.attach('1.3')
|
||||
win.showInactive(); win.webContents.setBackgroundThrottling(false)
|
||||
await read('document.fonts.ready.then(()=>true)', win); await pause(150)
|
||||
const image = await win.webContents.debugger.sendCommand('Page.captureScreenshot', { format: 'png', fromSurface: true, captureBeyondViewport: false })
|
||||
const file = path.join(directory, name + '.png'), bytes = Buffer.from(image.data, 'base64')
|
||||
await fs.writeFile(file, bytes); screenshots.push({ file, sha256: hash(bytes), visible: win.isVisible(), method: 'Page.captureScreenshot({fromSurface:true})' }); return bytes
|
||||
}
|
||||
async function openSettings(section) {
|
||||
await read(`window.ycode.app.openSettings(${JSON.stringify(section)})`)
|
||||
for (let i = 0; i < 150; i++) { settings = BrowserWindow.getAllWindows().find(win => win !== main); if (settings && !settings.webContents.isLoading()) break; await pause(25) }
|
||||
assert.ok(settings); await wait('!!document.querySelector(".settings-window-root")', 'real native settings window', settings); return settings
|
||||
}
|
||||
async function themeSnapshot(label) {
|
||||
const value = { label, main: await read('[document.documentElement.dataset.theme,localStorage.getItem("ycode:theme")]'),
|
||||
settings: await read('[document.documentElement.dataset.theme,localStorage.getItem("ycode:theme")]', settings), settingsWindowId: settings.id }
|
||||
themes.push(value); return value
|
||||
}
|
||||
function compareRobot(sourceBytes, ownBytes) {
|
||||
const roi = { x: 1085, y: 286, width: 120, height: 78 }
|
||||
const source = nativeImage.createFromBuffer(sourceBytes), actual = nativeImage.createFromBuffer(ownBytes)
|
||||
assert.deepEqual(source.getSize(), { width: 1336, height: 901 }); assert.deepEqual(actual.getSize(), source.getSize())
|
||||
const a = source.crop(roi).toBitmap({ scaleFactor: 1 }), b = actual.crop(roi).toBitmap({ scaleFactor: 1 }), delta = Buffer.alloc(a.length)
|
||||
let changedPixels = 0, maxChannelDifference = 0, totalChannelDifference = 0
|
||||
const histogram = {}
|
||||
for (let offset = 0; offset < a.length; offset += 4) {
|
||||
let pixelDelta = 0
|
||||
for (let channel = 0; channel < 4; channel++) { const difference = Math.abs(a[offset + channel] - b[offset + channel]); pixelDelta = Math.max(pixelDelta, difference); maxChannelDifference = Math.max(maxChannelDifference, difference); totalChannelDifference += difference; if (channel < 3) delta[offset + channel] = Math.min(255, difference * 12) }
|
||||
delta[offset + 3] = 255
|
||||
if (pixelDelta) changedPixels++
|
||||
histogram[pixelDelta] = (histogram[pixelDelta] || 0) + 1
|
||||
}
|
||||
return { report: { roi: [1085, 286, 1205, 364], totalPixels: 9360, changedPixels, maxChannelDifference, totalChannelDifference,
|
||||
pixelMaximumDifferenceHistogram: histogram, exact: changedPixels === 0, tolerance: 0, excludedPixels: 0 }, sourceCrop: source.crop(roi).toPNG(), actualCrop: actual.crop(roi).toPNG(), diff: nativeImage.createFromBitmap(delta, { width: 120, height: 78 }).toPNG() }
|
||||
}
|
||||
;(async () => {
|
||||
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href); await app.whenReady()
|
||||
for (let i = 0; i < 200; i++) { main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode'); if (main && !main.webContents.isLoading()) break; await pause(25) }
|
||||
assert.ok(main); observe(main); await wait('!!window.ycode&&!!document.querySelector(".source-home")', 'production homepage')
|
||||
assert.equal(path.resolve((await read('window.ycode.app.info()')).configDir), data)
|
||||
check('fresh isolated App has no messages/sessions', await read('window.ycode.session.list().then(s=>s.length)'), 0)
|
||||
await step('ui-dark-theme-and-reused-settings-navigation')
|
||||
await openSettings('appearance'); observe(settings)
|
||||
await click('[data-settings-tab="appearance"]', settings)
|
||||
await clickText('.segmented__item', '深色', settings)
|
||||
await wait('document.documentElement.dataset.theme==="dark"', 'main receives dark appearance')
|
||||
let theme = await themeSnapshot('dark-selected-in-native-settings')
|
||||
check('native UI dark changes both renderers and persistent shared storage', [theme.main, theme.settings], [['dark', 'dark'], ['dark', 'dark']])
|
||||
await shot('main-dark'); await shot('settings-dark', settings)
|
||||
const previous = settings.id
|
||||
await openSettings('model'); await wait('!!document.querySelector(".models-settings-panel")', 'reused settings navigates to actual models panel', settings)
|
||||
check('openSettings(model) reuses existing native window', settings.id, previous)
|
||||
check('reused settings actually navigated away from appearance', await read('!!document.querySelector(".models-settings-panel")&&!document.querySelector(".segmented")', settings), true)
|
||||
await click('.settings-window-close', settings)
|
||||
for (let i = 0; i < 150 && !settings.isDestroyed(); i++) await pause(20)
|
||||
check('settings close destroys actual native window', settings.isDestroyed(), true)
|
||||
await openSettings('appearance'); observe(settings)
|
||||
check('reopened settings uses a fresh native window', settings.id !== previous, true)
|
||||
theme = await themeSnapshot('reopened-settings-persists-dark')
|
||||
check('native settings reopen preserves shared dark appearance', [theme.main, theme.settings], [['dark', 'dark'], ['dark', 'dark']])
|
||||
await clickText('.segmented__item', '浅色', settings)
|
||||
await wait('document.documentElement.dataset.theme==="light"', 'main receives light appearance')
|
||||
theme = await themeSnapshot('light-restored-in-native-settings')
|
||||
check('native UI light restores both renderers and storage', [theme.main, theme.settings], [['light', 'light'], ['light', 'light']])
|
||||
await click('.settings-window-close', settings)
|
||||
await step('source-software-gpu-home-calibration')
|
||||
await read('localStorage.setItem("ycode:sidebar-width","284.25");localStorage.setItem("ycode:sidebar-collapsed","0")')
|
||||
main.webContents.reload(); await wait('!!document.querySelector(".source-home")', 'home after source sidebar state reload')
|
||||
if (!main.webContents.debugger.isAttached()) main.webContents.debugger.attach('1.3')
|
||||
await main.webContents.debugger.sendCommand('Emulation.setDeviceMetricsOverride', { width: 1336, height: 901, deviceScaleFactor: 1, mobile: false })
|
||||
main.webContents.setBackgroundThrottling(false); main.showInactive()
|
||||
await wait('innerWidth===1336&&innerHeight===901', 'source-sized viewport'); await pause(300)
|
||||
await click('.sidebar__tab[title="新建任务"]')
|
||||
await read('document.querySelector(".wb-composer__input").focus()')
|
||||
await read('Promise.all([...document.images].map(img=>img.decode().catch(()=>undefined))).then(()=>true)'); await pause(250)
|
||||
const layout = await read(`(()=>{const selectors=['.sidebar','.source-home','.wb-home-header__title','.wb-home-composer','.quick-actions','.wb-home-route__growth-buddy','.source-home-robot img','.cr-input-container'];return {viewport:[innerWidth,innerHeight],devicePixelRatio,theme:document.documentElement.dataset.theme,focused:document.activeElement?.className,nodes:selectors.map(selector=>{const n=document.querySelector(selector);if(!n)return {selector,missing:true};const r=n.getBoundingClientRect(),s=getComputedStyle(n);return {selector,bounds:[r.x,r.y,r.width,r.height],fontFamily:s.fontFamily,fontSize:s.fontSize,transform:s.transform,background:s.backgroundColor,objectFit:s.objectFit,border:s.border,shadow:s.boxShadow,image:n instanceof HTMLImageElement?{src:n.currentSrc,naturalWidth:n.naturalWidth,naturalHeight:n.naturalHeight}:undefined}})}})()`)
|
||||
const composerStructure = await read(`(()=>{const surface=document.querySelector('.wb-home-route__input-box'),main=surface?.querySelector(':scope > .cr-input-box__main'),footer=surface?.querySelector(':scope > .wb-composer__workspace-footer'),input=main?.querySelector('.cr-input-container'),mainBox=main?.getBoundingClientRect(),footerBox=footer?.getBoundingClientRect(),inputBox=input?.getBoundingClientRect();return {footerIsDirectChild:footer?.parentElement===surface,surfaceGap:surface?getComputedStyle(surface).gap:undefined,surfaceBackground:surface?getComputedStyle(surface).backgroundImage:undefined,mainBackground:main?getComputedStyle(main).backgroundColor:undefined,footerBackground:footer?getComputedStyle(footer).backgroundColor:undefined,inputRadius:input?getComputedStyle(input).borderRadius:undefined,inputBounds:inputBox?{x:inputBox.x,right:inputBox.right,bottom:inputBox.bottom}:undefined,footerGap:mainBox&&footerBox?footerBox.top-mainBox.bottom:undefined,mainWidth:mainBox?.width,footerWidth:footerBox?.width}})()`)
|
||||
check('calibrated viewport and source sidebar width', [layout.viewport, layout.nodes.find(node => node.selector === '.sidebar')?.bounds[2]], [[1336, 901], 284.25])
|
||||
check('focused real home composer', layout.focused?.includes('wb-composer__input'), true)
|
||||
check('home footer is nested in the input card with no flex gap', [composerStructure.footerIsDirectChild, composerStructure.surfaceGap], [true, '0px'])
|
||||
check('home input editor touches the footer across the full width', [composerStructure.footerGap, composerStructure.mainWidth, composerStructure.footerWidth], [0, 800, 800])
|
||||
check('home white editor retains all four rounded corners', composerStructure.inputRadius, '24px')
|
||||
check('home main and footer share the outer gradient backdrop', [composerStructure.surfaceBackground?.startsWith('linear-gradient('), composerStructure.mainBackground, composerStructure.footerBackground], [true, 'rgba(0, 0, 0, 0)', 'rgba(0, 0, 0, 0)'])
|
||||
check('calibrated robot source wrapper geometry', layout.nodes.find(node => node.selector === '.wb-home-route__growth-buddy')?.bounds, [1085.125, 286.5, 120, 120])
|
||||
const gpu = { mode: 'visible-swiftshader', electronVersion: process.versions.electron, chromiumVersion: process.versions.chrome,
|
||||
hardwareAccelerationDisabledEarly: true, offscreen: false, windowVisible: main.isVisible(), useAngle: app.commandLine.getSwitchValue('use-angle'),
|
||||
disableGpuCompositing: app.commandLine.hasSwitch('disable-gpu-compositing'), disableGpuSwitchPresent: app.commandLine.hasSwitch('disable-gpu'),
|
||||
captureMethod: 'Page.captureScreenshot({format:png,fromSurface:true,captureBeyondViewport:false})', featureStatus: app.getGPUFeatureStatus(),
|
||||
info: await Promise.race([app.getGPUInfo('complete'), pause(4000).then(() => ({ unavailable: 'timeout' }))]) }
|
||||
const home = await shot('home-focused-swiftshader')
|
||||
const homeImage = nativeImage.createFromBuffer(home), homeBitmap = homeImage.toBitmap(), homeWidth = homeImage.getSize().width
|
||||
const cornerBounds = composerStructure.inputBounds
|
||||
const cornerSamples = [Math.ceil(cornerBounds.x) + 2, Math.floor(cornerBounds.right) - 3].map(x => {
|
||||
const y = Math.floor(cornerBounds.bottom) - 4, offset = (y * homeWidth + x) * 4
|
||||
return { x, y, rgb: [homeBitmap[offset + 2], homeBitmap[offset + 1], homeBitmap[offset]] }
|
||||
})
|
||||
check('both lower editor corner cutouts show the gray backdrop instead of the white page', cornerSamples, samples => samples.every(sample => sample.rgb.every(channel => channel >= 200 && channel < 253)))
|
||||
const sourceBytes = await fs.readFile(referencePath); assert.equal(hash(sourceBytes), referenceSha256)
|
||||
const comparison = compareRobot(sourceBytes, home)
|
||||
await Promise.all([fs.writeFile(path.join(directory, 'robot-source.png'), comparison.sourceCrop), fs.writeFile(path.join(directory, 'robot-ycode.png'), comparison.actualCrop), fs.writeFile(path.join(directory, 'robot-diff-amplified12.png'), comparison.diff)])
|
||||
check('robot static ROI exact pixels at zero tolerance', comparison.report.changedPixels, 0)
|
||||
check('desktop UI generated no sessions/messages', await read('window.ycode.session.list().then(s=>s.length)'), 0)
|
||||
check('no renderer errors', rendererErrors, [])
|
||||
const report = { result: checks.every(item => item.pass) ? 'PASS' : 'FAIL', scope: 'Native production App appearance synchronization/navigation and one focused static robot region. No message sends, fake IPC, hover/video/full-UI parity claim.',
|
||||
evidence: { reference: { path: referencePath, evidenceId: 'ev_4fd56b4f30ca87b4d7012c80195becea384ffa95582b059558a16b8d871d57a8', sha256: referenceSha256 },
|
||||
sourceAsset: { path: assetPath, evidenceId: 'ev_474129797ce3ab73eaaab2cd41247653e7f128c3b89b598158b57b74e1f17e88', sha256: assetSha256 } }, checks, themes, gpu, layout, screenshots, robotComparison: comparison.report, rendererErrors }
|
||||
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2))
|
||||
console.log('APPEARANCE_INTEGRATION_' + report.result + ' ' + directory)
|
||||
console.log('APPEARANCE_ROBOT ' + JSON.stringify(comparison.report))
|
||||
for (const item of checks.filter(check => !check.pass)) console.log('APPEARANCE_FAILURE ' + JSON.stringify(item))
|
||||
app.exit(report.result === 'PASS' ? 0 : 1)
|
||||
})().catch(async error => {
|
||||
await fs.writeFile(path.join(directory, 'failure.json'), JSON.stringify({ phase, error: String(error), stack: error.stack, checks, themes, rendererErrors }, null, 2))
|
||||
console.error(error); if (main && !main.isDestroyed()) await shot('failure').catch(() => undefined); app.exit(1)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user