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.
349 lines
19 KiB
TypeScript
349 lines
19 KiB
TypeScript
import { act, createElement as h } from 'react'
|
|
import assert from 'node:assert/strict'
|
|
import { describe, test } from 'vitest'
|
|
import { DICT_EN } from '../../../src/client/i18n'
|
|
import { TestClientCtx } from '../helpers/harness'
|
|
import { click, flush, hover, mount, query, queryAll, text, unhover } from '../helpers/kit'
|
|
import { buttonByText, makeView, mountRich, projectionsFor, T0, timeline } from './contextViewHarness'
|
|
|
|
describe('ContextView — interactions', () => {
|
|
test('renders the rich tab: stats, chart bars, markers, events, nodes, browser', async () => {
|
|
const m = await mountRich('sv-rich')
|
|
assert.ok(text(m.container).includes('deepseek-v4-flash · deepseek'))
|
|
assert.equal(queryAll(m.container, '.lc-bar').length, 3)
|
|
// The compaction event lands as the ✂ marker on the first request after it.
|
|
assert.equal(queryAll(m.container, '.lc-bar-marker').length, 1)
|
|
assert.equal(queryAll(m.container, '.lc-event').length, 2)
|
|
assert.ok(text(m.container).includes(DICT_EN['files.title']))
|
|
assert.ok(text(m.container).includes(DICT_EN['files.scopeLatest']))
|
|
assert.ok(text(m.container).includes(DICT_EN['browser.liveNow']))
|
|
// Turn strip partitions the two turn groups (turn 1 + the turn-less 0).
|
|
assert.deepEqual(queryAll(m.container, '.lc-turn-label').map(el => text(el)), ['1', '0'])
|
|
await m.unmount()
|
|
})
|
|
|
|
test('bar hover previews in the browser; hover loss on a granularity switch falls back', async () => {
|
|
const m = await mountRich('sv-hover')
|
|
const chart = query(m.container, '.lc-chart')
|
|
const bar2 = query(m.container, '.lc-bar[data-seq="2"]')
|
|
await hover(bar2)
|
|
assert.ok(bar2.className.includes('lc-bar-hovered'))
|
|
// The hovered bar's turn lights the strip even without strip hover.
|
|
assert.ok(query(m.container, '.lc-chart-scroll').className.includes('lc-chart-dim'))
|
|
|
|
// Switching to turn granularity drops seq 2 (turn 1 keeps its LAST step):
|
|
// the stale hovered seq matches nothing and the active bar falls back.
|
|
await click(buttonByText(m.container, DICT_EN['gran.turn']))
|
|
assert.equal(queryAll(m.container, '.lc-bar').length, 2)
|
|
assert.equal(queryAll(m.container, '.lc-bar-hovered').length, 0)
|
|
await unhover(chart)
|
|
await click(buttonByText(m.container, DICT_EN['gran.step']))
|
|
assert.equal(queryAll(m.container, '.lc-bar').length, 3)
|
|
await m.unmount()
|
|
})
|
|
|
|
test('a request without a turn highlights nothing on hover', async () => {
|
|
const m = await mountRich('sv-noturn')
|
|
const chart = query(m.container, '.lc-chart')
|
|
await hover(query(m.container, '.lc-bar[data-seq="6"]'))
|
|
assert.ok(query(m.container, '.lc-bar[data-seq="6"]').className.includes('lc-bar-hovered'))
|
|
assert.ok(!query(m.container, '.lc-chart-scroll').className.includes('lc-chart-dim'))
|
|
await unhover(chart)
|
|
await m.unmount()
|
|
})
|
|
|
|
test('turn strip hover dims, strip click focuses the turn in turn granularity', async () => {
|
|
const m = await mountRich('sv-strip')
|
|
const turns = queryAll(m.container, '.lc-turn')
|
|
assert.equal(turns.length, 2)
|
|
await hover(turns[0])
|
|
assert.ok(query(m.container, '.lc-chart-scroll').className.includes('lc-chart-dim'))
|
|
await unhover(query(m.container, '.lc-turns'))
|
|
assert.ok(!query(m.container, '.lc-chart-scroll').className.includes('lc-chart-dim'))
|
|
|
|
// Strip click: granularity switches to turn and the focus is consumed once.
|
|
await click(turns[0])
|
|
assert.ok(buttonByText(m.container, DICT_EN['gran.turn']).className.includes('lc-gran-on'))
|
|
assert.equal(queryAll(m.container, '.lc-bar').length, 2)
|
|
await click(buttonByText(m.container, DICT_EN['gran.step']))
|
|
await m.unmount()
|
|
})
|
|
|
|
test('pinning a bar selects its step in the browser; unpinning returns to live', async () => {
|
|
const m = await mountRich('sv-pin')
|
|
const pick = query<HTMLSelectElement>(m.container, 'select.lc-br-pick')
|
|
assert.equal(pick.value, 'live')
|
|
|
|
const bar4 = query(m.container, '.lc-bar[data-seq="4"]')
|
|
await click(bar4)
|
|
assert.ok(bar4.className.includes('lc-bar-selected'))
|
|
assert.equal(pick.value, '4')
|
|
assert.ok(query(m.container, '.lc-detail').textContent?.includes('✂') === true)
|
|
|
|
await click(bar4)
|
|
assert.ok(!bar4.className.includes('lc-bar-selected'))
|
|
assert.equal(pick.value, 'live')
|
|
await m.unmount()
|
|
})
|
|
|
|
test('brief rows locate their node in the browser (input, mid response, live response)', async () => {
|
|
const m = await mountRich('sv-brief')
|
|
const pick = query<HTMLSelectElement>(m.container, 'select.lc-br-pick')
|
|
|
|
// Pin the second bar: brief = opener + inputs (node 3) + response (node 4).
|
|
await click(query(m.container, '.lc-bar[data-seq="4"]'))
|
|
const briefRows = queryAll(m.container, '.lc-brief-row')
|
|
assert.equal(briefRows.length, 3)
|
|
|
|
// The In row reveals node 3 (tool) inside the step's OWN surface.
|
|
const inRow = briefRows.find(r => text(r).includes(DICT_EN['brief.input']))
|
|
assert.ok(inRow !== undefined)
|
|
await click(inRow)
|
|
assert.equal(pick.value, '4')
|
|
assert.ok(text(query(m.container, '.lc-br-elem-on')).includes('file output'))
|
|
|
|
// The response of a middle bar first appears in the NEXT step's surface.
|
|
const replyRow = briefRows.find(r => text(r).includes('reply two'))
|
|
assert.ok(replyRow !== undefined)
|
|
await click(replyRow)
|
|
assert.equal(pick.value, '6')
|
|
assert.ok(text(query(m.container, '.lc-br-elem-on')).includes('reply two'))
|
|
|
|
// The last bar's response lands on the LIVE surface.
|
|
await click(query(m.container, '.lc-bar[data-seq="6"]'))
|
|
const lastReply = queryAll(m.container, '.lc-brief-row').find(r => text(r).includes('reply three'))
|
|
assert.ok(lastReply !== undefined)
|
|
await click(lastReply)
|
|
assert.equal(pick.value, 'live')
|
|
assert.ok(text(query(m.container, '.lc-br-elem-on')).includes('reply three'))
|
|
await m.unmount()
|
|
})
|
|
|
|
test('composition legend hover lights the linked browser category', async () => {
|
|
const m = await mountRich('sv-link')
|
|
const comp = queryAll(m.container, '.lc-card').find(c => text(c).includes(DICT_EN['overview.title']))
|
|
assert.ok(comp !== undefined)
|
|
|
|
const chip = query(comp, '.lc-legend .lc-chip')
|
|
await hover(chip)
|
|
assert.ok(query(m.container, '.lc-br-cat-row').className.includes('lc-br-cat-on'))
|
|
await unhover(chip)
|
|
assert.ok(!query(m.container, '.lc-br-cat-row').className.includes('lc-br-cat-on'))
|
|
await m.unmount()
|
|
})
|
|
|
|
test('browser category hover lights the trend chart segments and the detail composition bar', async () => {
|
|
const m = await mountRich('sv-link-trend')
|
|
// Category rows render in CATS order; hover "tools" (second row).
|
|
const toolsRow = queryAll(m.container, '.lc-br-cat-row')[1]
|
|
await hover(toolsRow)
|
|
assert.equal(query(m.container, '.lc-chart').getAttribute('data-catdim'), 'tools')
|
|
// The detail's composition bar mirrors the same key with its tip off (covered in requestDetail.spec).
|
|
const detailBar = query(m.container, '.lc-detail .lc-stacked')
|
|
assert.ok(detailBar.className.includes('lc-stacked-dim'))
|
|
assert.ok(queryAll(detailBar, '.lc-stacked-seg-on').length === 1)
|
|
assert.ok(queryAll(m.container, '.lc-detail-row-on').length === 1, 'the matching detail row lights too')
|
|
|
|
await unhover(toolsRow)
|
|
assert.equal(query(m.container, '.lc-chart').hasAttribute('data-catdim'), false)
|
|
assert.ok(!query(m.container, '.lc-detail .lc-stacked').className.includes('lc-stacked-dim'))
|
|
|
|
// The overview's 'free' track hover names no category — the trend card stays neutral (same filter as the browser link).
|
|
await hover(query(m.container, '.lc-stacked-free'))
|
|
assert.equal(query(m.container, '.lc-chart').hasAttribute('data-catdim'), false)
|
|
assert.ok(!query(m.container, '.lc-detail .lc-stacked').className.includes('lc-stacked-dim'))
|
|
await unhover(query(m.container, '.lc-stacked-free'))
|
|
await m.unmount()
|
|
})
|
|
|
|
test('expanding a browser category focuses the trend bars on it; collapsing restores all', async () => {
|
|
const m = await mountRich('sv-focus')
|
|
assert.equal(text(query(m.container, '.lc-axis-top')), '420', 'unfocused: the axis tops at the largest whole-bar total')
|
|
// Five segments on the bar: richTimeline carries no injects, so inject renders none.
|
|
assert.equal(queryAll(m.container, '.lc-bar[data-seq="4"] .lc-bar-stack > .lc-cat-seg').length, 5)
|
|
|
|
// Expanding the browser's assistant category (row 5 — row 4 is the empty
|
|
// skill bucket) focuses every bar on it — one segment per bar, the axis
|
|
// rescaled to the category's own max (20/60/80; the first rides its provider-prompt anchor to 21).
|
|
await click(queryAll(m.container, '.lc-br-cat-row')[5])
|
|
assert.equal(text(query(m.container, '.lc-axis-top')), '80')
|
|
const segs = queryAll(m.container, '.lc-bar .lc-bar-stack > .lc-cat-seg')
|
|
assert.equal(segs.length, 3)
|
|
for (const s of segs) assert.equal(s.getAttribute('data-cat'), 'assistant')
|
|
|
|
await click(queryAll(m.container, '.lc-br-cat-row')[5])
|
|
assert.equal(text(query(m.container, '.lc-axis-top')), '420')
|
|
assert.equal(queryAll(m.container, '.lc-bar[data-seq="4"] .lc-bar-stack > .lc-cat-seg').length, 5)
|
|
await m.unmount()
|
|
})
|
|
|
|
test('the adaptive switch rides the trend title and toggles mount-locally', async () => {
|
|
const m = await mountRich('sv-adaptive')
|
|
const card = queryAll(m.container, '.lc-card').find(c => text(c).includes(DICT_EN['trend.title']))
|
|
assert.ok(card !== undefined)
|
|
const titleText = query(card, '.lc-card-title-text')
|
|
assert.equal(text(titleText), DICT_EN['trend.title'])
|
|
|
|
// The three display modifiers ride the title text's right as ONE segmented group, left of the
|
|
// card's right-hand control cluster.
|
|
const toggleOf = () => buttonByText(m.container, DICT_EN['trend.adaptive'])
|
|
const modsGroupOf = () => buttonByText(m.container, DICT_EN['trend.dna']).parentElement as HTMLElement
|
|
assert.ok(modsGroupOf().className.includes('lc-trend-mods'))
|
|
assert.equal(modsGroupOf().previousElementSibling, titleText)
|
|
assert.equal(toggleOf().parentElement, modsGroupOf())
|
|
assert.equal(toggleOf().previousElementSibling, buttonByText(m.container, DICT_EN['trend.dna']), 'Adaptive rides right of DNA inside the group')
|
|
assert.equal(toggleOf().getAttribute('title'), DICT_EN['trend.adaptiveHint'])
|
|
assert.ok(!toggleOf().className.includes('lc-gran-on'), 'off at mount')
|
|
|
|
// Toggling is mount-local: the chart keeps rendering (jsdom has no layout, so the zero-width viewport falls
|
|
// back to the whole-log axis instead of flattening the bars) and nothing is written back to settings.
|
|
await click(toggleOf())
|
|
assert.ok(toggleOf().className.includes('lc-gran-on'))
|
|
assert.equal(text(query(m.container, '.lc-axis-top')), '420')
|
|
await click(toggleOf())
|
|
assert.ok(!toggleOf().className.includes('lc-gran-on'))
|
|
assert.equal(text(query(m.container, '.lc-axis-top')), '420')
|
|
await m.unmount()
|
|
})
|
|
|
|
test('the duration toggle overlays the active-time curve with a right-hand quartile axis', async () => {
|
|
const View = makeView(new TestClientCtx())
|
|
const m = await mount(h(View, {
|
|
sessionId: 'sv-ms-curve',
|
|
useProjection: projectionsFor(timeline({
|
|
requests: [
|
|
{ seq: 2, turn: 1, step: 1, time: T0 + 1000, system: 100, tools: 200, user: 10, inject: 0, assistant: 20, tool: 0, total: 330, activeMs: 1000 },
|
|
{ seq: 4, turn: 1, step: 2, time: T0 + 3000, system: 100, tools: 200, user: 10, inject: 0, assistant: 60, tool: 30, total: 400, activeMs: 2000 },
|
|
],
|
|
})),
|
|
}))
|
|
|
|
const msBtn = () => buttonByText(m.container, DICT_EN['trend.duration'])
|
|
const group = msBtn().parentElement as HTMLElement
|
|
assert.ok(group.className.includes('lc-trend-mods'))
|
|
assert.equal(msBtn().getAttribute('title'), DICT_EN['trend.durationTip'])
|
|
assert.equal(msBtn().previousElementSibling, buttonByText(m.container, DICT_EN['trend.adaptive']), 'Duration rides right of the adaptive switch')
|
|
// The settings default (show) mounts the overlay ON: the curve spans the two stamped bars and the
|
|
// right axis reads the 2s peak (jsdom's zero-width viewport keeps the whole-log scale, adaptive off anyway).
|
|
assert.ok(msBtn().className.includes('lc-gran-on'), 'on at mount (the settings default)')
|
|
assert.equal(queryAll(m.container, '.lc-duration polyline:not(.lc-dur-halo)').length, 1)
|
|
assert.equal(text(query(m.container, '.lc-axis-r .lc-axis-top')), '2.0s')
|
|
assert.equal(text(query(m.container, '.lc-axis-r .lc-axis-mid')), '1.0s')
|
|
|
|
await click(msBtn())
|
|
assert.ok(!msBtn().className.includes('lc-gran-on'))
|
|
assert.equal(queryAll(m.container, '.lc-duration').length, 0)
|
|
assert.equal(queryAll(m.container, '.lc-axis-r').length, 0)
|
|
|
|
await click(msBtn())
|
|
assert.equal(queryAll(m.container, '.lc-duration polyline:not(.lc-dur-halo)').length, 1)
|
|
assert.equal(queryAll(m.container, '.lc-axis-r').length, 1)
|
|
await m.unmount()
|
|
})
|
|
|
|
test('the trend card\'s DNA toggle fingerprints the bars, combinable with Delta', async () => {
|
|
const m = await mountRich('sv-trend-dna')
|
|
const dnaBtn = () => buttonByText(m.container, DICT_EN['trend.dna'])
|
|
assert.ok(!dnaBtn().className.includes('lc-gran-on'), 'off at mount')
|
|
const dnaGroup = dnaBtn().parentElement as HTMLElement
|
|
assert.ok(dnaGroup.className.includes('lc-trend-mods'))
|
|
assert.equal(dnaBtn().getAttribute('title'), DICT_EN['trend.dnaTip'])
|
|
assert.equal(dnaBtn().nextElementSibling, buttonByText(m.container, DICT_EN['trend.adaptive']), 'DNA rides left of the adaptive switch')
|
|
|
|
await click(dnaBtn())
|
|
assert.ok(dnaBtn().className.includes('lc-gran-on'))
|
|
assert.equal(queryAll(m.container, '.lc-bar-dna').length, 3)
|
|
assert.equal(text(query(m.container, '.lc-axis-top')), '420')
|
|
|
|
// The strip reads in the model's read order: the tool schema leads, then the messages by seq.
|
|
const g = query(m.container, '.lc-bar[data-seq="4"] .lc-bar-dna').style.background
|
|
const amberAt = g.indexOf('color-amber-500')
|
|
const greenAt = g.indexOf('color-green-500')
|
|
const blueAt = g.indexOf('color-blue-500')
|
|
const tealAt = g.indexOf('color-teal-500')
|
|
assert.ok(amberAt >= 0 && greenAt > amberAt && blueAt > greenAt && tealAt > blueAt, g)
|
|
|
|
// DNA + Delta: the mode switch stays live and each bar diffs its bands against the previous
|
|
// one — the first bar carries no change (no strip), the axis turns signed around a zero line.
|
|
await click(buttonByText(m.container, DICT_EN['gran.delta']))
|
|
assert.equal(queryAll(m.container, '.lc-bar-dna').length, 2)
|
|
assert.ok(text(query(m.container, '.lc-axis-mid')).includes('0'), 'the delta zero line label rides the axis')
|
|
|
|
await click(dnaBtn())
|
|
assert.ok(!dnaBtn().className.includes('lc-gran-on'))
|
|
assert.equal(queryAll(m.container, '.lc-bar-dna').length, 0)
|
|
assert.ok(queryAll(m.container, '.lc-bar-up').length > 0, 'delta arms are back')
|
|
await m.unmount()
|
|
})
|
|
|
|
test('the trend and browser DNA toggles move as one', async () => {
|
|
const m = await mountRich('sv-dna-link')
|
|
const trendDna = () => buttonByText(m.container, DICT_EN['trend.dna'])
|
|
const browserDna = () => query(m.container, '.lc-br-dna-ctl .lc-gran-btn')
|
|
assert.ok(!trendDna().className.includes('lc-gran-on') && !browserDna().className.includes('lc-gran-on'), 'both off at mount')
|
|
|
|
await click(trendDna())
|
|
assert.ok(browserDna().className.includes('lc-gran-on'), 'the browser follows the trend toggle')
|
|
assert.equal(queryAll(m.container, '.lc-br-bar-dna').length, 1)
|
|
|
|
await click(browserDna())
|
|
assert.ok(!trendDna().className.includes('lc-gran-on'), 'the trend follows the browser toggle')
|
|
assert.equal(queryAll(m.container, '.lc-bar-dna').length, 0)
|
|
assert.equal(queryAll(m.container, '.lc-br-bar-dna').length, 0)
|
|
await m.unmount()
|
|
})
|
|
|
|
test('clicking a DNA band reveals that item in the Context browser', async () => { const m = await mountRich('sv-dna-reveal')
|
|
await click(buttonByText(m.container, DICT_EN['trend.dna']))
|
|
// Give the strip a real 112px stack area, then click its very top: the hit is the LAST band of
|
|
// bar seq 4 — the 'file output' tool result (system and the header lead the strip below it).
|
|
const dnaDiv = query(m.container, '.lc-bar[data-seq="4"] .lc-bar-dna')
|
|
dnaDiv.getBoundingClientRect = () => ({ top: 0, left: 0, right: 14, bottom: 112, width: 14, height: 112, x: 0, y: 0, toJSON: () => null }) as DOMRect
|
|
await act(async () => { dnaDiv.dispatchEvent(new MouseEvent('click', { bubbles: true, clientY: 0 })) })
|
|
await flush()
|
|
// The browser left the live surface for the bar's step and opened the node's row — the pin
|
|
// alone would only select the step (accordion reset), so the open row is the reveal.
|
|
assert.equal(query<HTMLSelectElement>(m.container, 'select.lc-br-pick').value, '4')
|
|
assert.ok(text(query(m.container, '.lc-br-elem-on')).includes('file output'))
|
|
await m.unmount()
|
|
})
|
|
|
|
test('delta mode pairs the detail with the previous record; first bar has none', async () => {
|
|
const m = await mountRich('sv-delta')
|
|
const chart = query(m.container, '.lc-chart')
|
|
await click(buttonByText(m.container, DICT_EN['gran.delta']))
|
|
// Default active bar is the newest: detail is the signed change vs its predecessor.
|
|
assert.ok(queryAll(m.container, '.lc-detail-tag').some(el => text(el) === DICT_EN['gran.delta']))
|
|
// Hovering the FIRST bar pairs it with null (change from zero).
|
|
await hover(query(m.container, '.lc-bar[data-seq="2"]'))
|
|
assert.ok(query(m.container, '.lc-bar[data-seq="2"]').className.includes('lc-bar-hovered'))
|
|
assert.ok(queryAll(m.container, '.lc-detail-tag').some(el => text(el) === DICT_EN['gran.delta']))
|
|
await unhover(chart)
|
|
await click(buttonByText(m.container, DICT_EN['gran.total']))
|
|
assert.ok(!queryAll(m.container, '.lc-detail-tag').some(el => text(el) === DICT_EN['gran.delta']))
|
|
await m.unmount()
|
|
})
|
|
|
|
test('event-kind filter narrows, unions, drops, and resets', async () => {
|
|
const m = await mountRich('sv-kinds')
|
|
const countEvents = () => queryAll(m.container, '.lc-event').length
|
|
const kindBtn = (k: string) => query(m.container, `.lc-kinds [data-kind="${k}"]`) as HTMLElement
|
|
assert.equal(countEvents(), 2)
|
|
// The buttons carry their per-kind tallies (richTimeline: 1 inject, 1 compaction, 0 prunes).
|
|
assert.deepEqual(
|
|
queryAll(m.container, '.lc-kinds .lc-kind-n').map(el => text(el)),
|
|
['1', '1', '0'],
|
|
)
|
|
|
|
await click(kindBtn('inject'))
|
|
assert.equal(countEvents(), 1)
|
|
await click(kindBtn('compaction'))
|
|
assert.equal(countEvents(), 2)
|
|
await click(kindBtn('compaction'))
|
|
assert.equal(countEvents(), 1)
|
|
await click(kindBtn('inject'))
|
|
assert.equal(countEvents(), 2)
|
|
await m.unmount()
|
|
})
|
|
})
|