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.
This commit is contained in:
1 parent
0dc088f1f7
commit
b3be45715e
20 files changed
+683
-7
No files matched your search
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".help-feedback-panel__card",
|
||||
".help-feedback-panel__action-button",
|
||||
".help-feedback-panel__footer"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "check-for-updates-without-installing", "selector": ".help-feedback-panel__action-button", "index": 0, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "observe-update-check-result", "duration_ms": 1200, "frames": 4 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-navigation__item--active",
|
||||
".settings-navigation__group--about",
|
||||
".settings-modal__panel",
|
||||
".help-feedback-panel"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-about-by-observed-nav-index-14-in-dark-theme", "selector": ".settings-navigation__item", "index": 14, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "verify-about-dark-page-stable", "duration_ms": 800, "frames": 4 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"include_fonts": true,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-navigation",
|
||||
".settings-navigation__item--active",
|
||||
".settings-navigation__group--about",
|
||||
".settings-modal__panel"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "scroll", "step_id": "reveal-bottom-about-us-navigation-group", "selector": ".settings-navigation", "delta_y": 500, "frames": 1 },
|
||||
{ "kind": "click", "step_id": "open-about-by-screenshot-confirmed-nav-index-14", "selector": ".settings-navigation__item", "index": 14, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "verify-about-page-stable", "duration_ms": 800, "frames": 4 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".appearance-card--light",
|
||||
".appearance-card--dark"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "wait", "step_id": "wait-for-theme-renderer-to-stabilize-after-target-change", "duration_ms": 800, "frames": 4 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-navigation__item--active",
|
||||
".settings-navigation__group--about",
|
||||
".settings-modal__panel",
|
||||
".help-feedback-panel"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-get-help-by-observed-nav-index-15-in-dark-theme", "selector": ".settings-navigation__item", "index": 15, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "verify-get-help-dark-page-stable", "duration_ms": 800, "frames": 4 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-navigation",
|
||||
".settings-navigation__item--active",
|
||||
".settings-navigation__group--about",
|
||||
".settings-modal__panel"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "scroll", "step_id": "reveal-bottom-about-us-navigation-group", "selector": ".settings-navigation", "delta_y": 500, "frames": 1 },
|
||||
{ "kind": "click", "step_id": "open-get-help-by-screenshot-confirmed-nav-index-15", "selector": ".settings-navigation__item", "index": 15, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "verify-get-help-page-stable", "duration_ms": 800, "frames": 4 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-navigation",
|
||||
".settings-navigation__item",
|
||||
".settings-navigation__group--about",
|
||||
".settings-modal__panel"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "scroll", "step_id": "reveal-bottom-about-us-navigation-group", "selector": ".settings-navigation", "delta_y": 500, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "verify-about-and-get-help-labels", "duration_ms": 400, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".settings-navigation__item--active",
|
||||
".appearance-card--light",
|
||||
".appearance-card--dark"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-appearance-by-observed-nav-index-3-in-dark-theme", "selector": ".settings-navigation__item", "index": 3, "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "verify-dark-theme-before-restoring-light", "duration_ms": 500, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".settings-navigation__item--active",
|
||||
".appearance-card--light",
|
||||
".appearance-card--dark"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "open-appearance-by-observed-nav-index-3", "selector": ".settings-navigation__item", "index": 3, "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "confirm-light-appearance-before-dark-sample", "duration_ms": 500, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".appearance-card--light",
|
||||
".appearance-card--dark"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "select-dark-theme-for-about-help-sample", "selector": ".appearance-card--dark", "index": 0, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": false,
|
||||
"track_selectors": [
|
||||
".settings-modal__panel",
|
||||
".appearance-card--light",
|
||||
".appearance-card--dark"
|
||||
],
|
||||
"actions": [
|
||||
{ "kind": "click", "step_id": "restore-light-theme-after-about-help-samples", "selector": ".appearance-card--light", "index": 0, "frames": 1 }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Recover only retained REA CSS; never access the installed application here.
|
||||
// Sources: REA light/dark evidence for WorkBuddy 5.8.3 Settings -> About / Get Help.
|
||||
// Both captures identify HelpFeedbackPanel-CQNpj-cs.css.
|
||||
import fs from 'node:fs/promises'
|
||||
import { createHash } from 'node:crypto'
|
||||
import postcss from 'postcss'
|
||||
|
||||
const sha = value => createHash('sha256').update(value).digest('hex')
|
||||
const evidenceIds = {
|
||||
aboutLight: 'ev_7e063ae669e788666080ec25be3247d2c75504d89d076f9963a737cf69ccf25c',
|
||||
helpLight: 'ev_4d3e8c9e277578ef06a16aa4aad5b4766c6a55744c65beb3e8a14033648476c9',
|
||||
aboutDark: 'ev_ed6dce11ff79cf06e3e01c6c1b5ae3e3cc11eac10b37327ce12c06d5c3447053',
|
||||
helpDark: 'ev_ea353952860b6b59699c26fc72d40740b9a3770fe9a3a6e76515686f4363abe6'
|
||||
}
|
||||
const expectedModuleShas = {
|
||||
light: 'fc5a287a1f5a4a76ede74ba971ff12f340ae2193e1dbef9c8898a9b7c3d894e7',
|
||||
dark: '19ec85a3c3fc11a581bbcdf8182e4d78a32dcf5d71307d3da1bbaf92177af765'
|
||||
}
|
||||
const loadModule = async (evidenceId, theme) => {
|
||||
const evidence = JSON.parse(await fs.readFile(`.tmp/rea-css-evidence/${evidenceId}.json`, 'utf8'))
|
||||
if (evidence.evidence_id !== evidenceId) throw new Error(`REA identity mismatch: ${evidenceId}`)
|
||||
const module = evidence.normalized_result.styles.items.find(item => /HelpFeedbackPanel-CQNpj-cs\.css$/.test(item.file_path))
|
||||
if (!module) throw new Error(`About/help panel module CSS is absent from ${evidenceId}`)
|
||||
const moduleSha = sha(module.source.artifact.text)
|
||||
if (moduleSha !== expectedModuleShas[theme]) throw new Error(`About/help ${theme} module CSS changed in ${evidenceId}`)
|
||||
return { filePath: module.file_path, css: module.source.artifact.text, sha: moduleSha }
|
||||
}
|
||||
|
||||
const aboutLight = await loadModule(evidenceIds.aboutLight, 'light')
|
||||
const helpLight = await loadModule(evidenceIds.helpLight, 'light')
|
||||
const aboutDark = await loadModule(evidenceIds.aboutDark, 'dark')
|
||||
const helpDark = await loadModule(evidenceIds.helpDark, 'dark')
|
||||
if ([helpLight, aboutDark, helpDark].some(item => item.filePath !== aboutLight.filePath)) throw new Error('About/help panel module path changed across captures')
|
||||
|
||||
const tokens = await fs.readFile('.tmp/rea-css-recovered/tokens-Df-i-zsO.css', 'utf8')
|
||||
const tokensSha = sha(tokens)
|
||||
if (tokensSha !== '13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696') throw new Error('Tokens CSS changed')
|
||||
|
||||
const lightTokens = new Map()
|
||||
const darkTokens = new Map()
|
||||
for (const rule of postcss.parse(tokens).nodes) {
|
||||
if (rule.type !== 'rule') continue
|
||||
const selectors = rule.selectors ?? []
|
||||
const dark = selectors.some(selector => selector.includes('[data-theme=dark]'))
|
||||
const light = selectors.some(selector => selector.trim() === ':root')
|
||||
if (!dark && !light) continue
|
||||
for (const node of rule.nodes ?? []) {
|
||||
if (node.type !== 'decl' || !node.prop.startsWith('--')) continue
|
||||
if (dark) darkTokens.set(node.prop, node.value)
|
||||
if (light) lightTokens.set(node.prop, node.value)
|
||||
}
|
||||
}
|
||||
|
||||
const relatedSelector = /\.help-feedback-panel(?:[\w-]*)?(?![\w-])/i
|
||||
const darkScope = /^\s*(?:\[data-theme=["']?dark["']?\]|body\[data-vscode-theme-name=["']IDE Night["']\]|body\.vscode-dark|\.vscode-dark|body\.dark|\.dark)(?=\s|$)/i
|
||||
function project(node, mode = 'all') {
|
||||
if (node.type === 'rule') {
|
||||
const selectors = []
|
||||
for (const selector of node.selectors) {
|
||||
if (!relatedSelector.test(selector)) continue
|
||||
const isDark = darkScope.test(selector)
|
||||
if (mode === 'shared' && isDark) continue
|
||||
if (mode === 'dark' && !isDark) continue
|
||||
const mapped = isDark ? selector.replace(darkScope, '[data-theme=dark]') : selector
|
||||
if (!selectors.includes(mapped)) selectors.push(mapped)
|
||||
}
|
||||
return selectors.length ? node.clone({ selector: selectors.join(',') }) : null
|
||||
}
|
||||
if (node.type === 'atrule' && node.nodes) {
|
||||
const nodes = node.nodes.map(child => project(child, mode)).filter(Boolean)
|
||||
return nodes.length ? node.clone({ nodes }) : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const sharedRoot = css => postcss.root({ nodes: postcss.parse(css).nodes.map(node => project(node, 'shared')).filter(Boolean) })
|
||||
const lightRoot = sharedRoot(aboutLight.css)
|
||||
const darkSharedRoot = sharedRoot(aboutDark.css)
|
||||
const darkSpecificRoot = postcss.root({ nodes: postcss.parse(aboutDark.css).nodes.map(node => project(node, 'dark')).filter(Boolean) })
|
||||
const normalizeRules = root => {
|
||||
const rules = []
|
||||
root.walkRules(rule => {
|
||||
const selectors = rule.selectors.map(selector => selector.replace(/\s+/g, ' ').trim()).join(',')
|
||||
const declarations = (rule.nodes ?? [])
|
||||
.filter(node => node.type === 'decl')
|
||||
.map(node => `${node.prop}:${node.value.replace(/\b0px\b/g, '0').replace(/\s+/g, ' ').trim()}`)
|
||||
.join(';')
|
||||
rules.push(`${selectors}{${declarations}}`)
|
||||
})
|
||||
return rules.join('\n')
|
||||
}
|
||||
if (normalizeRules(lightRoot) !== normalizeRules(darkSharedRoot)) throw new Error('Light/dark about-help module rules differ beyond serialization')
|
||||
const root = postcss.root({ nodes: [...lightRoot.nodes, ...darkSpecificRoot.nodes] })
|
||||
const referenced = new Set()
|
||||
const collect = text => {
|
||||
for (const match of text.matchAll(/var\(\s*(--[\w-]+)/g)) referenced.add(match[1])
|
||||
}
|
||||
collect(root.toString())
|
||||
for (let depth = 0; depth < 8; depth++) {
|
||||
const before = referenced.size
|
||||
for (const name of [...referenced]) {
|
||||
for (const value of [lightTokens.get(name), darkTokens.get(name)]) {
|
||||
if (typeof value === 'string') collect(value)
|
||||
}
|
||||
}
|
||||
if (referenced.size === before) break
|
||||
}
|
||||
|
||||
const missingLight = [...referenced].filter(name => !lightTokens.has(name))
|
||||
if (missingLight.length) throw new Error(`Tokens missing from light root scope: ${missingLight.join(', ')}`)
|
||||
const pick = map => new Map([...referenced].map(name => [name, map.get(name) ?? lightTokens.get(name)]))
|
||||
const scope = (selector, map) => `${selector}{${[...map].map(([property, value]) => `${property}:${value}`).join(';')}}`
|
||||
const scopedTokens = `${scope('.help-feedback-panel', pick(lightTokens))}\n${scope('[data-theme=dark] .help-feedback-panel', pick(darkTokens))}\n`
|
||||
const evidenceList = Object.entries(evidenceIds).map(([name, evidenceId]) => `${name}=${evidenceId}`).join('; ')
|
||||
const css = `/* REA ${evidenceList}; HelpFeedbackPanel-CQNpj-cs.css light-sha256=${aboutLight.sha} dark-sha256=${aboutDark.sha}; tokens sha256=${tokensSha}. */\n${root.toString()}\n${scopedTokens}`
|
||||
await fs.writeFile('src/renderer/src/components/source-about-help-panels-recovered.css', css)
|
||||
|
||||
const rules = []
|
||||
root.walkRules(rule => rules.push(rule.selector))
|
||||
const darkRules = []
|
||||
darkSpecificRoot.walkRules(rule => darkRules.push(rule.selector))
|
||||
console.log(`Recovered ${rules.length} about/help rules (${Buffer.byteLength(css)} bytes); ${darkRules.length} dark-specific module rules; scoped ${pick(lightTokens).size} light / ${pick(darkTokens).size} dark tokens`)
|
||||
console.log(rules.join('\n'))
|
||||
if (rules.length < 15) throw new Error('Unexpectedly few about/help rules')
|
||||
@@ -0,0 +1,215 @@
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
@@ -125,10 +125,10 @@ if (!process.versions.electron) {
|
||||
// 原版 4 组;about 组按组件约定渲染为分隔线而不是标题。
|
||||
check('nav keeps the four recovered groups', [nav.groups.length, nav.dividers], [3, 1])
|
||||
// 通用(settings) 在 TODO 10 接入后同样可用。
|
||||
const expectedDisabled = EXPECTED_TABS.filter(tab => !['settings', 'models', 'appearance', 'keyboardShortcuts', 'personalization'].includes(tab))
|
||||
const expectedDisabled = EXPECTED_TABS.filter(tab => !['settings', 'models', 'appearance', 'keyboardShortcuts', 'personalization', 'helpFeedback', 'getHelp'].includes(tab))
|
||||
check('unwired entries stay disabled', nav.tabs.filter((tab, index) => nav.disabled[index] === 'true'), expectedDisabled)
|
||||
check('unwired entries keep the 暂未接入 hint', nav.titles.filter(title => (title ?? '').includes('暂未接入')).length, expectedDisabled.length)
|
||||
check('settings, appearance, keyboard shortcuts, personalization and models stay enabled', nav.tabs.filter((tab, index) => !nav.disabled[index]), ['settings', 'appearance', 'keyboardShortcuts', 'personalization', 'models'])
|
||||
check('settings, appearance, shortcuts, personalization, models, about and help stay enabled', nav.tabs.filter((tab, index) => !nav.disabled[index]), ['settings', 'appearance', 'keyboardShortcuts', 'personalization', 'models', 'helpFeedback', 'getHelp'])
|
||||
|
||||
await step('settings-search-excludes-removed')
|
||||
await read(`(()=>{const input=document.querySelector('.settings-navigation input[type="search"]');
|
||||
|
||||
Reference in new issue
Block a user