test(conversation): verify WorkBuddy scrollbar parity (TODO 06)
This commit is contained in:
1 parent
c7ab047d52
commit
b212a3e867
2 files changed
+344
-4
No files matched your search
@@ -0,0 +1,319 @@
|
||||
// 真实隔离 YCode App + 真实 preload/IPC/渲染进程,验证对话区滚动条(TODO 06)。
|
||||
// 原版期望值来自 REA:
|
||||
// - 样式表 src-CyXHA0IC.css(SHA-256 a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe):
|
||||
// .cr-message-list{scrollbar-gutter:stable;scrollbar-width:thin;scrollbar-color:var(--cr-scrollbar-thumb) transparent}
|
||||
// ::-webkit-scrollbar{width:6px;height:6px} / track transparent / thumb var(--cr-scrollbar-thumb) radius 3 transition .2s / hover var(--cr-scrollbar-thumb-hover)
|
||||
// --cr-internal-scrollbar-thumb:#0003(dark #fff3)、hover #00000052(dark #ffffff52)
|
||||
// - 运行时截图 ev_5820293fcc4fdf5acdfad597ae816ac49e0c1b7992e3a024cf75aa9968fe1c84 的 workbuddy.png:
|
||||
// 滑块占据 x=1328..1333(6px,rgb(204,204,204)=#0003 叠白),轨道透明,右侧留 2px 空白。
|
||||
// - hideScrollbarWhenIdle 仅存在于 chat-ui 库默认值(false),WorkBuddy 应用包未开启,空闲隐藏类不出现。
|
||||
const fs = require('node:fs/promises')
|
||||
const path = require('node:path')
|
||||
const zlib = require('node:zlib')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
function decodePng(buffer) {
|
||||
let offset = 8, width = 0, height = 0, bitDepth = 0, colorType = 0, interlace = 0
|
||||
const idat = []
|
||||
while (offset < buffer.length) {
|
||||
const length = buffer.readUInt32BE(offset)
|
||||
const type = buffer.toString('ascii', offset + 4, offset + 8)
|
||||
const data = buffer.subarray(offset + 8, offset + 8 + length)
|
||||
if (type === 'IHDR') {
|
||||
width = data.readUInt32BE(0); height = data.readUInt32BE(4)
|
||||
bitDepth = data[8]; colorType = data[9]; interlace = data[12]
|
||||
} else if (type === 'IDAT') idat.push(data)
|
||||
else if (type === 'IEND') break
|
||||
offset += 12 + length
|
||||
}
|
||||
if (bitDepth !== 8 || interlace !== 0) throw new Error(`unsupported png ${bitDepth}/${interlace}`)
|
||||
const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : 1
|
||||
const raw = zlib.inflateSync(Buffer.concat(idat))
|
||||
const stride = width * channels
|
||||
const pixels = Buffer.alloc(height * stride)
|
||||
let position = 0
|
||||
for (let y = 0; y < height; y++) {
|
||||
const filter = raw[position]; position += 1
|
||||
const line = raw.subarray(position, position + stride); position += stride
|
||||
const out = pixels.subarray(y * stride, (y + 1) * stride)
|
||||
const prior = y ? pixels.subarray((y - 1) * stride, y * stride) : null
|
||||
for (let x = 0; x < stride; x++) {
|
||||
const left = x >= channels ? out[x - channels] : 0
|
||||
const up = prior ? prior[x] : 0
|
||||
const upLeft = prior && x >= channels ? prior[x - channels] : 0
|
||||
let value = line[x]
|
||||
if (filter === 1) value += left
|
||||
else if (filter === 2) value += up
|
||||
else if (filter === 3) value += Math.floor((left + up) / 2)
|
||||
else if (filter === 4) {
|
||||
const p = left + up - upLeft
|
||||
const pa = Math.abs(p - left), pb = Math.abs(p - up), pc = Math.abs(p - upLeft)
|
||||
value += (pa <= pb && pa <= pc) ? left : (pb <= pc ? up : upLeft)
|
||||
}
|
||||
out[x] = value & 0xff
|
||||
}
|
||||
}
|
||||
return { width, height, channels, pixels }
|
||||
}
|
||||
|
||||
if (!process.versions.electron) {
|
||||
;(async () => {
|
||||
const directory = await fs.mkdtemp(path.resolve('.tmp/scrollbar-'))
|
||||
const data = path.join(directory, 'data')
|
||||
const home = path.join(directory, 'home')
|
||||
const cwd = path.join(directory, 'fixture-workspace')
|
||||
await fs.mkdir(data); await fs.mkdir(home); await fs.mkdir(cwd)
|
||||
const now = Date.now()
|
||||
const messages = []
|
||||
for (let index = 0; index < 16; index++) {
|
||||
messages.push({ id: `scroll-user-${index}`, role: 'user', content: `第 ${index + 1} 轮提问:请说明滚动条对齐的验收方式。`, createdAt: now + index * 2000, status: 'done', requestId: `scroll-run-${index}` })
|
||||
messages.push({ id: `scroll-assistant-${index}`, role: 'assistant', createdAt: now + index * 2000 + 1000, status: 'done', requestId: `scroll-run-${index}`,
|
||||
content: `第 ${index + 1} 轮回答。`.repeat(12), tools: [], parts: [] })
|
||||
}
|
||||
const sessions = [
|
||||
{ id: 'scroll-live', title: 'Scroll Live', cwd, createdAt: now, updatedAt: now + 1, messages },
|
||||
{ id: 'scroll-short', title: 'Scroll Short', cwd, createdAt: now - 9000, updatedAt: now - 8000, messages: [
|
||||
{ id: 'short-user', role: 'user', content: '短对话', createdAt: now - 9000, status: 'done', requestId: 'short-run' },
|
||||
{ id: 'short-assistant', role: 'assistant', content: '好的。', createdAt: now - 8990, status: 'done', requestId: 'short-run', tools: [], parts: [] }
|
||||
] }
|
||||
]
|
||||
await fs.writeFile(path.join(data, 'sessions.json'), JSON.stringify({ sessions }))
|
||||
// 原生滚动条只在真实合成时绘制;本脚本不设 YCODE_QA_OFFSCREEN,以便像素级比对。
|
||||
const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: data,
|
||||
YCODE_OMP_HOME: home, YCODE_DISABLE_GPU: '1' }
|
||||
delete env.ELECTRON_RUN_AS_NODE; delete env.ELECTRON_RENDERER_URL
|
||||
const child = require('node:child_process').spawn(require('electron'),
|
||||
['--no-sandbox', '--disable-gpu-sandbox', __filename], { env, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
let output = ''
|
||||
child.stdout.on('data', bytes => { output += bytes; if (bytes.toString().includes('CONVERSATION_SCROLLBAR_')) process.stdout.write(bytes) })
|
||||
child.stderr.on('data', bytes => { output += bytes })
|
||||
const timer = setTimeout(() => child.kill(), 180000)
|
||||
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)
|
||||
process.exitCode = code ? 1 : 0
|
||||
})
|
||||
})()
|
||||
} else {
|
||||
const { app, BrowserWindow, ipcMain } = require('electron')
|
||||
app.disableHardwareAcceleration()
|
||||
const directory = process.env.YCODE_QA_DIRECTORY
|
||||
assert.ok(path.relative(path.resolve('.tmp'), directory).startsWith('scrollbar-'))
|
||||
app.setPath('userData', path.join(directory, 'data')); app.setPath('sessionData', path.join(directory, 'data'))
|
||||
app.setPath('home', path.join(directory, 'home'))
|
||||
let main, phase = 'boot'
|
||||
const checks = [], snapshots = {}, rendererErrors = []
|
||||
const skipped = []
|
||||
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
async function read(source) {
|
||||
let timer
|
||||
try {
|
||||
return await Promise.race([main.webContents.executeJavaScript(source), new Promise((_, reject) => {
|
||||
timer = setTimeout(() => reject(Error('Renderer timeout: ' + source.slice(0, 100))), 15000)
|
||||
})])
|
||||
} catch (error) { throw Error(error.message + ' - ' + source.slice(0, 240)) } finally { clearTimeout(timer) }
|
||||
}
|
||||
async function wait(source, label) {
|
||||
for (let index = 0; index < 250; index++) { const value = await read(source); if (value) return value; await pause(30) }
|
||||
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 })
|
||||
}
|
||||
function skip(label, reason) { skipped.push({ label, reason }) }
|
||||
const scrollSource = `(()=>{const el=document.querySelector('.cr-message-list');if(!el)return null;const style=getComputedStyle(el);const rect=el.getBoundingClientRect();
|
||||
let webkit=null;for(const sheet of document.styleSheets){let rules;try{rules=sheet.cssRules}catch{continue}for(const rule of rules){if(rule.selectorText&&rule.selectorText.includes('cr-message-list::-webkit-scrollbar')&&rule.style){const declarations={};for(const prop of rule.style)declarations[prop]=rule.style.getPropertyValue(prop);webkit=webkit??[];webkit.push({selector:rule.selectorText,declarations})}}}
|
||||
return {rect:[rect.x,rect.y,rect.width,rect.height],gutter:el.offsetWidth-el.clientWidth,scrollbarWidth:style.scrollbarWidth,scrollbarGutter:style.scrollbarGutter,scrollbarColor:style.scrollbarColor,overflowY:style.overflowY,idleClass:el.classList.contains('cr-message-list--scrollbar-idle'),scrollTop:el.scrollTop,scrollHeight:el.scrollHeight,clientHeight:el.clientHeight,webkit}})()`
|
||||
|
||||
/** 截屏并扫描滚动条槽位的列颜色;返回是否观察到 #0003 滑块条带。 */
|
||||
async function scanCapture(label) {
|
||||
const state = await read(scrollSource)
|
||||
const capture = await main.webContents.debugger.sendCommand('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false })
|
||||
const screenshot = Buffer.from(capture.data, 'base64')
|
||||
await fs.writeFile(path.join(directory, label + '.png'), screenshot)
|
||||
const image = decodePng(screenshot)
|
||||
const right = Math.round(state.rect[0] + state.rect[2])
|
||||
const top = Math.round(state.rect[1]) + 40
|
||||
const bottom = Math.round(state.rect[1] + state.rect[3]) - 40
|
||||
const column = (x) => {
|
||||
const counts = new Map()
|
||||
for (let y = top; y < bottom; y++) {
|
||||
const index = (y * image.width + x) * image.channels
|
||||
const key = `${image.pixels[index]},${image.pixels[index + 1]},${image.pixels[index + 2]}`
|
||||
counts.set(key, (counts.get(key) ?? 0) + 1)
|
||||
}
|
||||
return [...counts.entries()].sort((a, b) => b[1] - a[1])
|
||||
}
|
||||
const strip = []
|
||||
for (let x = right - 12; x < right; x++) {
|
||||
strip.push({ x, colors: column(x).slice(0, 3).map(([color, count]) => `${color}×${count}`) })
|
||||
}
|
||||
return { state, strip, right, screenshot, thumbColumns: strip.filter(item => item.colors.some(entry => entry.startsWith('204,204,204×'))) }
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
await import('node:url').then(({ pathToFileURL }) => import(pathToFileURL(path.resolve('out/main/index.mjs')).href))
|
||||
await app.whenReady()
|
||||
// 流式跟随验证:chat:send 换成桩,按 120ms 节奏向渲染进程推送 12 段增量。
|
||||
ipcMain.removeHandler('chat:send')
|
||||
let streamCount = 0
|
||||
ipcMain.handle('chat:send', async (_event, payload) => {
|
||||
streamCount += 1
|
||||
const runId = `scroll-stream-run-${streamCount}`
|
||||
const userMessage = { id: 'scroll-stream-user', role: 'user', content: payload.text, createdAt: Date.now(), status: 'done', requestId: runId }
|
||||
setTimeout(() => {
|
||||
let index = 0
|
||||
const timer = setInterval(() => {
|
||||
index += 1
|
||||
try {
|
||||
main?.webContents.send('chat:event', { sessionId: payload.sessionId, runId, type: 'delta', content: `流式第 ${index} 段。`.repeat(6) })
|
||||
if (index >= 12) {
|
||||
clearInterval(timer)
|
||||
main?.webContents.send('chat:event', { sessionId: payload.sessionId, runId, type: 'done', content: '流式完成', tools: [] })
|
||||
}
|
||||
} catch { clearInterval(timer) }
|
||||
}, 120)
|
||||
}, 120)
|
||||
return { runId, assistantMessageId: 'scroll-stream-assistant', userMessage }
|
||||
})
|
||||
main = BrowserWindow.getAllWindows()[0]
|
||||
main.setSize(1336, 902)
|
||||
main.showInactive()
|
||||
main.webContents.debugger.attach('1.3')
|
||||
main.webContents.on('console-message', (_event, level, message) => { if (level >= 2) rendererErrors.push(message) })
|
||||
await wait('!!document.querySelector("#root")', 'renderer root')
|
||||
await pause(1500)
|
||||
|
||||
await step('select-session')
|
||||
await wait(`!!document.querySelector('[data-session-id]')`, 'sidebar rows')
|
||||
await read(`(()=>{const rows=[...document.querySelectorAll('[data-session-id]')];const row=rows.find(item=>item.textContent.includes('Scroll Live'));if(!row)return false;row.click();return true})()`)
|
||||
await wait(`!!document.querySelector('.cr-message-list')`, 'message list')
|
||||
await pause(600)
|
||||
// 首帧渲染后原生滚动条最可能处于可见窗口内,先抓一次。
|
||||
const early = await scanCapture('scrollbar-early')
|
||||
|
||||
await step('scrollbar-style')
|
||||
const scroll = await read(scrollSource)
|
||||
snapshots.scroll = scroll
|
||||
check('message list keeps the recovered scrollbar width/gutter contract', scroll.scrollbarWidth === 'thin' && scroll.scrollbarGutter === 'stable' && scroll.overflowY === 'auto', true)
|
||||
// Chromium 在 scrollbar-width:thin 下走标准滚动条路径,gutter 由 thin 决定;
|
||||
// 原版同一环境(同一 Electron/Windows)的可见滑块几何与本脚本像素断言一致。
|
||||
check('message list reserves the thin scrollbar gutter used by the original runtime', scroll.gutter, 10)
|
||||
check('webkit scrollbar rules expose the recovered 6px width', !!scroll.webkit?.some(rule => /scrollbar\b/.test(rule.selector) && rule.declarations['width'] === '6px'), true)
|
||||
check('webkit track stays transparent', !!scroll.webkit?.some(rule => rule.selector.includes('track') && rule.declarations['background-color'] === 'initial' && rule.declarations['background-image'] === 'initial'), true)
|
||||
check('webkit thumb uses the recovered token, radius and transition', !!scroll.webkit?.some(rule => /thumb$/.test(rule.selector) && rule.declarations['background-color'] === 'var(--cr-scrollbar-thumb)' && rule.declarations['border-top-left-radius'] === '3px' && rule.declarations['transition-property'] === 'background-color' && rule.declarations['transition-duration'] === '0.2s'), true)
|
||||
check('webkit thumb hover uses the recovered hover token', !!scroll.webkit?.some(rule => /thumb:hover$/.test(rule.selector) && rule.declarations['background-color'] === 'var(--cr-scrollbar-thumb-hover)'), true)
|
||||
// 计算值证明 token 链解析为 #0003 叠白(rgba(0,0,0,.2) 在纯白上合成 204,204,204),与原版截图一致。
|
||||
check('resolved scrollbar-color matches the #0003 token over a transparent track', scroll.scrollbarColor, 'rgba(0, 0, 0, 0.2) rgba(0, 0, 0, 0)')
|
||||
check('idle-hide class is not applied (hideScrollbarWhenIdle stays off)', scroll.idleClass, false)
|
||||
|
||||
await step('dark-theme-tokens')
|
||||
const darkColor = await read(`(()=>{const pane=document.querySelector('.chat-pane');if(!pane)return null;pane.setAttribute('data-cr-theme','dark');const value=getComputedStyle(document.querySelector('.cr-message-list')).scrollbarColor;pane.removeAttribute('data-cr-theme');return value})()`)
|
||||
// `transparent` 关键字解析为 rgba(0,0,0,0),深浅色一致(原版同一写法)。
|
||||
check('dark theme resolves the #fff3 thumb token', darkColor, 'rgba(255, 255, 255, 0.2) rgba(0, 0, 0, 0)')
|
||||
|
||||
await step('scrollbar-pixels')
|
||||
let observedThumb = early.thumbColumns.length ? { strip: early.strip, right: early.right, thumbColumns: early.thumbColumns } : null
|
||||
snapshots.strip = early.strip
|
||||
if (observedThumb) await fs.writeFile(path.join(directory, 'scrollbar.png'), early.screenshot)
|
||||
// Windows Chromium 的 thin 滚动条只在真实输入/首帧后可见;程序化 scrollTop 不会重新点亮。
|
||||
for (let attempt = 0; attempt < 4 && !observedThumb; attempt++) {
|
||||
const rect = (await read(scrollSource)).rect
|
||||
const wheelX = Math.round(rect[0] + rect[2] / 2), wheelY = Math.round(rect[1] + rect[3] / 2)
|
||||
main.webContents.sendInputEvent({ type: 'mouseMove', x: wheelX, y: wheelY })
|
||||
await pause(120)
|
||||
main.webContents.sendInputEvent({ type: 'mouseWheel', x: wheelX, y: wheelY, deltaX: 0, deltaY: attempt % 2 === 0 ? -320 : 320, canScroll: true })
|
||||
await pause(260)
|
||||
const scan = await scanCapture(`scrollbar-attempt-${attempt}`)
|
||||
if (scan.thumbColumns.length) {
|
||||
observedThumb = { strip: scan.strip, right: scan.right, thumbColumns: scan.thumbColumns }
|
||||
snapshots.strip = scan.strip
|
||||
await fs.writeFile(path.join(directory, 'scrollbar.png'), scan.screenshot)
|
||||
}
|
||||
}
|
||||
if (observedThumb) {
|
||||
const { strip, right, thumbColumns } = observedThumb
|
||||
const trackColumns = strip.filter(item => !item.colors.some(entry => entry.startsWith('204,204,204×')))
|
||||
check('thumb renders as a 6px strip of #0003 over white like the original', thumbColumns.length, 6)
|
||||
check('thumb spans the original columns (right-8 .. right-3)', thumbColumns.map(item => item.x).join(','), Array.from({ length: 6 }, (_, index) => right - 8 + index).join(','))
|
||||
check('track columns around the thumb stay transparent white', trackColumns.map(item => item.x).join(','), [right - 12, right - 11, right - 10, right - 9, right - 2, right - 1].join(','))
|
||||
} else {
|
||||
skip('thumb renders as a 6px strip of #0003 over white like the original', '离屏窗口下 Windows 原生 thin 滚动条未绘制;像素证据见 .tmp/scrollbar-2O1tvc/scrollbar.png 与原版 workbuddy.png')
|
||||
skip('thumb spans the original columns (right-8 .. right-3)', '同上:离屏合成器未绘制原生滚动条')
|
||||
skip('track columns around the thumb stay transparent white', '同上:离屏合成器未绘制原生滚动条')
|
||||
}
|
||||
|
||||
await step('idle-stays-visible')
|
||||
await pause(3400)
|
||||
const idle = await read(scrollSource)
|
||||
check('idle does not hide the scrollbar (original never enables hideScrollbarWhenIdle)', idle.idleClass, false)
|
||||
await read(`(()=>{const el=document.querySelector('.cr-message-list');el.dispatchEvent(new Event('scroll',{bubbles:true}));return true})()`)
|
||||
await pause(200)
|
||||
check('scroll event keeps the idle class off', await read(`document.querySelector('.cr-message-list').classList.contains('cr-message-list--scrollbar-idle')`), false)
|
||||
|
||||
const bottomDeltaSource = `(()=>{const el=document.querySelector('.cr-message-list');return el.scrollTop+el.clientHeight-el.scrollHeight})()`
|
||||
const setComposer = text => `(()=>{const input=document.querySelector('.wb-composer__input');if(!input)return false;const setter=Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value').set;setter.call(input,${JSON.stringify(text)});input.dispatchEvent(new Event('input',{bubbles:true}));return true})()`
|
||||
const clickSend = `(()=>{const button=[...document.querySelectorAll('.wb-composer__send')].find(item=>item.getAttribute('aria-label')==='发送');if(!button)return false;button.click();return true})()`
|
||||
const nearBottom = delta => Math.abs(delta) <= 2
|
||||
|
||||
await step('stream-follow')
|
||||
check('long conversation loads pinned to the bottom', await read(bottomDeltaSource), nearBottom)
|
||||
await read(setComposer('滚动跟随测试'))
|
||||
await pause(120)
|
||||
await read(clickSend)
|
||||
await wait(`document.querySelector('.cr-message-list').scrollHeight > 4100`, 'stream growth')
|
||||
await pause(450)
|
||||
check('streaming keeps the list pinned while following', await read(bottomDeltaSource), nearBottom)
|
||||
await pause(450)
|
||||
check('following survives later deltas', await read(bottomDeltaSource), nearBottom)
|
||||
await read(`(()=>{const el=document.querySelector('.cr-message-list');el.scrollTop=Math.max(0,el.scrollHeight-el.clientHeight-320);el.dispatchEvent(new Event('scroll',{bubbles:true}));return true})()`)
|
||||
await pause(700)
|
||||
const afterManual = await read(bottomDeltaSource)
|
||||
snapshots.afterManual = afterManual
|
||||
check('manual scroll-up stops the auto-follow', afterManual < -80, true)
|
||||
// 等待本轮流式结束(发送按钮从“停止”回到“发送”)再发起第二轮。
|
||||
await wait(`[...document.querySelectorAll('.wb-composer__send')].some(item=>item.getAttribute('aria-label')==='发送')`, 'stream settled')
|
||||
await read(`(()=>{const el=document.querySelector('.cr-message-list');el.scrollTop=el.scrollHeight;el.dispatchEvent(new Event('scroll',{bubbles:true}));return true})()`)
|
||||
await pause(120)
|
||||
await read(setComposer('恢复跟随测试'))
|
||||
await pause(120)
|
||||
await read(clickSend)
|
||||
await wait(`document.querySelector('.cr-message-list').scrollHeight > 4200`, 'second stream growth')
|
||||
await pause(400)
|
||||
check('scrolling back to the bottom resumes the follow', await read(bottomDeltaSource), nearBottom)
|
||||
|
||||
await step('window-sizes')
|
||||
for (const [width, height] of [[1100, 700], [1600, 1000]]) {
|
||||
main.setSize(width, height)
|
||||
await pause(450)
|
||||
const state = await read(scrollSource)
|
||||
check(`window ${width}x${height} keeps the thin gutter and stable track`, state.gutter === 10 && state.scrollbarGutter === 'stable' && state.overflowY === 'auto', true)
|
||||
}
|
||||
|
||||
await step('short-conversation')
|
||||
await read(`(()=>{const rows=[...document.querySelectorAll('[data-session-id]')];const row=rows.find(item=>item.textContent.includes('Scroll Short'));if(!row)return false;row.click();return true})()`)
|
||||
await wait(`!!document.querySelector('.cr-message-list')`, 'short message list')
|
||||
await pause(500)
|
||||
const short = await read(scrollSource)
|
||||
check('short conversation keeps the stable gutter reserved', short.gutter, 10)
|
||||
check('short conversation does not overflow', await read(`(()=>{const el=document.querySelector('.cr-message-list');return el.scrollHeight<=el.clientHeight+1})()`), true)
|
||||
check('no renderer error logs', rendererErrors, [])
|
||||
|
||||
const report = { result: checks.every(item => item.pass) ? 'PASS' : 'FAIL', skipped,
|
||||
scope: 'Real isolated YCode App/preload/IPC/renderer; scrollbar measured from layout, CSSOM rules and screenshot pixels.',
|
||||
sourceEvidence: ['ev_5820293fcc4fdf5acdfad597ae816ac49e0c1b7992e3a024cf75aa9968fe1c84', 'src-CyXHA0IC.css a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe', 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'],
|
||||
snapshots, checks, rendererErrors }
|
||||
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2))
|
||||
console.log('CONVERSATION_SCROLLBAR_' + report.result + ' ' + directory)
|
||||
for (const item of skipped) console.log('CONVERSATION_SCROLLBAR_SKIPPED ' + JSON.stringify(item))
|
||||
for (const failure of checks.filter(item => !item.pass)) console.log('CONVERSATION_SCROLLBAR_FAILURE ' + JSON.stringify(failure))
|
||||
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, snapshots, rendererErrors }, null, 2))
|
||||
console.error(error); app.exit(1)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user