Files
ycode/scripts/verify-about-help-settings.cjs
T
xiaohan.yuan b3be45715e feat(settings): replicate the WorkBuddy about and help panels without external jumps (TODO 19)
Recover the shared WorkBuddy 5.8.3 HelpFeedbackPanel stylesheet through REA (18 module rules plus scoped light/dark tokens) and rebuild the 关于 and 获取帮助 pages: 638x620 panels, 638x108 two-row cards, 82x32/102x32 actions, the 638x226 follow card with three 120x120 QR frames and the 638x44 footer.

Keep the user's no-jump requirement explicit: every external entry (检查更新, 前往官网, 填写反馈, 查看文档, 隐私政策, 服务协议) keeps the original wording and geometry but is aria-disabled with a 暂未接入/尚未配置 tooltip and performs no navigation; the WorkBuddy account QR codes become same-size 尚未配置 placeholders. The version line reads the real appInfo version.

Also fix a CSS bundling regression found while wiring this page: the shared general-settings stylesheet was inlined three times and overrode the personalization long-card padding, collapsing the 598x160 previews to 614 wide. Remove the duplicate import and raise the personalization padding specificity.

Verify with scripts/verify-about-help-settings.cjs (15 assertions: both pages, geometry, real version, disabled jumpless actions, no anchors, dark surfaces) and keep all existing settings regressions green.
2026-10-17 11:20:01 +08:00

