fix(B02): show installed recommendation actions and remove duplicate navigation

This commit is contained in:
Codex committed 2026-10-21 13:56:37 +09:00
1 parent 9f1e583e23
commit 14a1354c95
7 files changed
+196 -25

No files matched your search

+44 -10
View File
@@ -17,7 +17,7 @@ if (!electron) {
const env = { ...process.env }
delete env.ELECTRON_RUN_AS_NODE
delete env.ELECTRON_RENDERER_URL
const child = spawn(electronModule, ['--no-sandbox', '--disable-gpu-sandbox', path.resolve('scripts/test-recommended-skills-ui.mjs'), output], { stdio: 'inherit', windowsHide: true, env })
const child = spawn(electronModule, [path.resolve('scripts/test-recommended-skills-ui.mjs'), output], { stdio: 'inherit', windowsHide: true, env })
const timer = setTimeout(() => child.kill(), 120000)
process.exitCode = await new Promise(resolve => child.on('exit', code => { clearTimeout(timer); resolve(code ?? 1) }))
} else {
@@ -28,7 +28,7 @@ if (!electron) {
const userData = path.join(output, 'data')
fs.mkdirSync(userData, { recursive: true })
app.setPath('userData', userData)
const checks = [], errors = [], calls = [], screenshots = []
const checks = [], errors = [], calls = [], screenshots = [], actionStyles = []
const recommendation = {
id: 'archify',
@@ -197,6 +197,29 @@ createRoot(document.getElementById('root')).render(
screenshots.push(name + '.png')
}
const verifyCurrentActionStyles = async theme => {
const selector = '[data-recommendation-id=archify] .sm-use-btn'
const inspect = () => read(`(()=>{const b=document.querySelector(${JSON.stringify(selector)});const s=getComputedStyle(b);return{width:s.width,height:s.height,background:s.backgroundColor,color:s.color,outline:s.outline,focused:document.activeElement===b,hover:b.matches(':hover')}})()`)
win.webContents.sendInputEvent({type:'mouseMove',x:1,y:1})
await read(`document.activeElement?.blur()`); await pause(200)
const normal = await inspect()
check(theme + ' use action default background is transparent', normal.background, value => value === 'rgba(0, 0, 0, 0)' || value === 'transparent')
const point = await read(`(()=>{const r=document.querySelector(${JSON.stringify(selector)}).getBoundingClientRect();return{x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2)}})()`)
win.webContents.sendInputEvent({type:'mouseMove',...point});await pause(200)
const hover = await inspect()
check(theme + ' pointer enters use action', hover.hover, true)
check(theme + ' hover has a distinct background', hover.background !== normal.background, true)
await capture('installed-use-' + theme + '-hover')
win.webContents.sendInputEvent({type:'mouseMove',x:1,y:1})
win.webContents.sendInputEvent({type:'keyDown',keyCode:'Tab'});win.webContents.sendInputEvent({type:'keyUp',keyCode:'Tab'})
await read(`document.querySelector(${JSON.stringify(selector)}).focus()`);await pause(200)
const focus = await inspect()
check(theme + ' use action remains focusable with stable 24px slot', focus.focused && focus.width === '24px' && focus.height === '24px', true)
await capture('installed-use-' + theme + '-focus')
actionStyles.push({theme,normal,hover,focus})
await read(`document.activeElement?.blur()`)
}
try {
await win.loadFile(path.join(output, 'index.html'))
await wait('!!document.querySelector(".yc-skills-page")')
@@ -229,7 +252,11 @@ createRoot(document.getElementById('root')).render(
check('a first install is not an overwrite', await read(`(${JSON.stringify(calls)}.filter(c=>c[0]==='installRecommended').every(c=>c[2]?.overwrite!==true))`), true)
await wait('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState==="current"')
check('the card reports the recommendation as installed', await read('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState'), 'current')
check('the installed skill is not offered for download again', await read('document.querySelector("[data-recommendation-id=archify] .sm-add-btn")?.disabled'), true)
check('the installed skill has no add button', await read('!!document.querySelector("[data-recommendation-id=archify] .sm-add-btn")'), false)
check('the installed skill uses the recovered installed card', await read('!!document.querySelector("[data-recommendation-id=archify] .sm-card--installed")'), true)
check('the installed action uses the original action slot', await read('!!document.querySelector("[data-recommendation-id=archify] .sm-status .sm-use-btn")'), true)
check('no duplicate under-card use action remains', await read('!!document.querySelector(".yc-skill-recommendation-meta button")'), false)
check('the duplicate footer is absent', await read('!!document.querySelector(".yc-skill-recommendation-footer")'), false)
check('the detail remains the same DOM instance throughout installation', await read('window.__detailBeforeInstall===document.querySelector("[role=dialog]")'), true)
check('the detail offers use after installation', await read('document.querySelector("[role=dialog] .sm-skill-detail-action-row")?.textContent.includes("去使用")'), true)
check('the success notification also offers use', await read('document.querySelector(".yc-skill-install-success")?.textContent.includes("去使用")'), true)
@@ -237,8 +264,13 @@ createRoot(document.getElementById('root')).render(
await capture('installed-detail')
await click('[role=dialog] .sm-skill-detail-close')
const installedActionStyle = await read(`(()=>{const b=document.querySelector('[data-recommendation-id=archify] .sm-use-btn');const s=getComputedStyle(b);const svg=b.querySelector('svg');return {width:s.width,height:s.height,svgWidth:getComputedStyle(svg).width,svgHeight:getComputedStyle(svg).height}})()`)
check('installed action uses recovered 24px slot and 16px icon', installedActionStyle, {width:'24px',height:'24px',svgWidth:'16px',svgHeight:'16px'})
await capture('installed-recommendation-light')
await verifyCurrentActionStyles('light')
// The installed list stays reachable and the skill can be used.
await click('.yc-skill-recommendation-footer button')
await click('[aria-label="已安装技能"]')
await wait('document.querySelectorAll(".sm-card").length===1')
check('the installed skill appears in the installed list', await read('document.querySelector(".sm-card-name")?.textContent'), 'Archify')
await click('[aria-label="返回专家中心"]')
@@ -247,7 +279,7 @@ createRoot(document.getElementById('root')).render(
// A local skill with the same name requires an explicit overwrite.
await read(`window.ycode.skills.qaSetInstalled(${JSON.stringify([localSkill])})`)
await wait('document.querySelector("[data-recommendation-id=archify]").dataset.recommendationState==="conflict"')
await click('[aria-label="安装 Archify"]')
await click('[aria-label="替换 Archify"]')
await wait('!!document.querySelector(".yc-skill-detail-shell")')
check('a same-name local skill requires an explicit replacement', await read('document.querySelector(".yc-skill-detail-shell").textContent.includes("替换")'), true)
check('the replacement notice mentions the local modification', await read('document.querySelector(".yc-skill-detail-shell").textContent.includes("本地内容已被修改")'), true)
@@ -295,8 +327,8 @@ createRoot(document.getElementById('root')).render(
await click('.yc-skill-install-success [aria-label="使用推荐技能 Archify"]')
check('the success notification invokes the same use callback', await read('window.__qa.uses.length'), 1)
await read(`window.ycode.skills.qaSetInstalled(${JSON.stringify([{ ...installedRecommendation, enabled: false }])})`)
await wait('document.querySelector(".yc-skill-recommendation-meta")?.textContent.includes("启用后使用")')
await click('.yc-skill-recommendation-meta [aria-label="使用推荐技能 Archify"]')
await wait('document.querySelector("[data-recommendation-id=archify] .sm-use-btn")?.title.includes("启用")')
await click('[data-recommendation-id=archify] .sm-use-btn')
await wait('window.__qa.uses.length===2')
check('disabled recommendation is enabled before use', await read('window.__qa.uses.at(-1).enabled'), true)
await read('window.ycode.skills.qaDelay(0)'); await read('window.ycode.skills.qaSetInstalled([])')
@@ -308,9 +340,10 @@ createRoot(document.getElementById('root')).render(
await read(`location.reload()`)
await wait('!!document.querySelector(".yc-skill-recommendation-unavailable")')
check('an empty catalog is reported as unavailable, not as a maintenance page', await read('!!document.querySelector("[data-capability-maintenance]")'), false)
check('the unavailable state still offers the installed list', await read('!!document.querySelector(".yc-skill-recommendation-unavailable button")'), true)
check('the unavailable state keeps top installed navigation', await read('!!document.querySelector("[aria-label=已安装技能]")'), true)
check('the unavailable state has no duplicate installed entry', await read('[...document.querySelectorAll(".yc-skill-recommendation-unavailable button")].some(e=>e.textContent.includes("查看已安装技能"))'), false)
await read(`window.ycode.skills.qaFailRecommendations(false)`)
await read(`location.reload()`)
await click('.yc-skill-recommendation-unavailable button')
await wait('!!document.querySelector("[data-recommendation-id=archify]")')
// An install owned by another window blocks a new install here.
@@ -354,6 +387,7 @@ createRoot(document.getElementById('root')).render(
check('the dark layout keeps the recommendation card inside the viewport', await read(`(()=>{const c=document.querySelector('[data-recommendation-id=archify] .sm-card');if(!c)return false;const r=c.getBoundingClientRect();return r.left>=0&&r.right<=innerWidth+1})()`), true)
check('the dark layout does not overflow the document', overflow.scrollWidth <= overflow.innerWidth + 1, true)
await capture('recommendation-dark')
await verifyCurrentActionStyles('dark')
win.setSize(720, 900)
await pause(400)
check('the recommendation card stays inside the narrow viewport', await read('(()=>{const c=document.querySelector("[data-recommendation-id=archify] .sm-card");if(!c)return false;const r=c.getBoundingClientRect();return r.left>=0&&r.right<=innerWidth+1})()'), true)
@@ -365,7 +399,7 @@ createRoot(document.getElementById('root')).render(
check('no renderer errors', errors, [])
fs.writeFileSync(path.join(output, 'report.json'), JSON.stringify({
result: 'PASS', checks, calls, screenshots,
result: 'PASS', checks, calls, screenshots, actionStyles,
limits: 'Controlled host in isolated Electron; the shipped page and styles are used. No network, no real skill storage, no real install.'
}, null, 2))
console.log(JSON.stringify({ result: 'PASS', checks: checks.length, output }))