248 lines
18 KiB
JavaScript
248 lines
18 KiB
JavaScript
// 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')
|
||
|
||
const EXTERNAL_ARROW_PATH = 'M9.8242 6.6001L9.8242 5.109Q9.8243 3.305 9.7462 2.7323Q9.7081 2.4537 9.6375 2.2354L0.8485 11.0243L0 10.1758L8.789 1.3868Q8.5706 1.3162 8.292 1.2782Q7.7193 1.2 5.9153 1.2001L4.4243 1.2001L4.4243 0L5.9152 0Q7.8008 -0 8.4543 0.0892Q9.5761 0.2423 10.179 0.8453Q10.782 1.4482 10.9351 2.57Q11.0243 3.2235 11.0243 5.1091L11.0243 6.6001L9.8242 6.6001Z'
|
||
|
||
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 => ({
|
||
id: row.id, rect: rect(row),
|
||
labelTag: row.querySelector('.help-feedback-panel__card-label')?.tagName,
|
||
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'),
|
||
iconWrapperClass: button.querySelector('.wb-button__icon')?.className ?? null,
|
||
iconFill: button.querySelector('.wb-button__icon svg')?.getAttribute('fill') ?? null,
|
||
path: button.querySelector('.wb-button__icon path')?.getAttribute('d') ?? null,
|
||
transform: button.querySelector('.wb-button__icon path')?.getAttribute('transform') ?? null })),
|
||
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')),
|
||
footerDivider: (() => { const node = document.querySelector('.help-feedback-panel__footer-divider')
|
||
return node ? { rect: rect(node), className: node.className, fontSize: getComputedStyle(node).fontSize,
|
||
lineHeight: getComputedStyle(node).lineHeight } : null })(),
|
||
footerLinks: [...document.querySelectorAll('.help-feedback-panel__footer-link')].map(link => ({
|
||
text: link.textContent.trim(), rect: rect(link), disabled: link.disabled, className: link.className,
|
||
color: getComputedStyle(link).color, fontSize: getComputedStyle(link).fontSize,
|
||
lineHeight: getComputedStyle(link).lineHeight })),
|
||
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
|
||
await fs.writeFile(path.join(directory, 'about-light.png'), (await settings.webContents.capturePage()).toPNG())
|
||
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('关于 adds the requested metadata row while keeping 606x32 row geometry',
|
||
[near(light.card.width, 638), near(light.card.height, 160),
|
||
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 keep the recovered ids and span labels',
|
||
light.rows.map(row => [row.id, row.labelTag]),
|
||
[['settings-about-version', 'SPAN'], ['settings-about-product', 'SPAN'], ['settings-about-model-metadata', 'SPAN']])
|
||
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.slice(0, 2).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.slice(0, 2).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('关于 metadata update is enabled in its own row', [light.buttons[2].text, light.buttons[2].ariaDisabled !== 'true', near(light.rows[2].rect.y, 156), near(light.rows[2].rect.height, 32)], ['更新元数据', true, true, true])
|
||
check('关于 external arrow keeps the exact REA wrapper, path and transform',
|
||
[light.buttons[1].iconWrapperClass, light.buttons[1].iconFill, light.buttons[1].path, light.buttons[1].transform],
|
||
['wb-button__icon wb-button__icon--right', 'currentColor', EXTERNAL_ARROW_PATH, 'matrix(1 0 0 1 2.57586 2.39996)'])
|
||
check('关注卡片 keeps 638x226 with the recovered title and three 120x120 placeholder QRs',
|
||
[near(light.follow.y, 216), 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.every(link => link.className.includes('wb-button--link') && link.className.includes('wb-button--small')),
|
||
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, true, 'rgba(0, 0, 0, 0.3)', true, true])
|
||
check('关于 footer divider keeps the recovered class and 3.125x17 glyph box',
|
||
[light.footerDivider?.className, near(light.footerDivider?.rect.width, 3.125, 0.25),
|
||
near(light.footerDivider?.rect.height, 17, 0.75), near(light.footerDivider?.rect.x, 549.438, 0.5)],
|
||
['help-feedback-panel__footer-divider', true, true, true])
|
||
check('关于 footer keeps the recovered 12/20 link labels and 13/normal divider glyph',
|
||
[light.footerLinks.map(link => [link.fontSize, link.lineHeight]), light.footerDivider?.fontSize, light.footerDivider?.lineHeight],
|
||
[[['12px', '20px'], ['12px', '20px']], '13px', 'normal'])
|
||
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('获取帮助 rows keep the recovered ids and span labels',
|
||
help.rows.map(row => [row.id, row.labelTag]),
|
||
[['settings-get-help-feedback', 'SPAN'], ['settings-get-help-docs', 'SPAN']])
|
||
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
|
||
await fs.writeFile(path.join(directory, 'about-dark.png'), (await settings.webContents.capturePage()).toPNG())
|
||
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 the row geometry and requested metadata extension',
|
||
[near(dark.panel.width, 638), near(dark.panel.height, 620), near(dark.card.height, 160),
|
||
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)
|
||
})
|
||
}
|