Files
dsh-context/tests/client/components/overviewButton.spec.ts
T
bowenliang123 c463620b71 feat(client): add a per-user toggle for the Context Insights sidebar entry
New insightsEntry field ('show' | 'hide', default 'show') in the
dsh-context settings namespace, edited from the Plugin configuration
card. The sidebar entry subscribes to the store and renders null while
hidden; the seat stays registered so flipping back needs no remount.

Fails open everywhere: a schema-less host, an unserved namespace, a
section without the field, a stale persisted value (loose union), or a
write that never persists all resolve to the visible entry.
2026-09-16 21:22:10 +08:00

111 lines
5.1 KiB
TypeScript

// The Context Dashboard's sidebar-foot entry (src/client/components/
// overviewButton.tsx): wide/rail rendering, the running badge, the store
// flip on click, and the per-user insights-entry toggle.
import { createElement as h, act } from 'react'
import assert from 'node:assert/strict'
import { afterEach, describe, test } from 'vitest'
import { makeOverviewButton } from '../../../src/client/components/overviewButton'
import { overviewStore } from '../../../src/client/overviewStore'
import { createContextSettings } from '../../../src/client/settings'
import { click, makeKit, mount, query, queryAll } from '../helpers/kit'
const kit = makeKit()
const Button = makeOverviewButton(kit)
afterEach(() => {
overviewStore.set(false)
})
/** A useSessions stand-in over a static snapshot. */
const useSessionsOf = (snapshot: unknown) => (<T>(sel: (s: unknown) => T): T => sel(snapshot))
describe('OverviewButton', () => {
test('the wide column renders icon and label; the rail renders the icon alone', async () => {
const wide = await mount(h(Button, { wide: true }))
assert.equal(query(wide.container, '.lc-ov-entry-label').textContent, 'Context Insights')
assert.equal(query(wide.container, 'button.lc-ov-entry').getAttribute('aria-label'), 'Context Insights')
assert.equal(queryAll(wide.container, '.lc-ov-entry-rail').length, 0)
await wide.unmount()
const rail = await mount(h(Button, { wide: false }))
assert.equal(queryAll(rail.container, '.lc-ov-entry-label').length, 0)
assert.ok(query(rail.container, '.lc-ov-entry-icon'))
assert.ok(query(rail.container, 'button.lc-ov-entry-rail'))
await rail.unmount()
// An absent wide flag (a foreign owner) keeps the label hidden.
const bare = await mount(h(Button, {}))
assert.equal(queryAll(bare.container, '.lc-ov-entry-label').length, 0)
assert.ok(query(bare.container, 'button.lc-ov-entry-rail'))
await bare.unmount()
})
test('the badge counts running sessions and hides at zero', async () => {
const running = useSessionsOf({ byId: { a: { running: true }, b: { running: false }, c: { running: true } } })
const m = await mount(h(Button, { wide: true, useSessions: running }))
assert.equal(query(m.container, '.lc-ov-badge').textContent, '2')
await m.unmount()
const idle = useSessionsOf({ byId: { a: { running: false } } })
const calm = await mount(h(Button, { wide: true, useSessions: idle }))
assert.equal(queryAll(calm.container, '.lc-ov-badge').length, 0)
await calm.unmount()
// No sessions seat at all: no badge, no throw.
const seatless = await mount(h(Button, { wide: true }))
assert.equal(queryAll(seatless.container, '.lc-ov-badge').length, 0)
await seatless.unmount()
})
test('clicking opens the overview through the shared store', async () => {
assert.equal(overviewStore.getSnapshot(), false)
const m = await mount(h(Button, { wide: true }))
await click(query(m.container, 'button.lc-ov-entry'))
assert.equal(overviewStore.getSnapshot(), true)
await m.unmount()
})
test('the zh locale renders the translated label', async () => {
const ZhButton = makeOverviewButton(makeKit('zh'))
const m = await mount(h(ZhButton, { wide: true }))
assert.equal(query(m.container, '.lc-ov-entry-label').textContent, '上下文洞察')
await m.unmount()
})
test('the insights-entry toggle takes the button down and back live', async () => {
// The real store: a local echo flips the rendering without a scope.
const settings = createContextSettings()
const Toggled = makeOverviewButton(kit, settings)
const m = await mount(h(Toggled, { wide: true }))
assert.ok(query(m.container, 'button.lc-ov-entry'), 'the default (show) renders the entry')
await act(async () => { settings.set('insightsEntry', 'hide') })
assert.equal(queryAll(m.container, 'button.lc-ov-entry').length, 0, 'hide renders nothing')
await act(async () => { settings.set('insightsEntry', 'show') })
assert.ok(query(m.container, 'button.lc-ov-entry'), 'flipping back restores the entry')
await m.unmount()
})
test('an unpersisted hide (a rejected write) rolls back to a visible entry', async () => {
// The fail-open contract, end to end: a settings store whose scope died
// before the write landed must not leave the entry hidden. The rejection
// is held back so the optimistic echo is observable on its own.
const settings = createContextSettings()
const Toggled = makeOverviewButton(kit, settings)
let rejectSet: ((err: unknown) => void) | undefined
settings.attach({
getSnapshot: () => ({ status: 'ready', value: {}, writable: true }),
subscribe: () => () => {},
set: () => new Promise((_, reject) => { rejectSet = reject }),
})
const m = await mount(h(Toggled, { wide: true }))
await act(async () => { settings.set('insightsEntry', 'hide') })
assert.equal(queryAll(m.container, 'button.lc-ov-entry').length, 0, 'the optimistic echo hides first')
await act(async () => { rejectSet?.(new Error('transport down')) })
assert.ok(query(m.container, 'button.lc-ov-entry'), 'the rollback restores the entry')
await m.unmount()
})
})