Files
dsh-context/tests/client/components/browserFocus.spec.ts
T
bowenliang123 9aaa11591a test(client): split the oversized specs into siblings with shared harnesses
browser.spec.ts (2360), trendChart.spec.ts (1629), contextView.spec.ts (1504), and
overview.spec.ts (1065) exceeded the 1000-line limit. Each splits along its existing
describe seams into same-directory siblings, with the fixtures that more than one
sibling needs extracted once into a shared harness.

The move is verbatim: test titles, describe blocks, and every assertion line are
preserved exactly (2061 tests before and after, no duplicate titles, no skips), and
the suite reports the same coverage.
2026-10-10 01:21:15 +08:00

199 lines
9.1 KiB
TypeScript

import { createElement as h } from 'react'
import assert from 'node:assert/strict'
import { describe, test } from 'vitest'
import { DICT_EN } from '../../../src/client/i18n'
import type { ContextHeaders } from '../../../src/shared/types'
import type { ConversationNodeLike } from '../../../src/client/services'
import { click, mount, query, queryAll, surfaceNode, text } from '../helpers/kit'
import { Browser, catRow, pickStep, props, req, tl } from './browserHarness'
describe('ContextBrowser focus bridges', () => {
const headers: ContextHeaders = {
headers: [{
seq: 1, time: 1, systemTokens: 3,
tools: [
{ name: 'beta', tokens: 50 },
{ name: 'alpha', tokens: 10 },
],
}],
}
const data = tl({
current: { system: 10, tools: 60, user: 10, inject: 0, skill: 0, assistant: 0, tool: 0, total: 80 },
requests: [req({ seq: 10, turn: 1, step: 0, total: 80 }), req({ seq: 20, turn: 1, step: 1, total: 80 })],
nodes: [surfaceNode({ seq: 1, tokens: 5, text: 'focusable' }), surfaceNode({ seq: 11, cat: 'assistant', tokens: 5, text: 'answer' })],
})
const convNodes: ConversationNodeLike[] = [{ kind: 'user', seq: 1, content: [{ type: 'text', text: 'focusable' }] }]
test('nodeFocus selects the step, opens the node, scrolls it into view, and clears', async () => {
const scrolls: unknown[] = []
const orig = Element.prototype.scrollIntoView
Element.prototype.scrollIntoView = function (this: Element, arg?: unknown) { scrolls.push(arg) } as never
try {
let handled = 0
const m = await mount(h(Browser, props({ data, headers, convNodes })))
await m.update(h(Browser, props({
data, headers, convNodes,
nodeFocus: { step: 'live', key: 'n1', cat: 'user' },
onNodeFocusHandled: () => { handled += 1 },
})))
assert.equal(handled, 1)
const onRows = queryAll(m.container, '.lc-br-elem-on')
assert.equal(onRows.length, 1)
assert.ok(text(onRows[0]).includes('focusable'))
assert.deepEqual(scrolls, [{ block: 'nearest' }], 'the open row scrolled into view')
// A step focus switches the assembled view.
await m.update(h(Browser, props({
data, headers, convNodes,
nodeFocus: { step: 20, key: 'n1', cat: 'user' },
onNodeFocusHandled: () => { handled += 1 },
})))
assert.ok(text(query(m.container, '.lc-br-meta')).includes('Turn 1 · Step 1'))
assert.equal(handled, 2)
await m.update(h(Browser, props({
data, headers, convNodes,
nodeFocus: { step: 'live', key: 'n999', cat: 'user' },
})))
assert.equal(queryAll(m.container, '.lc-br-elem-on').length, 0)
await m.unmount()
} finally {
Element.prototype.scrollIntoView = orig
}
})
test('pinSeq selects the pinned step and unpinning returns to live (accordion resets)', async () => {
const m = await mount(h(Browser, props({ data, headers })))
await click(catRow(m, 'user'))
assert.equal(queryAll(m.container, '.lc-br-body').length, 1)
await m.update(h(Browser, props({ data, headers, pinSeq: 10 })))
assert.ok(text(query(m.container, '.lc-br-meta')).includes('Turn 1 · Step 0'))
assert.equal(queryAll(m.container, '.lc-br-body').length, 0, 'pinning resets the accordion')
assert.equal(query<HTMLSelectElement>(m.container, 'select.lc-br-pick').value, '10')
await m.update(h(Browser, props({ data, headers, pinSeq: null })))
assert.ok(text(query(m.container, '.lc-br-meta')).includes('Live · Next Request'))
await m.unmount()
})
})
describe('ContextBrowser — the split generation detail states', () => {
test('a pending detail read names the state on the note strip', async () => {
const m = await mount(h(Browser, { data: tl({}), headers: null, detailState: 'loading' }))
assert.ok(text(m.container).includes(DICT_EN['detail.loading']))
// The picker still offers the live surface (the sections read zero until the detail lands).
assert.equal(queryAll(m.container, '.lc-br-pick option').length, 1)
await m.unmount()
})
test('a failed detail read arms the retry button on the strip', async () => {
let retries = 0
const m = await mount(h(Browser, {
data: tl({}),
headers: null,
detailState: 'failed',
onDetailRetry: () => { retries++ },
}))
await click(query(m.container, '.lc-br-retry'))
assert.equal(retries, 1)
await m.unmount()
})
test('a landed (or inline) detail renders no strip', async () => {
for (const state of ['ready', 'legacy'] as const) {
const m = await mount(h(Browser, { data: tl({}), headers: null, detailState: state }))
assert.ok(!text(m.container).includes(DICT_EN['detail.loading']))
assert.ok(!text(m.container).includes(DICT_EN['detail.loadFailed']))
await m.unmount()
}
// And the prop-less caller (older hosts) renders nothing either.
const m = await mount(h(Browser, { data: tl({}), headers: null }))
assert.ok(queryAll(m.container, '.lc-br-retry').length === 0)
await m.unmount()
})
})
describe('ContextBrowser open-category reporting', () => {
test('toggling categories and step picks report the open category outward', async () => {
const data = tl({
requests: [req({ seq: 10, turn: 1, step: 0 }), req({ seq: 20, turn: 2, step: 0 })],
nodes: [
surfaceNode({ seq: 1, cat: 'user', text: 'hi' }),
surfaceNode({ seq: 2, cat: 'user', text: 'again' }),
surfaceNode({ seq: 3, cat: 'tool', tokens: 30, tool: 'bash', text: 'out' }),
],
})
const opens: (string | null)[] = []
const onOpenCat = (c: string | null): void => { opens.push(c) }
const m = await mount(h(Browser, props({ data, onOpenCat })))
assert.deepEqual(opens, [null], 'the mount-time pin pass reports the closed accordion')
await click(catRow(m, 'user'))
assert.deepEqual(opens, [null, 'user'])
await click(catRow(m, 'user'))
assert.deepEqual(opens, [null, 'user', null])
await click(catRow(m, 'tool'))
assert.deepEqual(opens, [null, 'user', null, 'tool'])
await pickStep(m, '20')
assert.deepEqual(opens, [null, 'user', null, 'tool', null], 'a step pick closes the open category')
await m.unmount()
})
test('a pin change and a brief reveal report their reset / target category', async () => {
const data = tl({ requests: [req({ seq: 10, turn: 1, step: 0 })], nodes: [surfaceNode({ seq: 1, cat: 'user', text: 'hi' })] })
const opens: (string | null)[] = []
const onOpenCat = (c: string | null): void => { opens.push(c) }
const m = await mount(h(Browser, props({ data, onOpenCat })))
await m.update(h(Browser, props({ data, onOpenCat, pinSeq: 10 })))
assert.deepEqual(opens, [null, null], 'pinning resets the accordion')
await m.update(h(Browser, props({
data, onOpenCat, pinSeq: 10,
nodeFocus: { step: 'live', key: 'n1', cat: 'tool' },
onNodeFocusHandled: () => {},
})))
assert.deepEqual(opens, [null, null, 'tool'], 'the reveal opens the node category')
await m.unmount()
})
test('catFocus opens the named category on the live surface, picking the kind chip', async () => {
const data = tl({
requests: [req({ seq: 10, turn: 1, step: 0 })],
nodes: [
surfaceNode({ seq: 1, cat: 'skill', tokens: 5, skill: 'pdf', text: 'instructions' }),
surfaceNode({ seq: 2, cat: 'assistant', tokens: 5, text: 'an answer' }),
surfaceNode({ seq: 3, cat: 'assistant', tokens: 5, calls: ['bash'] }),
],
})
const opens: (string | null)[] = []
const onOpenCat = (c: string | null): void => { opens.push(c) }
let handled = 0
const m = await mount(h(Browser, props({ data, onOpenCat })))
// The stats card's Answers figure: assistant open, the answer chip picked — the tool-call-only row filters out.
await m.update(h(Browser, props({
data,
onOpenCat,
catFocus: { cat: 'assistant', kind: 'answer' },
onCatFocusHandled: () => { handled += 1 },
})))
assert.equal(handled, 1)
assert.deepEqual(opens, [null, 'assistant'])
assert.ok(catRow(m, 'assistant').className.includes('lc-br-cat-open'))
const assistantCat = catRow(m, 'assistant').parentElement as HTMLElement
const chips = queryAll(assistantCat, '.lc-gran-btn')
const answerChip = chips.find(b => text(b).includes('Answer'))
assert.ok(answerChip !== undefined && answerChip.className.includes('lc-gran-on'), 'the answer chip is picked')
const shownRows = queryAll(assistantCat, '.lc-br-elem')
assert.equal(shownRows.length, 1, 'only the textual reply survives the answer filter')
assert.ok(text(shownRows[0]).includes('an answer'))
// The stats card's Skill Loads figure: skill open, no chip picked, lens cleared.
await m.update(h(Browser, props({
data,
onOpenCat,
catFocus: { cat: 'skill' },
onCatFocusHandled: () => { handled += 1 },
})))
assert.equal(handled, 2)
assert.deepEqual(opens, [null, 'assistant', 'skill'])
assert.ok(catRow(m, 'skill').className.includes('lc-br-cat-open'))
assert.ok(!catRow(m, 'assistant').className.includes('lc-br-cat-open'), 'the previous category closed')
await m.unmount()
})
})