fix(B01): preserve question routing and nested disclosure state
This commit is contained in:
1 parent
21ca0e0fc3
commit
51531dcae8
15 files changed
+341
-55
No files matched your search
@@ -0,0 +1,68 @@
|
||||
/** Real nested React DOM components; deliberately no Electron, SDK, network, or layout claim. */
|
||||
import assert from 'node:assert/strict'
|
||||
import { createRequire } from 'node:module'
|
||||
const { JSDOM } = createRequire(process.cwd() + '/package.json')(process.env.YCODE_TEST_DOM_MODULE || 'jsdom')
|
||||
const dom = new JSDOM('<html><body><main id="root"></main></body></html>', { pretendToBeVisual: true, url: 'http://localhost/' })
|
||||
for (const key of ['window', 'document', 'Node', 'Element', 'HTMLElement', 'MouseEvent', 'KeyboardEvent', 'Event']) Object.defineProperty(globalThis, key, { value: dom.window[key], configurable: true })
|
||||
Object.defineProperty(globalThis, 'navigator', { value: dom.window.navigator, configurable: true })
|
||||
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
||||
const { act } = require('react') as typeof import('react')
|
||||
const { createRoot } = require('react-dom/client') as typeof import('react-dom/client')
|
||||
const { CompletedFold } = require('../src/renderer/src/workbuddy/conversation/CompletedFold') as typeof import('../src/renderer/src/workbuddy/conversation/CompletedFold')
|
||||
const { ToolGroup } = require('../src/renderer/src/workbuddy/conversation/ToolCall') as typeof import('../src/renderer/src/workbuddy/conversation/ToolCall')
|
||||
const { ToolCallRenderer } = require('../src/renderer/src/workbuddy/conversation/ToolCallRenderer') as typeof import('../src/renderer/src/workbuddy/conversation/ToolCallRenderer')
|
||||
const { ThinkingBlock, TaskBlock, PlanCreateBlock } = require('../src/renderer/src/workbuddy/conversation/SpecializedBlocks') as typeof import('../src/renderer/src/workbuddy/conversation/SpecializedBlocks')
|
||||
const root = createRoot(document.getElementById('root')!)
|
||||
const tool = (id: string) => ({ id, name: id === 'q' ? 'AskUserQuestion' : 'UnknownOrdinaryTool', status: 'executed' as const, args: { value: id }, result: 'RAW TOOL RESULT', metaData: id === 'q' ? { questionAnswer: { questions: [{ question: 'Choose?', answers: ['Yes'] }] } } : undefined })
|
||||
const initialParts = [{ type: 'reasoning', id: 'r', text: 'Thought' }, { type: 'tool', toolId: 't' }, { type: 'tool', toolId: 'q' }, { type: 'text', text: 'Answer' }, { type: 'reasoning', id: 'r2', text: 'Later thought' }, { type: 'tool', toolId: 't2' }, { type: 'tool', toolId: 'task' }, { type: 'tool', toolId: 'plan' }]
|
||||
function Fixture({ parts = initialParts, id = 'message-one', finished = true }: { parts?: typeof initialParts; id?: string; finished?: boolean }) {
|
||||
return <CompletedFold foldId={id} parts={parts} turnFinished={finished} classifyToolCall={part => part.toolId === 'q' ? { role: 'inline-question' } : undefined}
|
||||
renderGroup={(group, children) => <ToolGroup variant="summary" className={`group-${group.units[0].part.id || group.units[0].part.toolId}`} stats={{ toolCallCount: group.toolCallCount, messageCount: group.units.length, errorCount: 0 }}>{children}</ToolGroup>}
|
||||
renderUnit={part => part.type === 'tool' && part.toolId === 'task' ? <TaskBlock status="executed" data={{ finalResult: 'Task result', toolInfo: [] }} /> : part.type === 'tool' && part.toolId === 'plan' ? <PlanCreateBlock status="success" data={{ id: 'plan', name: 'Plan', todolist: [] }} /> : part.type === 'reasoning' ? <ThinkingBlock text={String(part.text)} isFinished /> : part.type === 'tool' ? <ToolCallRenderer tool={tool(String(part.toolId))} /> : <p>{String(part.text)}</p>} />
|
||||
}
|
||||
const get = (selector: string) => { const el = document.querySelector(selector); assert.ok(el, selector); return el }
|
||||
const expanded = (selector: string) => get(selector).getAttribute('aria-expanded')
|
||||
const outer = '.cr-collapse--completed > .cr-collapse__header'
|
||||
const question = '.ask-user-question__header .cr-tool-head__main'
|
||||
const ordinary = '[data-tool-call-id="t"] .cr-tool-head__main'
|
||||
const thinking = '.group-r .cr-reasoning > .cr-collapse__header'
|
||||
const group = '.group-r > .cr-collapse__header'
|
||||
const second = '.group-r2 > .cr-collapse__header'
|
||||
async function click(selector: string) { await act(async () => get(selector).dispatchEvent(new MouseEvent('click', { bubbles: true }))) }
|
||||
async function keyboard(selector: string, key: string) { await act(async () => get(selector).dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }))) }
|
||||
async function main() {
|
||||
await act(async () => root.render(<Fixture />))
|
||||
assert.equal(expanded(outer), 'false')
|
||||
await keyboard(outer, 'Enter')
|
||||
for (const selector of [group, thinking, ordinary, question]) { assert.equal(expanded(selector), 'false'); await click(selector); assert.equal(expanded(selector), 'true') }
|
||||
assert.equal(expanded(second), 'false', 'local summaries are independent')
|
||||
for (const selector of ['.cb-task-result-item-title', '.cb-plan-header-left']) { assert.equal(expanded(selector), 'true'); await click(selector); assert.equal(expanded(selector), 'false') }
|
||||
await keyboard(outer, ' ')
|
||||
const content = document.getElementById(get(outer).getAttribute('aria-controls')!)!
|
||||
assert.equal(content.getAttribute('aria-hidden'), 'true')
|
||||
assert.ok(content.hasAttribute('inert'), 'outer hidden descendants cannot receive focus')
|
||||
await keyboard(outer, 'Enter')
|
||||
for (const selector of [group, thinking, ordinary, question]) assert.equal(expanded(selector), 'true', `${selector} survives outer remount`)
|
||||
assert.equal(expanded(second), 'false')
|
||||
for (const selector of ['.cb-task-result-item-title', '.cb-plan-header-left']) assert.equal(expanded(selector), 'false', 'specialized tool choice retained')
|
||||
await click(ordinary)
|
||||
await act(async () => root.render(<Fixture parts={[...initialParts, { type: 'tool', toolId: 't3' }]} />))
|
||||
await click(outer); await click(outer)
|
||||
assert.equal(expanded(ordinary), 'false', 'explicit collapsed choice persists after later event')
|
||||
for (const selector of [group, thinking, question]) assert.equal(expanded(selector), 'true', `${selector} retained after later event`)
|
||||
assert.equal(expanded(second), 'false')
|
||||
assert.equal(document.querySelectorAll('.ask-user-question--completed').length, 1, 'no duplicated hidden content trees')
|
||||
await act(async () => root.render(<Fixture id="message-two" />))
|
||||
assert.equal(expanded(outer), 'false', 'message identity resets outer state')
|
||||
await click(outer)
|
||||
for (const selector of [group, thinking, ordinary, question]) assert.equal(expanded(selector), 'false', 'message identity isolates nested state')
|
||||
await act(async () => root.render(<Fixture id="live-message" finished={false} />))
|
||||
for (const selector of [group, thinking, ordinary, question]) await click(selector)
|
||||
await act(async () => root.render(<Fixture id="live-message" finished />))
|
||||
assert.equal(expanded(outer), 'false', 'completion starts folded after live events')
|
||||
await click(outer)
|
||||
for (const selector of [group, thinking, ordinary, question]) assert.equal(expanded(selector), 'true', `${selector} survives live-to-completed reparenting`)
|
||||
await act(async () => root.unmount()); dom.window.close()
|
||||
console.log('completed-fold-nested: real question/tool/thinking/local-summary remount, event, keyboard and identity contracts passed')
|
||||
}
|
||||
main().then(() => process.exit(0)).catch(error => { console.error(error); dom.window.close(); process.exit(1) })
|
||||
Reference in new issue
Block a user