feat(conversation): replicate table quote-to-chat action (TODO 02)
This commit is contained in:
1 parent
e335588826
commit
ed31a7c1b4
20 files changed
+939
-46
No files matched your search
@@ -115,8 +115,13 @@ assert.deepEqual(oversizedAttachmentNames([
|
||||
{ name: 'over.bin', size: MAX_ATTACHMENT_BYTES + 1 }
|
||||
]), ['over.bin'])
|
||||
assert.deepEqual(buildComposerSendPayload(' prompt ', [], ['C:/work/file.ts'], 'full-access'), {
|
||||
text: 'prompt', attachments: [], resources: ['C:/work/file.ts'], mode: 'full-access'
|
||||
text: 'prompt', attachments: [], resources: ['C:/work/file.ts'], quotes: [], mode: 'full-access'
|
||||
})
|
||||
const quotedPayload = buildComposerSendPayload('', [], [], 'full-access', [
|
||||
{ id: 'm1-text-0:table:table-0', kind: 'table', sourceId: 'm1-text-0:table:table-0', messageId: 'm1', markdown: '| a |\n| - |\n| 1 |', preview: '| a |' }
|
||||
])
|
||||
assert.deepEqual(quotedPayload.quotes.map((quote) => quote.sourceId), ['m1-text-0:table:table-0'])
|
||||
assert.equal(quotedPayload.text, '', '引用单独存在时正文可以为空')
|
||||
assert.equal(parseResourcePath('file:///C:/work/file.ts'), 'C:\\work\\file.ts')
|
||||
assert.equal(parseResourcePath('C:\\work\\file.ts'), 'C:\\work\\file.ts')
|
||||
assert.deepEqual(readReferenceUris({ getData: (type) => type === 'application/vnd.code.uri-list' ? 'file:///C:/a.ts\r\nfile:///C:/b.ts' : '' }), ['file:///C:/a.ts', 'file:///C:/b.ts'])
|
||||
|
||||
@@ -31,9 +31,11 @@ async function main(): Promise<void> {
|
||||
assert.doesNotMatch(render('before<script>window.bad = true</script>after'), /<script|window\.bad/)
|
||||
assert.match(render('Waiting $x +', true), /markdown-content__loading-char/)
|
||||
const table = '| a | b |\n| - | - |\n| 1 | 2 |'
|
||||
const tableWithAction = (streaming: boolean): string => renderToStaticMarkup(<MarkdownContent content={table} streaming={streaming} onQuote={() => undefined} />)
|
||||
assert.match(tableWithAction(true), /class="markdown-content__table-quote" disabled=""/)
|
||||
assert.doesNotMatch(tableWithAction(false), /class="markdown-content__table-quote" disabled/)
|
||||
const tableWithAction = (streaming: boolean): string => renderToStaticMarkup(<MarkdownContent content={table} messageId="fixture-message" sourceId="fixture-message-text-0" streaming={streaming} onQuote={() => undefined} />)
|
||||
assert.match(tableWithAction(true), /class="cr-message-quote-table-action" disabled=""[^>]*aria-label="添加到对话"/)
|
||||
assert.match(tableWithAction(false), /class="cr-message-quote-table-action"[^>]*aria-label="添加到对话">/)
|
||||
assert.doesNotMatch(tableWithAction(false), /class="cr-message-quote-table-action" disabled/)
|
||||
assert.match(tableWithAction(false), /data-message-quote-source-id="fixture-message-text-0:table:table-0"/)
|
||||
const opacity = render('abcdef', true)
|
||||
assert.match(opacity, /style="opacity:0\.6/)
|
||||
assert.match(opacity, /style="opacity:0\.1/)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"include_fonts": false,
|
||||
"include_message_content": true,
|
||||
"track_selectors": [".cr-table-block", ".cr-message-quote-table-menu", ".cr-message-quote-table-action", ".cr-input-banner-rail", ".cr-input-quote-card"],
|
||||
"actions": [
|
||||
{ "kind": "hover", "step_id": "hover-table", "selector": ".cr-table-block", "index": 0, "frames": 1 },
|
||||
{ "kind": "wait", "step_id": "menu-visible", "duration_ms": 250, "frames": 1 },
|
||||
{ "kind": "click", "step_id": "add-table-to-conversation", "selector": ".cr-message-quote-table-action", "index": 0, "frames": 0 },
|
||||
{ "kind": "wait", "step_id": "quote-card-rendered", "duration_ms": 600, "frames": 2 }
|
||||
]
|
||||
}
|
||||
@@ -13,7 +13,7 @@ if(!process.versions.electron){
|
||||
const contentNodes=sourceLayout.message_content.nodes
|
||||
const paragraph=contentNodes.filter(node=>node.node_index>=1622&&node.node_index<1630&&node.node_name==='#text').map(node=>node.text).join('')
|
||||
const sourceMarkdown='✅ **取消链路修复并双验证通过**:\n\n'+markdown+'\n\n'+paragraph
|
||||
const originalFixture=`<div data-table-native-source-flow style={{position:'absolute',inset:0,transform:'translateY(11220px)'}}><div className="chat-pane cr-theme" data-cr-theme="light" style={{position:'absolute',left:324.28125,top:-10834.671875,width:436.703125,height:'auto',minHeight:0,display:'block',overflow:'visible'}}><MarkdownContent content={source} onQuote={text=>window.qa.quoted.push(text)}/></div></div>`
|
||||
const originalFixture=`<div data-table-native-source-flow style={{position:'absolute',inset:0,transform:'translateY(11220px)'}}><div className="chat-pane cr-theme" data-cr-theme="light" style={{position:'absolute',left:324.28125,top:-10834.671875,width:436.703125,height:'auto',minHeight:0,display:'block',overflow:'visible'}}><MarkdownContent content={source} messageId="fixture-message" sourceId="fixture-message-text-0" onQuote={quote=>window.qa.quoted.push(quote)}/></div></div>`
|
||||
const sourceFixture=`<div className="chat-pane cr-theme" data-cr-theme="light" style={{position:'absolute',inset:0,display:'block',overflow:'visible'}}>
|
||||
<div className="cr-message-list-viewport" data-source-node="772" style={{position:'absolute',left:284.28125,top:94,width:526.703125,height:639}}>
|
||||
<div className="cr-message-list cr-message-list--virtual-anchored" data-source-scroll data-source-node="773" style={{overflow:${JSON.stringify(layerCase==='source-clip-flow'?'hidden':'auto')}}}>
|
||||
@@ -24,7 +24,7 @@ if(!process.versions.electron){
|
||||
<div className="cr-document__virtual-item cr-document__virtual-item--flow" data-source-node="1545" style={{height:477.5}}>
|
||||
<div className="cr-frame cr-frame--left" style={{height:477.5,paddingTop:16,paddingBottom:16}}><div className="cr-frame__content" style={{height:445.5}}><div className="cr-agent" style={{height:445.5}}>
|
||||
<div style={{height:36.5}}/><div className="cr-agent__body-row" style={{height:409}}><div className="cr-agent__body" style={{height:409}}><div className="cr-agent__content" style={{height:293.75}}>
|
||||
<div style={{height:26.25}}/><div className="cr-text-block cr-text-block--spaced-top" style={{marginTop:12}}><MarkdownContent content={sourceMarkdown} onQuote={text=>window.qa.quoted.push(text)}/></div>
|
||||
<div style={{height:26.25}}/><div className="cr-text-block cr-text-block--spaced-top" style={{marginTop:12}}><MarkdownContent content={sourceMarkdown} messageId="source-message" sourceId="source-message-text-0" onQuote={quote=>window.qa.quoted.push(quote)}/></div>
|
||||
</div></div></div>
|
||||
</div></div></div>
|
||||
</div>
|
||||
@@ -108,7 +108,7 @@ if(!process.versions.electron){
|
||||
await fs.writeFile(path.join(output,'candidate-roi'+suffix+'.png'),candidate.crop(roi).toPNG())
|
||||
win.webContents.sendInputEvent({type:'mouseMove',x:600,y:402})
|
||||
await new Promise(resolve=>setTimeout(resolve,180))
|
||||
const quote=await win.webContents.executeJavaScript("(()=>{const e=document.querySelector('.markdown-content__table-quote'),r=e.getBoundingClientRect();return {x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2),opacity:getComputedStyle(e).opacity}})()")
|
||||
const quote=await win.webContents.executeJavaScript("(()=>{const e=document.querySelector('.cr-message-quote-table-action'),r=e.getBoundingClientRect();return {x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2),opacity:getComputedStyle(e.closest('.cr-message-quote-table-menu')).opacity}})()")
|
||||
assert.equal(quote.opacity,'1')
|
||||
win.webContents.sendInputEvent({type:'mouseDown',button:'left',clickCount:1,x:quote.x,y:quote.y})
|
||||
win.webContents.sendInputEvent({type:'mouseUp',button:'left',clickCount:1,x:quote.x,y:quote.y})
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
// 真实隔离 YCode App + 真实 preload/IPC/渲染进程,验证表格“添加到对话”与引用标签。
|
||||
// 原版期望值来自固定 REA 归档:ev_38cca1ac66606ab07fb495035e3509f5c4d8ec3e5e2b74d0fdcfcfadb120da3b(菜单/按钮几何)
|
||||
// 与 src-CyXHA0IC.css(SHA-256 a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe,菜单/标签样式)。
|
||||
const fs = require('node:fs/promises')
|
||||
const path = require('node:path')
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
const TABLE_MARKDOWN = [
|
||||
'| 项目 | 说明 |',
|
||||
'| --- | --- |',
|
||||
'| alpha | 第一行 |',
|
||||
'| beta | 第二行 |'
|
||||
].join('\n')
|
||||
|
||||
if (!process.versions.electron) {
|
||||
;(async () => {
|
||||
const directory = await fs.mkdtemp(path.resolve('.tmp/quote-table-'))
|
||||
const data = path.join(directory, 'data')
|
||||
const home = path.join(directory, 'home')
|
||||
const cwd = path.join(directory, 'fixture-workspace')
|
||||
await fs.mkdir(data); await fs.mkdir(home); await fs.mkdir(cwd)
|
||||
const now = Date.now()
|
||||
const crypto = require('node:crypto')
|
||||
const quotedUserMessage = {
|
||||
id: crypto.randomUUID(), role: 'user', content: '这条来自历史记录', createdAt: now - 1000, status: 'done',
|
||||
quotes: [{ id: 'history-text-0:table:table-0', kind: 'table', sourceId: 'history-text-0:table:table-0', messageId: 'history', markdown: TABLE_MARKDOWN, preview: TABLE_MARKDOWN }]
|
||||
}
|
||||
const sessions = [
|
||||
{ id: crypto.randomUUID(), title: 'Quote Live', cwd, createdAt: now, updatedAt: now + 1, messages: [
|
||||
{ id: crypto.randomUUID(), role: 'user', content: '给我一个表格', createdAt: now - 2000, status: 'done' },
|
||||
{ id: crypto.randomUUID(), role: 'assistant', content: `这里是表格:\n\n${TABLE_MARKDOWN}\n\n以上。`, createdAt: now - 1000, status: 'done' }
|
||||
] },
|
||||
{ id: crypto.randomUUID(), title: 'Quote History', cwd, createdAt: now - 5000, updatedAt: now - 4000, messages: [
|
||||
quotedUserMessage,
|
||||
{ id: crypto.randomUUID(), role: 'assistant', content: '好的。', createdAt: now - 900, status: 'done' }
|
||||
] }
|
||||
]
|
||||
await fs.writeFile(path.join(data, 'sessions.json'), JSON.stringify({ sessions }))
|
||||
const env = { ...process.env, YCODE_QA_DIRECTORY: directory, YCODE_USER_DATA_DIR: data,
|
||||
YCODE_OMP_HOME: home, YCODE_DISABLE_GPU: '1', YCODE_QA_OFFSCREEN: '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('QUOTE_TABLE_')) process.stdout.write(bytes) })
|
||||
child.stderr.on('data', bytes => { output += bytes })
|
||||
const timer = setTimeout(() => child.kill(), 150000)
|
||||
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)
|
||||
process.exitCode = code ? 1 : 0
|
||||
})
|
||||
})()
|
||||
} else {
|
||||
const { app, BrowserWindow, ipcMain } = require('electron')
|
||||
app.disableHardwareAcceleration()
|
||||
const directory = process.env.YCODE_QA_DIRECTORY
|
||||
assert.ok(path.relative(path.resolve('.tmp'), directory).startsWith('quote-table-'))
|
||||
app.setPath('userData', path.join(directory, 'data')); app.setPath('sessionData', path.join(directory, 'data'))
|
||||
app.setPath('home', path.join(directory, 'home'))
|
||||
let main, phase = 'boot'
|
||||
const checks = [], snapshots = {}, rendererErrors = []
|
||||
let sentPayload = null
|
||||
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
async function read(source) {
|
||||
let timer
|
||||
try {
|
||||
return await Promise.race([main.webContents.executeJavaScript(source), new Promise((_, reject) => {
|
||||
timer = setTimeout(() => reject(Error('Renderer timeout: ' + source.slice(0, 100))), 15000)
|
||||
})])
|
||||
} catch (error) { throw Error(error.message + ' - ' + source.slice(0, 240)) } finally { clearTimeout(timer) }
|
||||
}
|
||||
async function wait(source, label) {
|
||||
for (let index = 0; index < 250; index++) { const value = await read(source); if (value) return value; await pause(30) }
|
||||
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 })
|
||||
}
|
||||
async function shot(name) {
|
||||
const capture = await main.webContents.debugger.sendCommand('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false })
|
||||
await fs.writeFile(path.join(directory, name + '.png'), Buffer.from(capture.data, 'base64'))
|
||||
}
|
||||
const menuSource = `(()=>{const menu=document.querySelector('.cr-message-quote-table-menu');if(!menu)return null;const button=menu.querySelector('.cr-message-quote-table-action');const style=getComputedStyle(menu);const rect=menu.getBoundingClientRect();const icon=menu.querySelector('svg');return {rect:[rect.x,rect.y,rect.width,rect.height],padding:style.padding,borderRadius:style.borderRadius,border:style.borderWidth+' '+style.borderColor,background:style.backgroundColor,boxShadow:style.boxShadow,position:style.position,top:style.top,right:style.right,added:menu.getAttribute('data-message-quote-table-added'),sourceId:menu.getAttribute('data-message-quote-source-id'),label:button?button.textContent:'',ariaLabel:button?button.getAttribute('aria-label'):null,button:button?(()=>{const bs=getComputedStyle(button);const box=button.getBoundingClientRect();return {width:box.width,height:box.height,fontSize:bs.fontSize,fontWeight:bs.fontWeight,lineHeight:bs.lineHeight,gap:bs.gap,padding:bs.padding,borderRadius:bs.borderRadius,display:bs.display,color:bs.color}})():null,icon:icon?[icon.getBoundingClientRect().width,icon.getBoundingClientRect().height,icon.querySelectorAll('path').length]:null}})()`
|
||||
const cardSource = `(()=>{const rail=document.querySelector('.cr-message-quote-rail');const card=document.querySelector('.cr-input-quote-card');if(!rail||!card)return null;const s=getComputedStyle(card);const rect=card.getBoundingClientRect();const accent=card.querySelector('.cr-input-quote-card__accent');const summary=card.querySelector('.cr-input-quote-card__summary');const remove=card.querySelector('.cr-input-quote-card__remove');return {railPadding:getComputedStyle(rail).padding,railGap:getComputedStyle(rail).gap,railDisplay:getComputedStyle(rail).display,count:document.querySelectorAll('.cr-input-quote-card').length,card:{width:rect.width,height:rect.height,borderRadius:s.borderRadius,padding:s.padding,gap:s.gap,display:s.display,background:s.backgroundColor},accent:accent?accent.getBoundingClientRect().width:null,summary:summary?summary.textContent:null,remove:remove?[remove.getBoundingClientRect().width,remove.getBoundingClientRect().height,remove.getAttribute('aria-label')]:null,sourceId:card.getAttribute('data-message-quote-source-id')}})()`
|
||||
|
||||
;(async () => {
|
||||
await import('node:url').then(({ pathToFileURL }) => import(pathToFileURL(path.resolve('out/main/index.mjs')).href))
|
||||
await app.whenReady()
|
||||
// 记录发送负载并返回合成结果:隔离环境下没有可用模型,但引用链路必须真实经过 IPC。
|
||||
ipcMain.removeHandler('chat:send')
|
||||
ipcMain.handle('chat:send', async (_event, payload) => {
|
||||
sentPayload = payload
|
||||
const runId = 'quote-run'
|
||||
const userMessage = { id: 'quote-user', role: 'user', content: payload.text, quotes: payload.quotes, createdAt: Date.now(), status: 'done', requestId: runId }
|
||||
const assistant = { id: 'quote-assistant', role: 'assistant', content: '收到表格。', createdAt: Date.now(), status: 'done', requestId: runId, tools: [], parts: [] }
|
||||
return { runId, assistantMessageId: assistant.id, userMessage, messages: [...(payload.history ?? []), userMessage, assistant] }
|
||||
})
|
||||
main = BrowserWindow.getAllWindows()[0]
|
||||
main.setSize(1336, 902)
|
||||
main.showInactive()
|
||||
main.webContents.debugger.attach('1.3')
|
||||
main.webContents.on('console-message', (_event, level, message) => { if (level >= 2) rendererErrors.push(message) })
|
||||
await wait('!!document.querySelector("#root")', 'renderer root')
|
||||
await pause(1500)
|
||||
await wait('!!document.querySelector("[data-session-id]")', 'sidebar rows')
|
||||
await step('select-live-session')
|
||||
await read(`(()=>{const rows=[...document.querySelectorAll('[data-session-id]')];const row=rows.find(item=>item.textContent.includes('Quote Live'));if(!row)return false;row.click();return true})()`)
|
||||
await wait(`!!document.querySelector('.cr-table-block')`, 'table block')
|
||||
|
||||
await step('table-action-menu')
|
||||
const menu = await wait(menuSource, 'table quote menu')
|
||||
snapshots.menu = menu
|
||||
check('menu source id matches the original composite id', /-text-\d+:table:table-\d+$/.test(menu.sourceId ?? ''), true)
|
||||
check('menu label is 添加到对话', menu.label, '添加到对话')
|
||||
check('menu aria-label matches the label', menu.ariaLabel, '添加到对话')
|
||||
check('menu is absolutely positioned at top 8 right 8', menu.position === 'absolute' && menu.top === '8px' && menu.right === '8px', true)
|
||||
check('menu padding is 4 like the original', menu.padding, '4px')
|
||||
check('menu radius is 8 like the original', menu.borderRadius, '8px')
|
||||
check('menu background is the primary surface', menu.background, 'rgb(255, 255, 255)')
|
||||
check('menu shows the recovered panel shadow', menu.boxShadow, 'rgba(0, 0, 0, 0.08) 0px 4px 16px 0px, rgba(0, 0, 0, 0.04) 0px 1px 4px 0px')
|
||||
check('button font is 14/22 weight 500', menu.button.fontSize === '14px' && menu.button.lineHeight === '22px' && menu.button.fontWeight === '500', true)
|
||||
check('button padding is 3px 8px like the original', menu.button.padding, '3px 8px')
|
||||
check('button gap is 4 like the original', menu.button.gap, '4px')
|
||||
check('button radius is 6 like the original', menu.button.borderRadius, '6px')
|
||||
check('button height is 28 like the original', Math.round(menu.button.height), 28)
|
||||
check('button width matches the original 106 within layout rounding', menu.button.width, value => Math.abs(value - 106) <= 2)
|
||||
check('button icon is 16x16 with both original paths', menu.icon[0] === 16 && menu.icon[1] === 16 && menu.icon[2] === 2, true)
|
||||
await read(`document.querySelector('.cr-message-quote-table-action').focus()`)
|
||||
await pause(320)
|
||||
check('focus-within reveals the menu like the original', await read(`getComputedStyle(document.querySelector('.cr-message-quote-table-menu')).opacity`), '1')
|
||||
await shot('table-action')
|
||||
|
||||
await step('add-to-conversation')
|
||||
await read(`document.querySelector('.cr-message-quote-table-action').click()`)
|
||||
await wait(cardSource, 'composer quote card')
|
||||
const card = await read(cardSource)
|
||||
snapshots.card = card
|
||||
check('added menu hides itself with the original --added class', await read(`!!document.querySelector('.cr-message-quote-table-menu--added') && document.querySelector('.cr-message-quote-table-menu').getAttribute('data-message-quote-table-added')==='true'`), true)
|
||||
check('added table takes the brand border like the original', await read(`getComputedStyle(document.querySelector('.cr-table-block__inner')).borderTopColor`), 'rgb(64, 209, 179)')
|
||||
check('composer renders exactly one quote card', card.count, 1)
|
||||
check('card radius is 10 like the original', card.card.borderRadius, '10px')
|
||||
check('card padding is 8 like the original', card.card.padding, '8px')
|
||||
check('card gap is 8 like the original', card.card.gap, '8px')
|
||||
check('card background is the item hover surface', card.card.background, 'rgb(242, 242, 242)')
|
||||
check('accent bar is 3px wide like the original', Math.round(card.accent), 3)
|
||||
check('card summary keeps the quoted table text', typeof card.summary === 'string' && card.summary.includes('alpha'), true)
|
||||
check('remove button is 16x16 with a label', card.remove[0] === 16 && card.remove[1] === 16 && !!card.remove[2], true)
|
||||
check('rail padding matches the original banner rail', card.railPadding, '0px 16px 8px')
|
||||
check('rail gap is 6 like the original', card.railGap, '6px')
|
||||
check('existing draft text is not overwritten by the quote', await read(`document.querySelector('.wb-composer__input').value`), '')
|
||||
await shot('composer-quote')
|
||||
|
||||
await step('remove-quote')
|
||||
await read(`document.querySelector('.cr-input-quote-card__remove').click()`)
|
||||
await pause(200)
|
||||
check('removing the card clears the composer rail', await read(`!!document.querySelector('.cr-input-quote-card')`), false)
|
||||
check('removing the quote restores the table button', await read(`!!document.querySelector('.cr-message-quote-table-action') && !document.querySelector('.cr-message-quote-table-menu--added')`), true)
|
||||
|
||||
await step('send-with-quote')
|
||||
await read(`document.querySelector('.cr-message-quote-table-action').click()`)
|
||||
await wait(cardSource, 'quote card before send')
|
||||
await read(`(()=>{const input=document.querySelector('.wb-composer__input');const setter=Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,'value').set;setter.call(input,'看看这个表格');input.dispatchEvent(new Event('input',{bubbles:true}));return true})()`)
|
||||
await pause(120)
|
||||
await read(`document.querySelector('.wb-composer__send').click()`)
|
||||
for (let index = 0; index < 100 && !sentPayload; index++) await pause(50)
|
||||
check('chat:send receives the quote payload through IPC', !!sentPayload && Array.isArray(sentPayload.quotes) && sentPayload.quotes.length === 1, true)
|
||||
check('sent quote keeps the table markdown snapshot', !!sentPayload.quotes[0].markdown.includes('| alpha |'), true)
|
||||
check('sent quote keeps the composite source id', /:table:table-\d+$/.test(sentPayload.quotes[0].sourceId), true)
|
||||
check('sent message keeps the typed text', sentPayload.text, '看看这个表格')
|
||||
const historyCount = await read(`(()=>{const row=[...document.querySelectorAll('[data-session-id]')].find(item=>item.textContent.includes('Quote Live'));return row?row.getAttribute('data-session-id'):null})()`)
|
||||
check('sent session identity is stable', typeof historyCount === 'string' && historyCount.length > 0, true)
|
||||
const sentChip = await wait(`(()=>{const chip=document.querySelector('.cr-frame--right .cr-context-chips .cr-message-quote-tag');if(!chip)return null;const s=getComputedStyle(chip);const box=chip.getBoundingClientRect();const icon=chip.querySelector('.cr-message-quote-tag__icon-container svg');return {height:box.height,borderRadius:s.borderRadius,padding:s.padding,fontSize:s.fontSize,fontWeight:s.fontWeight,lineHeight:s.lineHeight,background:s.backgroundColor,cursor:s.cursor,metaType:chip.getAttribute('data-content-block-meta-type'),icon:icon?[icon.getBoundingClientRect().width,icon.getBoundingClientRect().height]:null,text:chip.textContent}})()`, 'sent quote chip')
|
||||
snapshots.sentChip = sentChip
|
||||
check('sent chip is 22 high like the original', Math.round(sentChip.height), 22)
|
||||
check('sent chip radius is 6 like the original', sentChip.borderRadius, '6px')
|
||||
check('sent chip padding is 2px 6px like the original', sentChip.padding, '2px 6px')
|
||||
check('sent chip font is 13/14 weight 500', sentChip.fontSize === '13px' && sentChip.lineHeight === '14px' && sentChip.fontWeight === '500', true)
|
||||
check('sent chip uses the read-only message-quote background', sentChip.background, 'rgba(0, 0, 0, 0.1)')
|
||||
check('sent chip is read-only', sentChip.cursor, 'default')
|
||||
check('sent chip keeps the message-quote meta type', sentChip.metaType, 'message-quote')
|
||||
check('sent chip keeps the original 16x16 icon box', sentChip.icon[0] === 16 && sentChip.icon[1] === 16, true)
|
||||
await shot('sent-quote')
|
||||
|
||||
await step('history-reload')
|
||||
await read(`(()=>{const rows=[...document.querySelectorAll('[data-session-id]')];const row=rows.find(item=>item.textContent.includes('Quote History'));if(!row)return false;row.click();return true})()`)
|
||||
await pause(600)
|
||||
const reloaded = await wait(`(()=>{const chip=document.querySelector('.cr-frame--right .cr-context-chips .cr-message-quote-tag');if(!chip)return null;return {text:chip.textContent,readOnly:chip.classList.contains('cr-message-quote-tag--read-only'),hasClose:!!chip.querySelector('.cr-message-quote-tag__close'),bubble:document.querySelector('.cr-frame--right .cr-self-bubble').textContent}})()`, 'history quote chip')
|
||||
snapshots.reloaded = reloaded
|
||||
check('history message renders the persisted quote chip', typeof reloaded.text === 'string' && reloaded.text.includes('| 项目 |'), true)
|
||||
check('history chip is read-only without a remove control', reloaded.readOnly === true && reloaded.hasClose === false, true)
|
||||
check('history message keeps its own text', reloaded.bubble.includes('这条来自历史记录'), true)
|
||||
check('no renderer error logs', rendererErrors, [])
|
||||
|
||||
const report = { result: checks.every(item => item.pass) ? 'PASS' : 'FAIL',
|
||||
scope: 'Real isolated YCode App/preload/IPC/renderer. chat:send is stubbed in the test main process to record the payload because the sandbox has no model.',
|
||||
sourceEvidence: ['ev_38cca1ac66606ab07fb495035e3509f5c4d8ec3e5e2b74d0fdcfcfadb120da3b', 'src-CyXHA0IC.css a5e26b537bb50323aefbf25abddad52e288dd3868598be8d255d70886fe77cfe'],
|
||||
snapshots, checks, rendererErrors }
|
||||
await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2))
|
||||
console.log('QUOTE_TABLE_' + report.result + ' ' + directory)
|
||||
for (const failure of checks.filter(item => !item.pass)) console.log('QUOTE_TABLE_FAILURE ' + JSON.stringify(failure))
|
||||
app.exit(report.result === 'PASS' ? 0 : 1)
|
||||
})().catch(async error => {
|
||||
await fs.writeFile(path.join(directory, 'failure.json'), JSON.stringify({ phase, error: String(error), stack: error.stack, checks, snapshots, rendererErrors }, null, 2))
|
||||
console.error(error); if (main) await shot('failure').catch(() => undefined); app.exit(1)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user