491 lines
25 KiB
JavaScript
491 lines
25 KiB
JavaScript
// TODO 18: verify the REA-recovered WorkBuddy "Discover Apps" shell against a real isolated YCode renderer.
|
|
// WorkBuddy source CSS: REA ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94
|
|
// (contexts-BkjwYSZT.css SHA-256 50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380).
|
|
const fs = require('node:fs/promises')
|
|
const path = require('node:path')
|
|
const assert = require('node:assert/strict')
|
|
|
|
if (!process.versions.electron) {
|
|
;(async () => {
|
|
const directory = await fs.mkdtemp(path.resolve('.tmp/app-discover-'))
|
|
const data = path.join(directory, 'data')
|
|
const home = path.join(directory, 'home')
|
|
const workspace = path.join(directory, 'workspace')
|
|
await Promise.all([fs.mkdir(data), fs.mkdir(home), fs.mkdir(workspace)])
|
|
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('APP_DISCOVER_')) process.stdout.write(bytes)
|
|
})
|
|
child.stderr.on('data', bytes => {
|
|
output += bytes
|
|
if (bytes.toString().includes('APP_DISCOVER_')) process.stderr.write(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.slice(-8000))
|
|
process.exitCode = code ? 1 : 0
|
|
})
|
|
})().catch(error => {
|
|
console.error(error)
|
|
process.exitCode = 1
|
|
})
|
|
} else {
|
|
const { app, BrowserWindow } = require('electron')
|
|
const { pathToFileURL } = require('node:url')
|
|
const directory = process.env.YCODE_QA_DIRECTORY
|
|
assert.ok(path.relative(path.resolve('.tmp'), directory).startsWith('app-discover-'))
|
|
app.disableHardwareAcceleration()
|
|
app.setPath('userData', path.join(directory, 'data'))
|
|
app.setPath('sessionData', path.join(directory, 'data'))
|
|
app.setPath('home', path.join(directory, 'home'))
|
|
|
|
let main
|
|
let phase = 'boot'
|
|
const checks = []
|
|
const snapshots = {}
|
|
const rendererErrors = []
|
|
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: ' + source.slice(0, 120))), 15000)
|
|
})
|
|
])
|
|
} catch (error) {
|
|
throw Error(error.message + ' - ' + source.slice(0, 240))
|
|
} finally {
|
|
clearTimeout(timer)
|
|
}
|
|
}
|
|
|
|
async function wait(source, label, win = main) {
|
|
for (let index = 0; index < 300; index++) {
|
|
const value = await read(source, win)
|
|
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
|
|
})
|
|
}
|
|
|
|
async function shot(name) {
|
|
if (!main.webContents.debugger.isAttached()) main.webContents.debugger.attach('1.3')
|
|
const image = await main.webContents.debugger.sendCommand('Page.captureScreenshot', {
|
|
format: 'png',
|
|
captureBeyondViewport: false
|
|
})
|
|
await fs.writeFile(path.join(directory, name + '.png'), Buffer.from(image.data, 'base64'))
|
|
}
|
|
|
|
async function mouseMove(selector) {
|
|
const point = await read(`(()=>{
|
|
const node=document.querySelector(${JSON.stringify(selector)});
|
|
if(!node)return null;
|
|
const box=node.getBoundingClientRect();
|
|
return [box.x+box.width/2,box.y+box.height/2]
|
|
})()`)
|
|
assert.ok(point, 'mouse target: ' + selector)
|
|
if (!main.webContents.debugger.isAttached()) main.webContents.debugger.attach('1.3')
|
|
await main.webContents.debugger.sendCommand('Input.dispatchMouseEvent', {
|
|
type: 'mouseMoved',
|
|
x: point[0],
|
|
y: point[1]
|
|
})
|
|
await pause(80)
|
|
}
|
|
|
|
async function mouseClick(selector) {
|
|
const point = await read(`(()=>{
|
|
const node=document.querySelector(${JSON.stringify(selector)});
|
|
if(!node)return null;
|
|
const box=node.getBoundingClientRect();
|
|
return [box.x+box.width/2,box.y+box.height/2]
|
|
})()`)
|
|
assert.ok(point, 'mouse target: ' + selector)
|
|
if (!main.webContents.debugger.isAttached()) main.webContents.debugger.attach('1.3')
|
|
await main.webContents.debugger.sendCommand('Input.dispatchMouseEvent', {
|
|
type: 'mousePressed',
|
|
x: point[0],
|
|
y: point[1],
|
|
button: 'left',
|
|
clickCount: 1
|
|
})
|
|
await main.webContents.debugger.sendCommand('Input.dispatchMouseEvent', {
|
|
type: 'mouseReleased',
|
|
x: point[0],
|
|
y: point[1],
|
|
button: 'left',
|
|
clickCount: 1
|
|
})
|
|
await pause(100)
|
|
}
|
|
|
|
const openPanel = async () => {
|
|
await read(`(()=>{
|
|
const trigger=document.querySelector('.industry-template-switcher__trigger');
|
|
if(!trigger)return false;
|
|
trigger.click();
|
|
return true
|
|
})()`)
|
|
await wait(`!!document.querySelector('.app-discover-popover')`, 'discover panel open')
|
|
await pause(100)
|
|
}
|
|
|
|
const fillSearch = async value => {
|
|
await read(`(()=>{
|
|
const input=document.querySelector('.app-discover-popover__search-input');
|
|
if(!input)return false;
|
|
const setter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set;
|
|
setter.call(input,${JSON.stringify(value)});
|
|
input.dispatchEvent(new Event('input',{bubbles:true}));
|
|
return true
|
|
})()`)
|
|
await pause(100)
|
|
}
|
|
|
|
const chooseApp = async tab => {
|
|
await read(`(()=>{
|
|
const item=document.querySelector('.industry-template-switcher__group-items [data-template-id=${JSON.stringify(tab)}]');
|
|
if(!item)return false;
|
|
item.click();
|
|
return true
|
|
})()`)
|
|
await wait(`(()=>{
|
|
const page=document.querySelector('.yc-capability-center');
|
|
return page&&page.getAttribute('data-capability-tab')===${JSON.stringify(tab)}
|
|
})()`, 'capability page ' + tab)
|
|
await pause(80)
|
|
}
|
|
|
|
const layoutSource = `(()=>{
|
|
const trigger=document.querySelector('.industry-template-switcher__trigger');
|
|
const pop=document.querySelector('.app-discover-popover');
|
|
const panel=pop?.querySelector('.industry-template-switcher__panel');
|
|
const searchHeader=pop?.querySelector('.industry-template-switcher__search-header');
|
|
const search=pop?.querySelector('.industry-template-switcher__search');
|
|
const list=pop?.querySelector('.industry-template-switcher__list');
|
|
if(!trigger||!pop||!panel||!searchHeader||!search||!list)return null;
|
|
const box=node=>{const r=node.getBoundingClientRect();return [r.x,r.y,r.width,r.height]};
|
|
const style=node=>getComputedStyle(node);
|
|
const font=node=>{const s=style(node);return s.fontSize+'/'+s.fontWeight+'/'+s.lineHeight};
|
|
const items=[...pop.querySelectorAll('.industry-template-switcher__group-items > .industry-template-switcher__item')];
|
|
const headers=[...pop.querySelectorAll('.industry-template-switcher__group-title')];
|
|
return {
|
|
theme:document.documentElement.dataset.theme??'light',
|
|
trigger:box(trigger),
|
|
triggerStyle:{padding:style(trigger).padding,height:style(trigger).height,radius:style(trigger).borderRadius,
|
|
background:style(trigger).backgroundColor,borderColor:style(trigger).borderTopColor,color:style(trigger).color,
|
|
font:font(trigger),gap:style(trigger).gap},
|
|
popover:{box:box(pop),position:style(pop).position,width:style(pop).width,maxHeight:style(pop).maxHeight,
|
|
padding:style(pop).padding,radius:style(pop).borderRadius,background:style(pop).backgroundColor,
|
|
borderWidth:style(pop).borderTopWidth,borderColor:style(pop).borderTopColor,display:style(pop).display,
|
|
overflow:style(pop).overflow,shadow:style(pop).boxShadow},
|
|
panel:box(panel),
|
|
searchHeader:{box:box(searchHeader),paddingBottom:style(searchHeader).paddingBottom,marginBottom:style(searchHeader).marginBottom},
|
|
search:{box:box(search),display:style(search).display,padding:style(search).padding,borderColor:style(search).borderTopColor},
|
|
list:{box:box(list),gap:style(list).gap,overflowY:style(list).overflowY},
|
|
headers:headers.map(node=>({text:node.textContent,font:font(node),color:style(node).color})),
|
|
items:items.map(item=>({
|
|
id:item.getAttribute('data-template-id'),box:box(item),padding:style(item).padding,gap:style(item).gap,
|
|
radius:style(item).borderRadius,minHeight:style(item).minHeight,
|
|
title:item.querySelector('.industry-template-switcher__item-title')?.textContent,
|
|
titleFont:font(item.querySelector('.industry-template-switcher__item-title')),
|
|
desc:item.querySelector('.industry-template-switcher__item-desc')?.textContent,
|
|
descFont:font(item.querySelector('.industry-template-switcher__item-desc')),
|
|
icon:box(item.querySelector('.industry-template-switcher__template-icon')),
|
|
selected:item.getAttribute('aria-selected')
|
|
})),
|
|
text:pop.textContent.replace(/\\s+/g,' ').trim(),
|
|
recentGrid:!!pop.querySelector('.industry-template-switcher__recent-grid'),
|
|
empty:pop.querySelector('.industry-template-switcher__empty')?.textContent??null
|
|
}
|
|
})()`
|
|
|
|
;(async () => {
|
|
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href)
|
|
await app.whenReady()
|
|
main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode') ?? BrowserWindow.getAllWindows()[0]
|
|
assert.ok(main)
|
|
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({ windowId: main.id, message })
|
|
})
|
|
await wait(`!!window.ycode && !!document.querySelector('.conversation-list')`, 'main renderer')
|
|
await wait(`!!document.querySelector('.industry-template-switcher__trigger')`, 'discover trigger')
|
|
await pause(500)
|
|
|
|
await step('trigger-semantics')
|
|
check('the trigger is a collapsed dialog button', await read(`(()=>{
|
|
const node=document.querySelector('.industry-template-switcher__trigger');
|
|
return [node.getAttribute('aria-haspopup'),node.getAttribute('aria-expanded'),node.getAttribute('aria-label')]
|
|
})()`), ['dialog', 'false', '发现应用'])
|
|
|
|
await step('closed-state-light')
|
|
snapshots.closedLight = await read(`(()=>{
|
|
const node=document.querySelector('.industry-template-switcher__trigger'),s=getComputedStyle(node),r=node.getBoundingClientRect();
|
|
return {box:[r.x,r.y,r.width,r.height],background:s.backgroundColor,border:s.borderTopColor,color:s.color,
|
|
padding:s.padding,radius:s.borderRadius,font:s.fontSize+'/'+s.fontWeight+'/'+s.lineHeight}
|
|
})()`)
|
|
check('the trigger keeps the recovered 98x28 pill geometry',
|
|
snapshots.closedLight.box.slice(2).map(Math.round), [98, 28])
|
|
check('the trigger keeps the recovered light/secondary paint',
|
|
[snapshots.closedLight.background, snapshots.closedLight.border, snapshots.closedLight.radius],
|
|
['rgb(242, 242, 242)', 'rgb(235, 235, 235)', '100px'])
|
|
check('the trigger keeps the recovered 12/500/20 typography', snapshots.closedLight.font, '12px/500/20px')
|
|
|
|
await step('open-light')
|
|
await openPanel()
|
|
const light = await read(layoutSource)
|
|
snapshots.light = light
|
|
check('the popover is anchored to the trigger right edge', Math.abs(light.popover.box[0] + 240 - (light.trigger[0] + light.trigger[2])) <= 1, true)
|
|
check('the popover sits 6px below the trigger', Math.round(light.popover.box[1] - (light.trigger[1] + light.trigger[3])), 6)
|
|
check('the popover keeps 240px max width and 420px max height',
|
|
[light.popover.width, light.popover.maxHeight], ['240px', '420px'])
|
|
check('the popover keeps 8px padding and 16px radius',
|
|
[light.popover.padding, light.popover.radius], ['8px', '16px'])
|
|
check('the light popover uses the recovered white surface and hairline',
|
|
[light.popover.background, light.popover.borderWidth, light.popover.borderColor],
|
|
['rgb(255, 255, 255)', '1px', 'rgb(222, 222, 222)'])
|
|
check('the popover is a fixed layered surface', [light.popover.position, light.popover.display], ['fixed', 'flex'])
|
|
check('the search header and 28px search field match the source geometry',
|
|
[Math.round(light.searchHeader.box[2]), Math.round(light.searchHeader.box[3]), Math.round(light.search.box[2]), Math.round(light.search.box[3])],
|
|
[222, 33, 222, 28])
|
|
check('the search header keeps the recovered divider spacing',
|
|
[light.searchHeader.paddingBottom, light.searchHeader.marginBottom], ['4px', '4px'])
|
|
check('the list has the recovered 4px gap and transparent scroll surface',
|
|
[light.list.gap, light.list.overflowY], ['4px', 'auto'])
|
|
check('the recommended group contains exactly three local capability entries',
|
|
light.items.map(item => item.id), ['experts', 'skills', 'connectors'])
|
|
check('each app entry keeps the 218x48 / 8px / 12px geometry',
|
|
light.items.map(item => [Math.round(item.box[2]), Math.round(item.box[3]), item.padding, item.gap]),
|
|
[[218, 48, '8px', '12px'], [218, 48, '8px', '12px'], [218, 48, '8px', '12px']])
|
|
check('each app entry keeps an 8px radius and 24px icon',
|
|
light.items.map(item => [item.radius, Math.round(item.icon[2]), Math.round(item.icon[3])]),
|
|
[['8px', 24, 24], ['8px', 24, 24], ['8px', 24, 24]])
|
|
check('entry titles and descriptions keep 13/16 and 11/14 typography',
|
|
light.items.map(item => [item.titleFont, item.descFont]),
|
|
[['13px/400/16px', '11px/400/14px'], ['13px/400/16px', '11px/400/14px'], ['13px/400/16px', '11px/400/14px']])
|
|
check('group headings keep the recovered 11/14 tertiary text',
|
|
light.headers.map(item => [item.text, item.font]), [['推荐', '11px/400/14px']])
|
|
check('the trigger is marked expanded while the panel is open',
|
|
await read(`document.querySelector('.industry-template-switcher__trigger').getAttribute('aria-expanded')`), 'true')
|
|
check('the trigger chevron rotates open', await read(`getComputedStyle(document.querySelector('.industry-template-switcher__chevron')).transform`),
|
|
value => typeof value === 'string' && value !== 'none')
|
|
|
|
await step('hover-enter')
|
|
await mouseMove('.industry-template-switcher__group-items [data-template-id="skills"]')
|
|
check('an unselected entry reveals the 24px 进入 action on hover', await read(`(()=>{
|
|
const item=document.querySelector('[data-template-id="skills"]');
|
|
const action=item.querySelector('.industry-template-switcher__item-action');
|
|
const button=item.querySelector('.industry-template-switcher__item-action-btn');
|
|
const s=getComputedStyle(action),b=getComputedStyle(button);
|
|
return [s.display,b.height,button.textContent]
|
|
})()`), ['flex', '24px', '进入'])
|
|
await shot('app-discover-light')
|
|
|
|
await step('search-match')
|
|
await fillSearch('技能')
|
|
const searched = await read(layoutSource)
|
|
snapshots.search = searched
|
|
check('search narrows the list to the matching local app', searched.items.map(item => item.id), ['skills'])
|
|
check('search highlights the matching glyphs', await read(`document.querySelector('.industry-template-switcher__match')?.textContent`), '技能')
|
|
check('search hides the recent/recommended grouping', searched.headers, [])
|
|
await fillSearch('不存在')
|
|
check('search shows the recovered empty state', await read(`document.querySelector('.industry-template-switcher__empty')?.textContent`), '未找到匹配的应用')
|
|
|
|
await step('keyboard-navigation')
|
|
await fillSearch('中心')
|
|
await read(`document.querySelector('.app-discover-popover__search-input').dispatchEvent(
|
|
new KeyboardEvent('keydown',{key:'ArrowDown',bubbles:true})
|
|
)`)
|
|
await pause(80)
|
|
check('ArrowDown moves focus from search to the first result',
|
|
await read(`document.activeElement?.getAttribute('data-template-id')`), 'experts')
|
|
await read(`document.activeElement.dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowDown',bubbles:true}))`)
|
|
await pause(50)
|
|
check('subsequent ArrowDown moves through results in visual order',
|
|
await read(`document.activeElement?.getAttribute('data-template-id')`), 'skills')
|
|
|
|
await step('navigate-experts')
|
|
await fillSearch('')
|
|
await chooseApp('experts')
|
|
check('clicking an app opens the real expert capability page',
|
|
await read(`document.querySelector('.yc-capability-center')?.getAttribute('data-capability-tab')`), 'experts')
|
|
check('the local app id is persisted as the most recent', await read(`JSON.parse(localStorage.getItem('ycode.app-discover.recent.v1')||'[]')`), ['experts'])
|
|
check('the popover closes after navigation', await read(`!document.querySelector('.app-discover-popover')`), true)
|
|
|
|
await step('active-app-exit')
|
|
await openPanel()
|
|
const activeApp = await read(`(()=>{
|
|
const item=document.querySelector('.industry-template-switcher__group-items [data-template-id="experts"]');
|
|
return {tag:item?.tagName,selected:item?.getAttribute('aria-selected'),
|
|
action:item?.querySelector('.industry-template-switcher__item-action-btn')?.textContent}
|
|
})()`)
|
|
check('the active app uses the source non-button option with check state',
|
|
[activeApp.tag, activeApp.selected, activeApp.action], ['DIV', 'true', '退出'])
|
|
await read(`document.querySelector('[data-template-id="experts"] .industry-template-switcher__item-action-btn').click()`)
|
|
await wait(`!document.querySelector('.yc-capability-center') && !document.querySelector('.app-discover-popover')`, 'exit active app to chat')
|
|
check('退出 returns from the active capability page to chat', await read(`!document.querySelector('.yc-capability-center')`), true)
|
|
|
|
await step('navigate-skills')
|
|
await openPanel()
|
|
await fillSearch('技能')
|
|
await chooseApp('skills')
|
|
check('clicking the skill app opens the real skills page',
|
|
await read(`document.querySelector('.yc-capability-center')?.getAttribute('data-capability-tab')`), 'skills')
|
|
check('recent app order puts the latest choice first', await read(`JSON.parse(localStorage.getItem('ycode.app-discover.recent.v1')||'[]')`), ['skills', 'experts'])
|
|
|
|
await step('navigate-connectors')
|
|
await openPanel()
|
|
await fillSearch('连接器')
|
|
await chooseApp('connectors')
|
|
check('clicking the connector app opens the real connectors page',
|
|
await read(`document.querySelector('.yc-capability-center')?.getAttribute('data-capability-tab')`), 'connectors')
|
|
check('recent app history is de-duplicated and ordered', await read(`JSON.parse(localStorage.getItem('ycode.app-discover.recent.v1')||'[]')`), ['connectors', 'skills', 'experts'])
|
|
|
|
await step('recent-after-reload')
|
|
main.webContents.reload()
|
|
await wait(`!!document.querySelector('.industry-template-switcher__trigger')`, 'renderer after reload')
|
|
await pause(600)
|
|
await openPanel()
|
|
const recent = await read(`(()=>{
|
|
const pop=document.querySelector('.app-discover-popover');
|
|
return {
|
|
grid:[...pop.querySelectorAll('.industry-template-switcher__recent-grid [data-template-id]')].map(node=>node.getAttribute('data-template-id')),
|
|
labels:[...pop.querySelectorAll('.industry-template-switcher__recent-grid .industry-template-switcher__grid-label')].map(node=>node.textContent),
|
|
gridDisplay:getComputedStyle(pop.querySelector('.industry-template-switcher__recent-grid')).display,
|
|
columns:getComputedStyle(pop.querySelector('.industry-template-switcher__recent-grid')).gridTemplateColumns
|
|
}
|
|
})()`)
|
|
snapshots.recent = recent
|
|
check('recent apps survive a renderer reload in recency order', recent.grid, ['connectors', 'skills', 'experts'])
|
|
check('recent apps use a three-column grid and the local labels',
|
|
[recent.gridDisplay, recent.columns.trim().split(/\s+/).length, recent.labels],
|
|
['grid', 3, ['连接器中心', '技能中心', '专家中心']])
|
|
|
|
await step('escape-close')
|
|
await read(`document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}))`)
|
|
await wait(`!document.querySelector('.app-discover-popover')`, 'Escape closes discover panel')
|
|
check('Escape closes the panel and restores trigger focus',
|
|
await read(`document.activeElement===document.querySelector('.industry-template-switcher__trigger')`), true)
|
|
|
|
await step('outside-close')
|
|
await openPanel()
|
|
await mouseClick('.conversation-list-brand-line')
|
|
await wait(`!document.querySelector('.app-discover-popover')`, 'outside click closes discover panel')
|
|
check('outside pointer input closes the panel', await read(`!document.querySelector('.app-discover-popover')`), true)
|
|
|
|
await step('dark-theme')
|
|
await read(`(()=>{
|
|
localStorage.setItem('ycode:theme','dark');
|
|
window.dispatchEvent(new StorageEvent('storage',{key:'ycode:theme',newValue:'dark'}));
|
|
return true
|
|
})()`)
|
|
await wait(`document.documentElement.dataset.theme==='dark'`, 'dark theme applied')
|
|
await openPanel()
|
|
snapshots.dark = await read(`(()=>{
|
|
const trigger=document.querySelector('.industry-template-switcher__trigger');
|
|
const pop=document.querySelector('.app-discover-popover');
|
|
const item=pop.querySelector('.industry-template-switcher__template-icon');
|
|
return {
|
|
theme:document.documentElement.dataset.theme,
|
|
triggerBackground:getComputedStyle(trigger).backgroundColor,
|
|
popoverBackground:getComputedStyle(pop).backgroundColor,
|
|
popoverBorder:getComputedStyle(pop).borderTopColor,
|
|
itemIconBackground:getComputedStyle(item).backgroundColor,
|
|
textColor:getComputedStyle(pop.querySelector('.industry-template-switcher__item-title')).color
|
|
}
|
|
})()`)
|
|
check('dark theme uses the recovered WorkBuddy trigger/popover paint',
|
|
[snapshots.dark.triggerBackground, snapshots.dark.popoverBackground, snapshots.dark.popoverBorder],
|
|
['rgb(46, 46, 46)', 'rgb(36, 36, 36)', 'rgb(61, 61, 61)'])
|
|
check('dark theme keeps the recovered primary text and icon surface',
|
|
[snapshots.dark.textColor, snapshots.dark.itemIconBackground],
|
|
['rgb(255, 255, 255)', 'rgb(41, 41, 41)'])
|
|
await shot('app-discover-dark')
|
|
|
|
await step('restore-light')
|
|
await read(`(()=>{
|
|
localStorage.setItem('ycode:theme','light');
|
|
window.dispatchEvent(new StorageEvent('storage',{key:'ycode:theme',newValue:'light'}));
|
|
return true
|
|
})()`)
|
|
await wait(`document.documentElement.dataset.theme==='light'`, 'light theme restored')
|
|
check('discover panel follows the shared light theme back',
|
|
await read(`getComputedStyle(document.querySelector('.app-discover-popover')).backgroundColor`), 'rgb(255, 255, 255)')
|
|
check('no renderer error logs', rendererErrors, [])
|
|
|
|
const report = {
|
|
result: checks.every(item => item.pass) ? 'PASS' : 'FAIL',
|
|
scope: 'Real isolated YCode App/preload/renderer at 1336x902; TODO 18 Discover Apps trigger, popover geometry, search/empty/keyboard/outside/Escape behavior, recent persistence, three local capability routes, light/dark paint.',
|
|
sourceEvidence: [
|
|
'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94',
|
|
'ev_7eb66ea8d5949eb7d872f1a93a97894a811b3beb7a02eabd81a500409f1045e4',
|
|
'ev_bd22e46c95d3887daba16dbe319389ad74e10afea8f71af9a7639e8bce95ebb4',
|
|
'ev_d3527391f609e4a8cb4d194a24ee17dcbbdfc0a2d58db240ef3686119694d85f',
|
|
'contexts-BkjwYSZT.css SHA-256 50cbd673f7f35fc7f966bdddc6c9f5677fdfe944cb904888d214619fca259380'
|
|
],
|
|
snapshots,
|
|
checks,
|
|
rendererErrors
|
|
}
|
|
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2))
|
|
console.log('APP_DISCOVER_' + report.result + ' ' + directory)
|
|
for (const failure of checks.filter(item => !item.pass)) {
|
|
console.log('APP_DISCOVER_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)
|
|
})
|
|
}
|