216 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// TODO 19 验收:设置 → 关于 / 获取帮助的 1:1 布局 + 真实版本 + 无 WorkBuddy 跳转 + 深浅主题。
// 真实隔离 YCode(真实 preload/IPC/渲染进程/文件系统)+ 原生设置窗口,视口 900×680(与原版一致)。
//
// 原版依据(REA ev_7e063a…/ev_4d3e8c…/ev_ed6dce…/ev_ea3539…/ev_17cc17…):
// 两页 panel [232,40,638,620];顶部卡片 638×108(行 606×32,spacing 20,padding 12×16);
// 关于=检查更新 82×32 + 前往官网 102×32 + 关注卡 638×226(三列 120×120 二维码框)+ 页脚 44;
// 获取帮助=意见反馈/填写反馈 82×32 + 帮助文档/查看文档 102×32。
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/about-help-settings-'))
const data = path.join(directory, 'data'), home = path.join(directory, 'home')
await fs.mkdir(data, { recursive: true })
await fs.mkdir(home, { recursive: true })
const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: data, YCODE_OMP_HOME: home,
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('ABOUT_HELP_SETTINGS_')) process.stdout.write(bytes) })
child.stderr.on('data', bytes => { output += 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(-4000))
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
app.setPath('home', path.join(directory, 'home'))
const checks = [], snapshots = {}, rendererErrors = []
let main, settings, phase = 'boot'
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)
})])
} 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(40) }
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 })
}
const near = (value, target, tolerance = 0.75) => Math.abs(value - target) <= tolerance
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 openSettings(section) {
await read(`window.ycode.app.openSettings(${JSON.stringify(section)})`)
for (let index = 0; index < 200; index++) {
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 pause(400)
return settings
}
const layoutSource = `(()=>{
const round = value => Math.round(value * 1000) / 1000
const rect = node => { if (!node) return null; const r = node.getBoundingClientRect()
return { x: round(r.x), y: round(r.y), width: round(r.width), height: round(r.height) } }
const panel = document.querySelector('.help-feedback-panel')
const buttons = [...document.querySelectorAll('.help-feedback-panel__action-button')]
return {
panel: rect(panel),
section: panel?.dataset.aboutHelpPanel,
card: rect(document.querySelector('.help-feedback-panel__card')),
rows: [...document.querySelectorAll('.help-feedback-panel__card-row')].map(row => ({
rect: rect(row),
label: row.querySelector('.help-feedback-panel__card-label')?.childNodes[0]?.textContent?.trim(),
value: row.querySelector('.help-feedback-panel__card-value')?.textContent?.trim()
})),
buttons: buttons.map(button => ({ text: button.textContent.trim(), rect: rect(button), disabled: button.disabled,
ariaDisabled: button.getAttribute('aria-disabled'),
title: button.getAttribute('title'), arrow: !!button.querySelector('.wb-button__icon svg') })),
follow: rect(document.querySelector('.help-feedback-panel__follow-card')),
followTitle: (() => { const node = document.querySelector('.help-feedback-panel__follow-title'); return node ? { ...rect(node), text: node.textContent.trim() } : null })(),
qrList: rect(document.querySelector('.help-feedback-panel__qr-list')),
qrItems: [...document.querySelectorAll('.help-feedback-panel__qr-item')].map(item => ({
label: item.querySelector('.help-feedback-panel__qr-label')?.textContent?.trim(),
rect: rect(item.querySelector('.help-feedback-panel__qr-container')),
placeholder: item.querySelector('.help-feedback-panel__qr--placeholder')?.textContent?.trim()
})),
footer: rect(document.querySelector('.help-feedback-panel__footer')),
footerLinks: [...document.querySelectorAll('.help-feedback-panel__footer-link')].map(link => ({
text: link.textContent.trim(), rect: rect(link), disabled: link.disabled, color: getComputedStyle(link).color })),
anchors: [...document.querySelectorAll('a')].map(anchor => anchor.getAttribute('href')).filter(Boolean),
cardBackground: panel ? getComputedStyle(document.querySelector('.help-feedback-panel__card')).backgroundColor : null,
labelColor: (() => { const node = document.querySelector('.help-feedback-panel__card-label'); return node ? getComputedStyle(node).color : null })(),
buttonBackground: buttons[0] ? getComputedStyle(buttons[0]).backgroundColor : null,
buttonBorder: buttons[0] ? getComputedStyle(buttons[0]).borderTopColor : null,
qrBackground: (() => { const node = document.querySelector('.help-feedback-panel__qr-container'); return node ? getComputedStyle(node).backgroundColor : null })(),
theme: document.documentElement.dataset.theme ?? 'light'
}})()`
;(async () => {
await import(pathToFileURL(path.resolve('out/main/index.mjs')).href); await app.whenReady()
for (let index = 0; index < 200; index++) {
main = BrowserWindow.getAllWindows().find(win => win.getTitle() === 'YCode')
if (main && !main.webContents.isLoading()) break
await pause(25)
}
assert.ok(main, 'main window'); observe(main)
await wait('!!window.ycode', 'preload api')
const appInfo = await read(`window.ycode.app.info()`)
snapshots.appInfo = { name: appInfo.name, version: appInfo.version }
await step('about-settings')
await openSettings('helpFeedback'); observe(settings)
await wait('!!document.querySelector(".help-feedback-panel")', 'about panel', settings)
const nav = await read(`(()=>{const item=document.querySelector('[data-settings-tab="helpFeedback"]')
return { exists: !!item, disabled: item?.getAttribute('aria-disabled'), active: document.querySelector('.settings-navigation__item--active')?.dataset.settingsTab }})()`, settings)
check('关于 nav entry is enabled and active', [nav.exists, nav.disabled !== 'true', nav.active], [true, true, 'helpFeedback'])
const light = await read(layoutSource, settings)
snapshots.aboutLight = light
check('关于 panel keeps the recovered 638x620 box at 232,40',
[near(light.panel.x, 232), near(light.panel.y, 40), near(light.panel.width, 638), near(light.panel.height, 620)], [true, true, true, true])
check('关于 top card keeps 638x108 with two 606x32 rows',
[near(light.card.width, 638), near(light.card.height, 108),
near(light.rows[0].rect.y, 52), near(light.rows[0].rect.width, 606), near(light.rows[0].rect.height, 32),
near(light.rows[1].rect.y, 104)], [true, true, true, true, true, true])
check('关于 rows show the real YCode version and the recovered labels',
[light.rows[0].label, light.rows[0].value, light.rows[1].label],
['当前版本', `v${appInfo.version}`, '关于 YCode'])
check('关于 action buttons keep 82x32 / 102x32 and stay explicitly disabled',
[light.buttons.map(button => button.text), near(light.buttons[0].rect.x, 772), near(light.buttons[0].rect.width, 82),
near(light.buttons[1].rect.x, 752), near(light.buttons[1].rect.width, 102),
light.buttons.every(button => button.ariaDisabled === 'true' && near(button.rect.height, 32)),
light.buttons[0].title, light.buttons[1].title, light.buttons[1].arrow],
[['检查更新', '前往官网'], true, true, true, true, true, '暂未接入', '尚未配置', true])
check('关注卡片 keeps 638x226 with the recovered title and three 120x120 placeholder QRs',
[near(light.follow.y, 164), near(light.follow.width, 638), near(light.follow.height, 226),
light.followTitle.text, light.qrList.width, light.qrItems.map(item => item.label),
light.qrItems.every(item => near(item.rect.width, 120) && near(item.rect.height, 120) && item.placeholder === '尚未配置')],
[true, true, true, '关注我们,获取实用技巧与产品最新动态', 408, ['公众号', '视频号', '腾讯频道'], true])
check('关于 footer keeps the recovered 44px row with two 12px disabled links',
[near(light.footer.y, 616), near(light.footer.width, 638), near(light.footer.height, 44),
light.footerLinks.map(link => link.text), light.footerLinks.every(link => link.disabled && near(link.rect.height, 20)),
light.footerLinks[0].color, near(light.footerLinks[0].rect.x, 489.438, 0.5), near(light.footerLinks[1].rect.x, 564.563, 0.5)],
[true, true, true, ['隐私政策', '服务协议'], true, 'rgba(0, 0, 0, 0.3)', true, true])
check('关于 page contains no anchor links (no WorkBuddy jumps)', light.anchors, [])
await step('help-settings')
await openSettings('getHelp')
await wait(`document.querySelector('.help-feedback-panel')?.dataset.aboutHelpPanel==='getHelp'`, 'help panel', settings)
const helpNav = await read(`(()=>{const item=document.querySelector('[data-settings-tab="getHelp"]')
return { disabled: item?.getAttribute('aria-disabled'), active: document.querySelector('.settings-navigation__item--active')?.dataset.settingsTab }})()`, settings)
check('获取帮助 nav entry is enabled and active', [helpNav.disabled !== 'true', helpNav.active], [true, 'getHelp'])
const help = await read(layoutSource, settings)
snapshots.helpLight = help
check('获取帮助 keeps the recovered 638x620 panel and 638x108 card',
[near(help.panel.width, 638), near(help.panel.height, 620), near(help.card.width, 638), near(help.card.height, 108)], [true, true, true, true])
check('获取帮助 rows and actions keep the recovered labels and geometry',
[help.rows.map(row => row.label), help.buttons.map(button => button.text),
near(help.buttons[0].rect.x, 772), near(help.buttons[0].rect.width, 82),
near(help.buttons[1].rect.x, 752), near(help.buttons[1].rect.width, 102),
help.buttons.every(button => button.ariaDisabled === 'true' && button.title === '尚未配置'), help.buttons[1].arrow],
[['意见反馈', '帮助文档'], ['填写反馈', '查看文档'], true, true, true, true, true, true])
check('获取帮助 has no QR block, footer or anchor links',
[help.follow, help.footer, help.anchors], [null, null, []])
await step('about-help-dark')
await openSettings('appearance')
await wait('!!document.querySelector(".appearance-card")', 'appearance panel', settings)
await read(`document.querySelector('[aria-label="深色"].appearance-card').click()`, settings)
await wait(`document.documentElement.dataset.theme==='dark'`, 'dark theme', settings)
await openSettings('helpFeedback')
await wait(`document.documentElement.dataset.theme==='dark'&&!!document.querySelector('.help-feedback-panel')`, 'dark about panel', settings)
const dark = await read(layoutSource, settings)
snapshots.aboutDark = dark
check('dark theme keeps the recovered card, text, button and QR surfaces',
[dark.cardBackground, dark.labelColor, dark.buttonBackground, dark.buttonBorder, dark.qrBackground],
['rgb(31, 31, 31)', 'rgb(242, 243, 245)', 'rgb(38, 38, 38)', 'rgb(61, 61, 61)', 'rgb(41, 41, 41)'])
check('dark theme keeps every recovered geometry',
[near(dark.panel.width, 638), near(dark.panel.height, 620), near(dark.card.height, 108),
near(dark.follow.height, 226), dark.qrItems.every(item => near(item.rect.width, 120))], [true, true, true, true, true])
await openSettings('appearance')
await wait('!!document.querySelector(".appearance-card")', 'appearance panel again', settings)
await read(`document.querySelector('[aria-label="浅色"].appearance-card').click()`, settings)
await wait(`document.documentElement.dataset.theme==='light'`, 'light restored', settings)
await step('about-help-errors')
check('no renderer error logs', rendererErrors, [])
const result = checks.every(item => item.pass) ? 'PASS' : 'FAIL'
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result, phase, checks, snapshots }, null, 2))
console.log('ABOUT_HELP_SETTINGS_' + result + ' ' + directory)
for (const failure of checks.filter(item => !item.pass)) console.log('ABOUT_HELP_SETTINGS_FAILURE ' + JSON.stringify(failure))
app.exit(result === 'PASS' ? 0 : 1)
})().catch(async error => {
console.error(error)
try { await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify({ result: 'ERROR', phase, checks, snapshots, error: String(error?.stack ?? error) }, null, 2)) } catch { /* ignore */ }
app.exit(1)
})
}