94 lines
4.3 KiB
TypeScript
94 lines
4.3 KiB
TypeScript
|
|
// @vitest-environment jsdom
|
||
|
|
// ContextMeter (composer trailing control): occupancy ring gating, the
|
||
|
|
// click-open breakdown panel, and its close gestures.
|
||
|
|
|
||
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
||
|
|
import { cleanup, fireEvent, render } from '@testing-library/react'
|
||
|
|
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||
|
|
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||
|
|
import { ContextMeter, type ContextMeterProps } from '../src/client/skeleton/ContextMeter.tsx'
|
||
|
|
import css from '../src/client/skeleton/ContextMeter.module.css'
|
||
|
|
import { zh } from '../src/client/locales.ts'
|
||
|
|
|
||
|
|
afterEach(cleanup)
|
||
|
|
|
||
|
|
// Mirrors the real lookup chain (conversation namespace, then common).
|
||
|
|
const t = makeTranslate(zh, commonZh) as ContextMeterProps['t']
|
||
|
|
|
||
|
|
const BREAKDOWN = { systemTokens: 120, toolsTokens: 21_500, messageTokens: 477_000 }
|
||
|
|
|
||
|
|
const segmentClass = css.segment
|
||
|
|
if (segmentClass === undefined) throw new Error('segment class missing from ContextMeter.module.css')
|
||
|
|
|
||
|
|
/** Stub the projection seat: a key-addressed table of whole values. */
|
||
|
|
function projections(values: Record<string, unknown>): ContextMeterProps['useProjection'] {
|
||
|
|
return (key: string) => values[key]
|
||
|
|
}
|
||
|
|
|
||
|
|
function meter(values: Record<string, unknown>) {
|
||
|
|
return render(<ContextMeter useProjection={projections(values)} t={t} />)
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('ContextMeter', () => {
|
||
|
|
it('renders nothing until both pressure and capacity are known', () => {
|
||
|
|
expect(meter({}).container.textContent).toBe('')
|
||
|
|
expect(meter({ contextPressure: { pressureTokens: 32_000 } }).container.textContent).toBe('')
|
||
|
|
expect(meter({ contextPressure: { contextWindow: 128_000 } }).container.textContent).toBe('')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('shows the occupancy ring and opens the breakdown panel on click', () => {
|
||
|
|
const view = meter({
|
||
|
|
contextPressure: { pressureTokens: 32_000, contextWindow: 128_000 },
|
||
|
|
contextBreakdown: BREAKDOWN,
|
||
|
|
})
|
||
|
|
const trigger = view.getByRole('button', { name: '上下文已用 25%' })
|
||
|
|
expect(view.container.querySelector('[role="dialog"]')).toBeNull()
|
||
|
|
fireEvent.click(trigger)
|
||
|
|
const panel = view.container.querySelector('[role="dialog"]')!
|
||
|
|
expect(panel.textContent).toContain('~32K / 128K')
|
||
|
|
expect(panel.textContent).toContain('25%')
|
||
|
|
expect(panel.textContent).toContain('上下文已用')
|
||
|
|
expect(panel.textContent).toContain('系统提示词~120')
|
||
|
|
expect(panel.textContent).toContain('工具~21.5K')
|
||
|
|
expect(panel.textContent).toContain('对话消息~477K')
|
||
|
|
// The occupancy bar splits into one colored segment per composition row.
|
||
|
|
expect(panel.getElementsByClassName(segmentClass)).toHaveLength(3)
|
||
|
|
// Clicking the trigger again toggles the panel shut.
|
||
|
|
fireEvent.click(trigger)
|
||
|
|
expect(view.container.querySelector('[role="dialog"]')).toBeNull()
|
||
|
|
})
|
||
|
|
|
||
|
|
it('omits the composition rows while the contextBreakdown projection is absent', () => {
|
||
|
|
const view = meter({ contextPressure: { pressureTokens: 32_000, contextWindow: 128_000 } })
|
||
|
|
fireEvent.click(view.getByRole('button', { name: '上下文已用 25%' }))
|
||
|
|
const panel = view.container.querySelector('[role="dialog"]')!
|
||
|
|
expect(panel.textContent).toContain('~32K / 128K')
|
||
|
|
expect(panel.textContent).not.toContain('系统提示词')
|
||
|
|
expect(panel.textContent).not.toContain('对话消息')
|
||
|
|
// Without composition shares, the bar falls back to one plain segment.
|
||
|
|
expect(panel.getElementsByClassName(segmentClass)).toHaveLength(1)
|
||
|
|
})
|
||
|
|
|
||
|
|
it('closes on outside pointerdown and Escape — but not inside clicks', () => {
|
||
|
|
const view = meter({
|
||
|
|
contextPressure: { pressureTokens: 32_000, contextWindow: 128_000 },
|
||
|
|
contextBreakdown: BREAKDOWN,
|
||
|
|
})
|
||
|
|
const trigger = view.getByRole('button', { name: '上下文已用 25%' })
|
||
|
|
const openPanel = () => {
|
||
|
|
fireEvent.click(trigger)
|
||
|
|
return view.container.querySelector('[role="dialog"]')!
|
||
|
|
}
|
||
|
|
// A pointerdown inside the panel keeps it open; outside closes it.
|
||
|
|
const again = openPanel()
|
||
|
|
fireEvent.pointerDown(again)
|
||
|
|
expect(view.container.querySelector('[role="dialog"]')).not.toBeNull()
|
||
|
|
fireEvent.pointerDown(document.body)
|
||
|
|
expect(view.container.querySelector('[role="dialog"]')).toBeNull()
|
||
|
|
// Escape.
|
||
|
|
openPanel()
|
||
|
|
fireEvent.keyDown(document, { key: 'Escape' })
|
||
|
|
expect(view.container.querySelector('[role="dialog"]')).toBeNull()
|
||
|
|
})
|
||
|
|
})
|