feat: ship skill and MCP workflows with signed updater for v0.0.7
This commit is contained in:
1 parent
1b52df0603
commit
a01085c656
136 files changed
+18646
-217
No files matched your search
@@ -0,0 +1,560 @@
|
||||
// Real isolated YCode App/preload/renderer verification for the update UI.
|
||||
// Production out/main/preload are loaded unchanged. Only the harness replaces updates:* IPC handlers
|
||||
// so checking, download progress, failure/retry, dismissal and install can be exercised deterministically.
|
||||
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/updates-ui-'))
|
||||
const data = path.join(directory, 'data')
|
||||
const home = path.join(directory, 'home')
|
||||
await Promise.all([fs.mkdir(data), fs.mkdir(home)])
|
||||
const env = {
|
||||
...process.env,
|
||||
YCODE_QA_DIRECTORY: directory,
|
||||
YCODE_USER_DATA_DIR: data,
|
||||
YCODE_DISABLE_GPU: '1',
|
||||
YCODE_DISABLE_SYSTEM_INTEGRATION: '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('UPDATES_UI_')) process.stdout.write(bytes)
|
||||
})
|
||||
child.stderr.on('data', bytes => {
|
||||
output += bytes
|
||||
if (bytes.toString().includes('UPDATES_UI_')) process.stderr.write(bytes)
|
||||
})
|
||||
const timer = setTimeout(() => child.kill(), 300000)
|
||||
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, ipcMain } = require('electron')
|
||||
const { pathToFileURL } = require('node:url')
|
||||
const packageVersion = require('../package.json').version
|
||||
const directory = process.env.YCODE_QA_DIRECTORY
|
||||
assert.ok(path.relative(path.resolve('.tmp'), directory).startsWith('updates-ui-'))
|
||||
|
||||
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 settings
|
||||
let phase = 'boot'
|
||||
const checks = []
|
||||
const snapshots = {}
|
||||
const rendererErrors = []
|
||||
const actionCounts = {
|
||||
check: 0,
|
||||
download: 0,
|
||||
install: 0,
|
||||
dismiss: 0,
|
||||
downloadPage: 0,
|
||||
log: 0
|
||||
}
|
||||
let updateState = {
|
||||
status: 'idle',
|
||||
currentVersion: packageVersion,
|
||||
lastExplicit: false
|
||||
}
|
||||
|
||||
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
function broadcastUpdateState() {
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
if (!win.isDestroyed()) win.webContents.send('updates:state', { ...updateState })
|
||||
}
|
||||
}
|
||||
|
||||
function setUpdateState(patch, replace = false) {
|
||||
updateState = {
|
||||
...(replace ? { currentVersion: updateState.currentVersion } : updateState),
|
||||
...patch,
|
||||
lastChecked: new Date().toISOString()
|
||||
}
|
||||
broadcastUpdateState()
|
||||
return { ...updateState }
|
||||
}
|
||||
|
||||
const nativeHandle = ipcMain.handle.bind(ipcMain)
|
||||
const absorbedUpdateHandlers = []
|
||||
ipcMain.handle = (channel, listener) => {
|
||||
if (channel.startsWith('updates:')) {
|
||||
absorbedUpdateHandlers.push(channel)
|
||||
return
|
||||
}
|
||||
return nativeHandle(channel, listener)
|
||||
}
|
||||
|
||||
nativeHandle('updates:get-state', () => ({ ...updateState }))
|
||||
nativeHandle('updates:check', () => {
|
||||
actionCounts.check += 1
|
||||
return setUpdateState({ status: 'checking', error: undefined, lastExplicit: true })
|
||||
})
|
||||
nativeHandle('updates:download', () => {
|
||||
actionCounts.download += 1
|
||||
return setUpdateState({ status: 'downloading', progress: 37, error: undefined, lastExplicit: true })
|
||||
})
|
||||
nativeHandle('updates:install', () => {
|
||||
actionCounts.install += 1
|
||||
return setUpdateState({ status: 'installing', error: undefined, lastExplicit: true })
|
||||
})
|
||||
nativeHandle('updates:dismiss', () => {
|
||||
actionCounts.dismiss += 1
|
||||
return setUpdateState({
|
||||
dismissedVersion: updateState.version,
|
||||
status: updateState.status === 'installing' ? 'ready' : updateState.status
|
||||
})
|
||||
})
|
||||
nativeHandle('updates:open-download-page', () => {
|
||||
actionCounts.downloadPage += 1
|
||||
})
|
||||
nativeHandle('updates:open-log', () => {
|
||||
actionCounts.log += 1
|
||||
})
|
||||
|
||||
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 += 1) {
|
||||
const value = await read(source, win)
|
||||
if (value) return value
|
||||
await pause(35)
|
||||
}
|
||||
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 observe(win) {
|
||||
win.webContents.on('console-message', (_event, level, message) => {
|
||||
if (level >= 3) rendererErrors.push({ windowId: win.id, message })
|
||||
})
|
||||
}
|
||||
|
||||
async function shot(name, win = main) {
|
||||
const image = await win.webContents.capturePage()
|
||||
await fs.writeFile(path.join(directory, name + '.png'), image.toPNG())
|
||||
}
|
||||
|
||||
async function click(selector, win = main) {
|
||||
const clicked = await read(`(()=>{
|
||||
const node=document.querySelector(${JSON.stringify(selector)});
|
||||
if(!node)return false;
|
||||
node.click();
|
||||
return true
|
||||
})()`, win)
|
||||
assert.equal(clicked, true, 'click target: ' + selector)
|
||||
await pause(100)
|
||||
}
|
||||
|
||||
async function openSettings(section) {
|
||||
await read(`window.ycode.app.openSettings(${JSON.stringify(section)})`)
|
||||
for (let index = 0; index < 240; index += 1) {
|
||||
settings = BrowserWindow.getAllWindows().find(win => win !== main)
|
||||
if (settings && !settings.webContents.isLoading()) break
|
||||
await pause(25)
|
||||
}
|
||||
assert.ok(settings, 'settings window')
|
||||
await wait('!!document.querySelector(".settings-window-root")', 'settings root', settings)
|
||||
await wait('!!document.querySelector(".help-feedback-panel")', 'about panel', settings)
|
||||
await pause(300)
|
||||
}
|
||||
|
||||
async function setTheme(theme) {
|
||||
await read(`(()=>{
|
||||
localStorage.setItem('ycode:theme',${JSON.stringify(theme)});
|
||||
window.dispatchEvent(new StorageEvent('storage',{key:'ycode:theme',newValue:${JSON.stringify(theme)}}));
|
||||
return true
|
||||
})()`)
|
||||
await wait(`document.documentElement.dataset.theme===${JSON.stringify(theme)}`, 'main theme ' + theme)
|
||||
if (settings && !settings.isDestroyed()) {
|
||||
await wait(`document.documentElement.dataset.theme===${JSON.stringify(theme)}`, 'settings theme ' + theme, settings)
|
||||
}
|
||||
}
|
||||
|
||||
const cardSource = `(()=>{
|
||||
const card=document.querySelector('.update-sidebar-card');
|
||||
const compact=document.querySelector('.update-sidebar-card-collapsed');
|
||||
if(!card&&!compact)return null;
|
||||
const box=node=>{const r=node.getBoundingClientRect();return [r.x,r.y,r.width,r.height]};
|
||||
const style=node=>getComputedStyle(node);
|
||||
const progress=card?.querySelector('.update-sidebar-card__progress');
|
||||
const primary=card?.querySelector('[data-update-action]');
|
||||
return {
|
||||
status:card?.getAttribute('data-status')??null,
|
||||
compact:!!compact,
|
||||
card:card?{box:box(card),background:style(card).backgroundColor,border:style(card).borderTopColor,
|
||||
title:card.querySelector('.update-sidebar-card__title')?.textContent??'',
|
||||
description:card.querySelector('.update-sidebar-card__description')?.textContent??'',
|
||||
primary:primary?{text:primary.textContent.trim(),kind:primary.getAttribute('data-update-action'),disabled:primary.disabled}:null,
|
||||
progress:progress?{value:progress.getAttribute('aria-valuenow'),width:progress.getBoundingClientRect().width}:null,
|
||||
manual:!!card.querySelector('[data-update-manual]'),log:!!card.querySelector('[data-update-log]')}:null,
|
||||
accountCardPresent:!!document.querySelector('.conversation-list-footer .update-sidebar-card-host')
|
||||
}
|
||||
})()`
|
||||
|
||||
const accountSource = `(()=>{
|
||||
const row=document.querySelector('.ycode-account-menu [data-update-action]');
|
||||
if(!row)return null;
|
||||
return {status:row.getAttribute('data-update-status'),kind:row.getAttribute('data-update-action'),
|
||||
label:row.querySelector('.user-menu-item-label')?.textContent??'',
|
||||
value:row.querySelector('.user-menu-item-value')?.textContent??'',
|
||||
ariaDisabled:row.getAttribute('aria-disabled')}
|
||||
})()`
|
||||
|
||||
const aboutSource = `(()=>{
|
||||
const row=document.querySelector('#settings-about-version');
|
||||
const button=row?.querySelector('[data-update-action]');
|
||||
const feedback=document.querySelector('.update-ui-feedback');
|
||||
if(!row||!button)return null;
|
||||
return {status:button.getAttribute('data-update-status'),kind:button.getAttribute('data-update-action'),
|
||||
label:button.textContent.trim(),disabled:button.disabled,feedback:feedback?.textContent.replace(/\\s+/g,' ').trim()??'',
|
||||
currentVersion:row.querySelector('.help-feedback-panel__card-value')?.textContent??''}
|
||||
})()`
|
||||
|
||||
;(async () => {
|
||||
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href)
|
||||
await app.whenReady()
|
||||
for (let index = 0; index < 240; index += 1) {
|
||||
main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode')
|
||||
if (main && !main.webContents.isLoading()) break
|
||||
await pause(25)
|
||||
}
|
||||
assert.ok(main, 'main window')
|
||||
observe(main)
|
||||
main.setSize(1336, 902)
|
||||
main.showInactive()
|
||||
await wait('!!window.ycode&&!!document.querySelector(".conversation-list")', 'main renderer')
|
||||
updateState.currentVersion = packageVersion
|
||||
broadcastUpdateState()
|
||||
await pause(300)
|
||||
|
||||
await step('ready-state')
|
||||
setUpdateState({
|
||||
status: 'ready',
|
||||
version: '0.0.3',
|
||||
notes: '改进更新卡片、失败重试与安装确认。',
|
||||
progress: undefined,
|
||||
error: undefined,
|
||||
dismissedVersion: undefined
|
||||
}, true)
|
||||
await wait(`!!document.querySelector('.update-sidebar-card[data-status="ready"]')`, 'ready update card')
|
||||
snapshots.ready = await read(cardSource)
|
||||
check('ready update card is mounted permanently above the account trigger',
|
||||
[snapshots.ready.status, snapshots.ready.accountCardPresent, snapshots.ready.compact], ['ready', true, false])
|
||||
check('ready card offers restart and shows the target version',
|
||||
[snapshots.ready.card.primary.text, /v0\.0\.3/.test(snapshots.ready.card.title)], ['重启升级', true])
|
||||
|
||||
await step('account-sync-ready')
|
||||
await click('.user-menu-trigger--workbuddy')
|
||||
await wait('!!document.querySelector(".ycode-account-menu")', 'account menu')
|
||||
snapshots.accountReady = await read(accountSource)
|
||||
check('account menu shares the ready state and remains actionable',
|
||||
[snapshots.accountReady.status, snapshots.accountReady.kind, snapshots.accountReady.label, snapshots.accountReady.ariaDisabled],
|
||||
['ready', 'install', '重启升级', 'false'])
|
||||
await shot('updates-main-light-ready')
|
||||
await click('.ycode-account-menu [data-update-action="install"]')
|
||||
check('account menu install action reaches the updates IPC bridge', actionCounts.install, 1)
|
||||
await wait(`document.querySelector('.ycode-account-menu [data-update-status]')?.getAttribute('data-update-status')==='installing'`, 'account installing state')
|
||||
await read(`document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',bubbles:true}))`)
|
||||
await pause(100)
|
||||
await shot('updates-main-light-ready-card')
|
||||
|
||||
await step('dismiss-and-reveal')
|
||||
setUpdateState({
|
||||
status: 'ready',
|
||||
version: '0.0.3',
|
||||
progress: undefined,
|
||||
error: undefined,
|
||||
dismissedVersion: undefined
|
||||
})
|
||||
await wait(`!!document.querySelector('.update-sidebar-card[data-status="ready"] [data-update-dismiss]')`, 'ready card dismiss')
|
||||
await click('[data-update-dismiss]')
|
||||
await wait('!!document.querySelector(".update-sidebar-card-collapsed")', 'collapsed update entry')
|
||||
check('closing the card dismisses this version and keeps a compact update entry',
|
||||
[actionCounts.dismiss, await read(`!document.querySelector('.update-sidebar-card')`)], [1, true])
|
||||
await click('[data-update-reveal]')
|
||||
await wait('!!document.querySelector(".update-sidebar-card")', 'revealed collapsed update card')
|
||||
check('the compact update entry can reveal the dismissed version card again',
|
||||
await read(`!!document.querySelector('.update-sidebar-card[data-status="ready"]')`), true)
|
||||
|
||||
await step('error-and-manual-links')
|
||||
setUpdateState({
|
||||
status: 'error',
|
||||
version: '0.0.4',
|
||||
progress: undefined,
|
||||
error: '模拟更新检查失败',
|
||||
dismissedVersion: undefined
|
||||
})
|
||||
await wait(`!!document.querySelector('.update-sidebar-card[data-status="error"]')`, 'error update card')
|
||||
snapshots.error = await read(cardSource)
|
||||
check('error state exposes retry, manual download and update log',
|
||||
[snapshots.error.card.primary.text, snapshots.error.card.manual, snapshots.error.card.log], ['重试', true, true])
|
||||
await click('[data-update-manual]')
|
||||
await click('[data-update-log]')
|
||||
check('manual download and update log actions reach the updates IPC bridge',
|
||||
[actionCounts.downloadPage, actionCounts.log], [1, 1])
|
||||
await click('.update-sidebar-card [data-update-action="check"]')
|
||||
await wait(`!!document.querySelector('.update-sidebar-card')===false`, 'checking hides the non-actionable card')
|
||||
check('retry invokes the real check IPC action', actionCounts.check, 1)
|
||||
setUpdateState({ status: 'ready', version: '0.0.4', error: undefined })
|
||||
await wait(`!!document.querySelector('.update-sidebar-card[data-status="ready"]')`, 'ready after retry')
|
||||
|
||||
await step('settings-ready')
|
||||
await openSettings('helpFeedback')
|
||||
observe(settings)
|
||||
snapshots.aboutReady = await read(aboutSource, settings)
|
||||
check('settings About shows the shared ready state',
|
||||
[snapshots.aboutReady.status, snapshots.aboutReady.kind, snapshots.aboutReady.label, /新版本 v0\.0\.4 已就绪/.test(snapshots.aboutReady.feedback)],
|
||||
['ready', 'install', '重启升级', true])
|
||||
await shot('updates-settings-light-ready', settings)
|
||||
|
||||
await step('settings-download-sync')
|
||||
setUpdateState({
|
||||
status: 'available',
|
||||
version: '0.0.5',
|
||||
progress: undefined,
|
||||
error: undefined,
|
||||
dismissedVersion: undefined
|
||||
})
|
||||
await wait(`document.querySelector('#settings-about-version [data-update-status]')?.getAttribute('data-update-status')==='available'`, 'settings available state', settings)
|
||||
await wait(`!!document.querySelector('.update-sidebar-card[data-status="available"]')`, 'main available card')
|
||||
await click('#settings-about-version [data-update-action="download"]', settings)
|
||||
await wait(`document.querySelector('#settings-about-version [data-update-action]')?.textContent.includes('下载中 37%')`, 'settings download progress', settings)
|
||||
await wait(`document.querySelector('.update-sidebar-card[data-status="downloading"]')?.querySelector('.update-sidebar-card__progress')?.getAttribute('aria-valuenow')==='37'`, 'main download progress')
|
||||
snapshots.downloading = {
|
||||
main: await read(cardSource),
|
||||
settings: await read(aboutSource, settings)
|
||||
}
|
||||
check('download initiated in settings synchronizes to the sidebar card',
|
||||
[actionCounts.download, snapshots.downloading.main.status, snapshots.downloading.main.card.progress.value, snapshots.downloading.settings.label],
|
||||
[1, 'downloading', '37', '下载中 37%'])
|
||||
setUpdateState({ status: 'ready', version: '0.0.5', progress: 100 })
|
||||
await wait(`document.querySelector('#settings-about-version [data-update-action]')?.textContent.trim()==='重启升级'`, 'settings ready after download', settings)
|
||||
await wait(`!!document.querySelector('.update-sidebar-card[data-status="ready"]')`, 'main ready after download')
|
||||
check('download completion synchronizes settings and sidebar', await Promise.all([
|
||||
read(`document.querySelector('#settings-about-version [data-update-action]')?.textContent.trim()`, settings),
|
||||
read(`document.querySelector('.update-sidebar-card__primary')?.textContent.trim()`)
|
||||
]), ['重启升级', '重启升级'])
|
||||
|
||||
await step('dark')
|
||||
await setTheme('dark')
|
||||
snapshots.dark = {
|
||||
main: await read(cardSource),
|
||||
settings: await read(aboutSource, settings)
|
||||
}
|
||||
check('update card remains rendered with shared dark theme text and surface',
|
||||
[snapshots.dark.main.status, snapshots.dark.main.card.background !== 'rgba(0, 0, 0, 0)'], ['ready', true])
|
||||
await shot('updates-main-dark')
|
||||
await shot('updates-settings-dark', settings)
|
||||
|
||||
await step('narrow-light')
|
||||
await setTheme('light')
|
||||
main.setSize(940, 800)
|
||||
await pause(300)
|
||||
snapshots.narrow = await read(`(()=>{
|
||||
const card=document.querySelector('.update-sidebar-card');
|
||||
const sidebar=document.querySelector('.conversation-list');
|
||||
if(!card||!sidebar)return null;
|
||||
const c=card.getBoundingClientRect(),s=sidebar.getBoundingClientRect();
|
||||
return {viewport:innerWidth,card:[c.x,c.y,c.width,c.height],sidebar:[s.x,s.y,s.width,s.height],
|
||||
horizontalOverflow:document.documentElement.scrollWidth>innerWidth,
|
||||
title:card.querySelector('.update-sidebar-card__title')?.textContent??''}
|
||||
})()`)
|
||||
check('narrow window keeps the update card inside the sidebar without horizontal overflow',
|
||||
[snapshots.narrow.horizontalOverflow, snapshots.narrow.card[2] <= snapshots.narrow.sidebar[2] + 1],
|
||||
[false, true])
|
||||
await shot('updates-main-narrow')
|
||||
|
||||
await step('up-to-date')
|
||||
setUpdateState({
|
||||
status: 'up-to-date',
|
||||
version: undefined,
|
||||
progress: undefined,
|
||||
error: undefined,
|
||||
dismissedVersion: undefined
|
||||
}, true)
|
||||
await wait('!document.querySelector(".update-sidebar-card")', 'up-to-date hides sidebar card')
|
||||
await wait(`document.querySelector('#settings-about-version [data-update-status]')?.getAttribute('data-update-status')==='up-to-date'`, 'settings up-to-date', settings)
|
||||
await click('.user-menu-trigger--workbuddy')
|
||||
await wait('!!document.querySelector(".ycode-account-menu")', 'account menu up-to-date')
|
||||
snapshots.upToDate = {
|
||||
mainCard: await read(`!document.querySelector('.update-sidebar-card')`),
|
||||
account: await read(accountSource),
|
||||
settings: await read(aboutSource, settings)
|
||||
}
|
||||
check('up-to-date state hides the sidebar card and synchronizes account/settings',
|
||||
[snapshots.upToDate.mainCard, snapshots.upToDate.account.status, snapshots.upToDate.account.value, /已是最新版本/.test(snapshots.upToDate.settings.feedback)],
|
||||
[true, 'up-to-date', '已是最新', true])
|
||||
|
||||
await step('unsupported')
|
||||
setUpdateState({
|
||||
status: 'checking',
|
||||
version: undefined,
|
||||
progress: undefined,
|
||||
error: undefined,
|
||||
dismissedVersion: undefined,
|
||||
unfinishedVersion: undefined
|
||||
}, true)
|
||||
await wait(`document.querySelector('.ycode-account-menu [data-update-status]')?.getAttribute('data-update-status')==='checking'`, 'background check clears explicit intent')
|
||||
setUpdateState({
|
||||
status: 'unsupported',
|
||||
version: undefined,
|
||||
progress: undefined,
|
||||
error: '当前平台不支持自动更新',
|
||||
dismissedVersion: undefined,
|
||||
unfinishedVersion: undefined
|
||||
}, true)
|
||||
await wait(`document.querySelector('.ycode-account-menu [data-update-status]')?.getAttribute('data-update-status')==='unsupported'`, 'unsupported account row')
|
||||
snapshots.unsupported = {
|
||||
main: await read(cardSource),
|
||||
account: await read(accountSource),
|
||||
settings: await read(aboutSource, settings)
|
||||
}
|
||||
check('background unsupported state stays out of the sidebar while account/settings remain honest',
|
||||
[snapshots.unsupported.main, snapshots.unsupported.account.label, snapshots.unsupported.account.value,
|
||||
snapshots.unsupported.account.kind, /当前平台不支持自动更新/.test(snapshots.unsupported.settings.feedback)],
|
||||
[null, '手动下载', '需手动下载', 'manual', true])
|
||||
|
||||
await step('background-error-quiet')
|
||||
setUpdateState({
|
||||
status: 'error',
|
||||
version: undefined,
|
||||
progress: undefined,
|
||||
error: '后台检查失败',
|
||||
dismissedVersion: undefined,
|
||||
unfinishedVersion: undefined
|
||||
}, true)
|
||||
await wait(`document.querySelector('.ycode-account-menu [data-update-status]')?.getAttribute('data-update-status')==='error'`, 'quiet background error')
|
||||
await pause(150)
|
||||
check('background error with no released version does not open a sidebar card',
|
||||
[await read(`!document.querySelector('.update-sidebar-card')`), (await read(accountSource)).label],
|
||||
[true, '重试'])
|
||||
await click('.ycode-account-menu [data-update-action="check"]')
|
||||
await wait(`document.querySelector('.ycode-account-menu [data-update-status]')?.getAttribute('data-update-status')==='checking'`, 'explicit retry checking')
|
||||
setUpdateState({ status: 'error', error: '显式检查失败' }, true)
|
||||
await wait(`!!document.querySelector('.update-sidebar-card[data-status="error"]')`, 'explicit error card')
|
||||
check('explicit retry failure is allowed to surface in the sidebar',
|
||||
[actionCounts.check, await read(`document.querySelector('.update-sidebar-card__description')?.textContent`)],
|
||||
[2, '显式检查失败'])
|
||||
|
||||
await step('unfinished-version')
|
||||
setUpdateState({
|
||||
status: 'up-to-date',
|
||||
version: undefined,
|
||||
progress: undefined,
|
||||
error: undefined,
|
||||
dismissedVersion: undefined,
|
||||
unfinishedVersion: '0.0.6'
|
||||
}, true)
|
||||
await wait(`!!document.querySelector('.update-sidebar-card[data-unfinished="true"]')`, 'unfinished update card')
|
||||
await wait(`document.querySelector('.ycode-account-menu [data-update-status]')?.getAttribute('data-update-status')==='up-to-date'`, 'unfinished account row')
|
||||
snapshots.unfinished = {
|
||||
main: await read(cardSource),
|
||||
account: await read(accountSource),
|
||||
settings: await read(aboutSource, settings)
|
||||
}
|
||||
check('unfinishedVersion survives an otherwise latest state as an actionable warning',
|
||||
[snapshots.unfinished.main.card.title, snapshots.unfinished.main.card.primary.text,
|
||||
snapshots.unfinished.account.label, snapshots.unfinished.account.value,
|
||||
/上次升级至 v0\.0\.6 未完成/.test(snapshots.unfinished.settings.feedback)],
|
||||
['上次升级未完成', '重试更新', '重试更新', '上次升级未完成', true])
|
||||
await click('[data-update-log]')
|
||||
check('unfinished update warning opens the update log', actionCounts.log, 2)
|
||||
|
||||
await step('errors')
|
||||
check('no renderer error logs', rendererErrors, [])
|
||||
|
||||
const report = {
|
||||
result: checks.every(item => item.pass) ? 'PASS' : 'FAIL',
|
||||
scope: 'Real isolated YCode main/preload/renderer and settings window at 1336x902, 940x800 narrow, 900x680 settings; deterministic updates IPC injection; ready/downloading/error/up-to-date/unsupported/unfinished states, retry, dismissal, manual/log links, cross-window synchronization, light/dark screenshots.',
|
||||
limitations: [
|
||||
'This verifies renderer interaction and IPC call shape, not a real package download, signature verification, NSIS install, or restart.',
|
||||
'The harness intercepts updates:install before the production native confirmation dialog, so confirmation copy and dialog behavior remain a main-process acceptance item.',
|
||||
'Static REA evidence does not include final visual measurements for every WorkBuddy update-card state; screenshots are visual review artifacts, not pixel-parity claims.'
|
||||
],
|
||||
injectedUpdateChannels: [
|
||||
'updates:get-state',
|
||||
'updates:check',
|
||||
'updates:download',
|
||||
'updates:install',
|
||||
'updates:dismiss',
|
||||
'updates:open-download-page',
|
||||
'updates:open-log'
|
||||
],
|
||||
absorbedProductionUpdateHandlers: absorbedUpdateHandlers,
|
||||
actionCounts,
|
||||
snapshots,
|
||||
checks,
|
||||
rendererErrors
|
||||
}
|
||||
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2))
|
||||
console.log('UPDATES_UI_' + report.result + ' ' + directory)
|
||||
for (const failure of checks.filter(item => !item.pass)) {
|
||||
console.log('UPDATES_UI_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,
|
||||
actionCounts,
|
||||
checks,
|
||||
snapshots,
|
||||
rendererErrors
|
||||
}, null, 2))
|
||||
console.error(error)
|
||||
app.exit(1)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user