Files
ycode/scripts/verify-pierre-diff.cjs

163 lines
12 KiB
JavaScript
Raw Permalink 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.
// Own offline YCode UI only. Original runtime evidence is collected separately through REA.
if (!process.versions.electron) {
const env = { ...process.env }; delete env.ELECTRON_RUN_AS_NODE
const { spawn, spawnSync } = require('node:child_process')
const server = spawn(process.execPath, [require('node:path').join(__dirname, 'verify-ui.mjs'), '0'], {
env, windowsHide: true, stdio: ['ignore', 'pipe', 'inherit']
})
let started = false
const timer = setTimeout(() => { if (!started) { server.kill(); process.exitCode = 1 } }, 10000)
server.stdout.on('data', bytes => {
const port = bytes.toString().match(/127\.0\.0\.1:(\d+)/)?.[1]
if (!port || started) return
started = true; clearTimeout(timer)
try {
const result = spawnSync(require('electron'), [__filename], {
env: { ...env, YCODE_DIFF_QA_PORT: port }, windowsHide: true, stdio: 'inherit', timeout: 60000
})
process.exitCode = result.status ?? 1
} finally { server.kill() }
})
server.on('error', error => { clearTimeout(timer); console.error(error); process.exitCode = 1 })
} else {
const { app, BrowserWindow } = require('electron')
const fs = require('node:fs/promises'), path = require('node:path'), assert = require('node:assert/strict')
app.setPath('userData', path.resolve('.tmp/pierre-diff-qa-user-data'))
app.disableHardwareAcceleration()
let win
const errors = []
const read = code => win.webContents.executeJavaScript(code)
async function waitFor(code) {
for (let n = 0; n < 200; n++) {
if (await read(code)) return
await new Promise(resolve => setTimeout(resolve, 40))
}
throw new Error('Condition failed: ' + code)
}
async function open(name, fileName, expectBody = true) {
await read(`Array.from(document.querySelectorAll('.msg__artifact')).find(n=>n.textContent===${JSON.stringify(name)}).click()`)
await waitFor(`document.querySelector('diffs-container')?.shadowRoot?.querySelector('[data-title]')?.textContent.includes(${JSON.stringify(fileName)})${expectBody ? " && !!document.querySelector('diffs-container')?.shadowRoot?.querySelector('pre')" : ''}`)
}
const diffState = `(()=>{const c=document.querySelector('diffs-container'),root=c.shadowRoot,pre=root.querySelector('pre'),editor=document.querySelector('[class*="_pierre-diff-editor_"]');return {
mode:pre?.getAttribute('data-diff-type')??null,text:pre?.textContent??'',changeType:root.querySelector('[data-change-type]')?.getAttribute('data-change-type'),
additions:root.querySelector('[data-additions-count]')?.textContent,deletions:root.querySelector('[data-deletions-count]')?.textContent,
codeColumns:root.querySelectorAll('code[data-code]').length,lineNumbers:root.querySelectorAll('[data-line-number-content]').length,
wordSpans:root.querySelectorAll('[data-diff-span]').length,fontSize:pre?getComputedStyle(pre).fontSize:null,lineHeight:pre?getComputedStyle(pre).lineHeight:null,
tokenColors:[...new Set(Array.from(root.querySelectorAll('[data-line] span')).map(n=>getComputedStyle(n).color))],
editorBackground:getComputedStyle(editor).backgroundColor,theme:root.querySelector('[data-theme-css]')?.textContent,
lineTypes:Array.from(root.querySelectorAll('[data-content] [data-line-type]')).map(n=>n.getAttribute('data-line-type'))}})()`
async function shot(file) {
await read('document.fonts.ready')
await new Promise(resolve => { win.webContents.once('paint', resolve); win.webContents.invalidate() })
await fs.writeFile(path.resolve('.tmp/' + file), (await win.webContents.capturePage()).toPNG())
}
async function theme(value) {
await read("document.querySelector('button[title=设置]').click()")
await read("Array.from(document.querySelectorAll('.settings__nav-item')).find(n=>n.textContent==='外观').click()")
await read(`Array.from(document.querySelectorAll('.segmented__item')).find(n=>n.textContent.trim()===${JSON.stringify(value === 'dark' ? '深色' : '浅色')}).click()`)
await waitFor(`document.documentElement.dataset.theme===${JSON.stringify(value)}`)
await read("document.querySelector('.overlay').click()")
}
app.whenReady().then(async () => {
win = new BrowserWindow({ show: false, useContentSize: true, width: 1336, height: 901,
webPreferences: { offscreen: true, contextIsolation: true } })
win.webContents.on('console-message', event => { if (event.level === 'error' || event.level === 3) errors.push(event.message) })
await win.loadURL(`http://127.0.0.1:${process.env.YCODE_DIFF_QA_PORT || '8792'}/?fixture=pierre-diff`)
await waitFor("document.querySelectorAll('.msg__artifact').length===4")
await open('修改 Diff', 'sample.ts')
const initial = await read(`(()=>{const c=document.querySelector('diffs-container');return {html:c.outerHTML,shadow:c.shadowRoot.innerHTML,toolbar:Array.from(document.querySelectorAll('[class*="pierre-diff-editor"] button')).map(b=>({title:b.title,label:b.getAttribute('aria-label'),html:b.outerHTML}))}})()`)
await fs.writeFile(path.resolve('.tmp/pierre-diff-initial-dom.json'), JSON.stringify(initial, null, 2))
assert.match(initial.shadow, /export const value/)
await waitFor("new Set(Array.from(document.querySelector('diffs-container').shadowRoot.querySelectorAll('[data-line] span')).map(n=>getComputedStyle(n).color)).size > 1")
const split = await read(diffState)
assert.equal(split.mode, 'split')
assert.equal(split.codeColumns, 2)
assert.ok(split.lineNumbers >= 2)
assert.ok(split.wordSpans >= 2)
assert.equal(split.additions, '+1')
assert.equal(split.deletions, '-1')
assert.equal(split.fontSize, '13px')
assert.equal(split.lineHeight, '20px')
assert.equal(split.editorBackground, 'rgb(255, 255, 255)')
const chatMarkdown = await read("(()=>{const s=getComputedStyle(document.querySelector('.msg--assistant .markdown-content'));return {padding:s.padding,background:s.backgroundColor}})()")
assert.deepEqual(chatMarkdown, { padding: '0px', background: 'rgba(0, 0, 0, 0)' })
await shot('conversation-pierre-diff-2026-10-15.png')
await read("document.querySelector('button[aria-label=\"Switch to unified view\"]').click()")
await waitFor("document.querySelector('diffs-container').shadowRoot.querySelector('pre').getAttribute('data-diff-type')==='single'")
const unified = await read(diffState)
assert.equal(unified.codeColumns, 1)
assert.match(unified.text, /export const value = 1/)
assert.match(unified.text, /export const value = 2/)
await read("document.querySelector('button[aria-label=\"Switch to split view\"]').click()")
await waitFor("document.querySelector('diffs-container').shadowRoot.querySelector('pre').getAttribute('data-diff-type')==='split'")
await theme('dark')
const dark = await read(diffState)
assert.equal(dark.editorBackground, 'rgb(7, 7, 7)')
assert.match(dark.theme, /color-scheme: dark/)
await shot('conversation-pierre-diff-dark-2026-10-15.png')
await theme('light')
await open('新增 Diff', 'new.txt')
const added = await read(diffState)
assert.match(added.text, /created/)
assert.ok(added.lineTypes.includes('change-addition'))
assert.ok(!added.lineTypes.includes('change-deletion'))
await open('删除 Diff', 'empty.txt')
const deleted = await read(diffState)
assert.match(deleted.text, /removed/)
assert.ok(deleted.lineTypes.includes('change-deletion'))
assert.ok(!deleted.lineTypes.includes('change-addition'))
await open('相同 Diff', 'same.txt', false)
const unchanged = await read(diffState)
assert.equal(unchanged.codeColumns, 0, 'Source zero-hunk diff renders its header with no code body')
assert.equal(unchanged.lineNumbers, 0)
assert.equal(unchanged.wordSpans, 0)
await win.loadURL(`http://127.0.0.1:${process.env.YCODE_DIFF_QA_PORT}/?fixture=artifact-source`)
await waitFor("!!document.querySelector('[data-tool-call-id=\"source-edit\"]')")
await read("document.querySelectorAll('.cr-collapse--completed > .cr-collapse__header, .cr-collapse--process > .cr-collapse__header').forEach(n=>{if(n.getAttribute('aria-expanded')==='false')n.click()})")
await read("document.querySelector('[data-tool-call-id=\"source-edit\"] .cr-tool-head__main').click()")
await waitFor("document.querySelector('[data-tool-call-id=\"source-edit\"] .cr-tool-exp__content-shell').getAttribute('aria-hidden')==='false'")
const write = await read(`(()=>{const root=document.querySelector('[data-tool-call-id="source-edit"]'),viewport=root.querySelector('.cr-tool-diff__viewport'),line=root.querySelector('.cr-tool-diff__line');return {text:root.textContent,hasAlert:!!root.querySelector('[role=alert]'),role:root.querySelector('.cr-tool-head__primary').getAttribute('role'),lineHeight:getComputedStyle(line).lineHeight,viewportHeight:viewport.getBoundingClientRect().height,nativeExpandMotion:CSS.supports('interpolate-size','allow-keywords')}})()`)
assert.equal(write.hasAlert, false)
assert.equal(write.role, 'link')
assert.match(write.text, /export const value = 1/)
assert.match(write.text, /export const value = 2/)
assert.equal(write.lineHeight, '21px', 'Source write diff uses 12px × 1.75; 20px is only its virtualizer estimate')
assert.ok(write.viewportHeight > 0)
assert.equal(write.nativeExpandMotion, true)
await read("document.querySelector('[data-tool-call-id=\"source-edit\"] .cr-tool-head__primary').focus()")
win.webContents.sendInputEvent({type:'keyDown',keyCode:'Return'})
win.webContents.sendInputEvent({type:'char',keyCode:'\r'})
win.webContents.sendInputEvent({type:'keyUp',keyCode:'Return'})
await waitFor("document.querySelector('diffs-container')?.shadowRoot?.querySelector('[data-title]')?.textContent.includes('example.ts')")
const fromWrite = await read(diffState)
assert.match(fromWrite.text, /export const value = 1/)
assert.match(fromWrite.text, /export const value = 2/)
assert.equal(await read("document.querySelector('[data-tool-call-id=\"source-edit\"] .cr-tool-exp__content-shell').getAttribute('aria-hidden')"), 'false')
await shot('conversation-write-file-preview-2026-10-15.png')
await read("document.querySelector('[data-tool-call-id=\"source-many\"] .cr-tool-head__main').click()")
await waitFor("document.querySelector('[data-tool-call-id=\"source-many\"] .cr-tool-diff__viewport').clientHeight>0")
const virtualRows = await read(`(()=>{const r=document.querySelector('[data-tool-call-id="source-many"]');return {initialRows:r.querySelectorAll('.cr-tool-diff__virtual-line').length,spacerHeight:r.querySelector('.cr-tool-diff__spacer').getBoundingClientRect().height,transition:getComputedStyle(r.querySelector('.cr-tool-exp__content-shell')).transitionDuration}})()`)
assert.ok(virtualRows.initialRows > 0 && virtualRows.initialRows < 300)
assert.ok(virtualRows.spacerHeight >= 6000)
assert.equal(virtualRows.transition, '0s')
await read("(()=>{const e=document.querySelector('[data-tool-call-id=\"source-many\"] .cr-tool-diff__viewport');e.scrollTop=e.scrollHeight;e.dispatchEvent(new Event('scroll'))})()")
await waitFor("document.querySelector('[data-tool-call-id=\"source-many\"] .cr-tool-diff__spacer').textContent.includes('value299')")
virtualRows.finalRows = await read("document.querySelectorAll('[data-tool-call-id=\"source-many\"] .cr-tool-diff__virtual-line').length")
assert.ok(virtualRows.finalRows > 0 && virtualRows.finalRows < 300)
assert.deepEqual(errors, [])
const report = { scope: 'YCode fixture only; original pixel parity unverified', split, unified, dark, added, deleted, unchanged, chatMarkdown, write, fromWrite, virtualRows, consoleErrors: errors }
await fs.writeFile(path.resolve('.tmp/pierre-diff-ui-report.json'), JSON.stringify(report, null, 2))
console.log(JSON.stringify({ scope: report.scope, splitUnified: true, syntaxHighlight: true, darkLight: true, addedDeletedUnchanged: true, chatMarkdownUnaffected: true, writeInlineDiffAndKeyboardOpen: true, consoleErrors: errors }))
win.destroy(); app.quit()
}).catch(async error => {
console.error(error)
console.error(JSON.stringify(errors))
if (win && !win.isDestroyed()) {
console.error(await read('document.body.innerText'))
await fs.writeFile(path.resolve('.tmp/pierre-diff-failure-dom.json'), JSON.stringify(await read(`(()=>{const root=document.querySelector('.resource-preview__body');return {html:root?.outerHTML,nodes:root?Array.from(root.querySelectorAll('*')).slice(0,40).map(n=>({tag:n.tagName,cl:n.className,bounds:n.getBoundingClientRect().toJSON(),height:getComputedStyle(n).height,display:getComputedStyle(n).display})):[]}})()`), null, 2))
win.destroy()
}
app.exit(1)
})
}