The Overview face now plants the same quartile and full-span poles in turn granularity as in step granularity, labeled with the turn number each pole lands on (step poles keep the step's 1-based position). The shared pole classes become lc-trend-flag/lc-trend-flag-label since they carry either number in both faces. The turn strip's blocks grow by record span — the ribbon's own x unit — so one-record-per-turn aggregates stay equal width and the strip labels line up with the poles; step mode keeps its step-proportional blocks. A trimmed head keeps absolute turn numbers on both layers.
362 lines
21 KiB
TypeScript
362 lines
21 KiB
TypeScript
/**
|
|
* Note: the 'trend.empty' panel and the defaultGranularity/defaultTrendMode settings reads live in the PARENT
|
|
* (contextView.tsx); TrendChart itself always renders the chart frame, so the empty-history arm is asserted as an
|
|
* empty frame here.
|
|
*/
|
|
|
|
import { createElement as h } from 'react'
|
|
import assert from 'node:assert/strict'
|
|
import { describe, test } from 'vitest'
|
|
import { RISE_CAP, aggregateByTurn } from '../../../src/client/components/trendChart'
|
|
import { CATS } from '../../../src/client/categories'
|
|
import type { TrendBand } from '../../../src/client/dna'
|
|
import type { RequestRecord } from '../../../src/shared/types'
|
|
import { click, hover, mount, query, queryAll, unhover } from '../helpers/kit'
|
|
import { CHART_H, TrendChart, assertColor, bars, kit, makeSpies, propsOf, req } from './trendChartHarness'
|
|
|
|
describe('TrendChart empty history', () => {
|
|
test('renders the chart frame with no bars and no turn blocks', async () => {
|
|
// The 'trend.empty' placeholder is the parent's (contextView) render arm; with zero requests the chart itself
|
|
// renders an empty frame: unit axis (maxTotal floors at 1), empty scroll content, empty turn strip.
|
|
const m = await mount(h(TrendChart, propsOf([])))
|
|
assert.equal(bars(m.container).length, 0)
|
|
assert.equal(queryAll(m.container, '.lc-turn').length, 0)
|
|
assert.equal(query(m.container, '.lc-axis-top').textContent, '1')
|
|
assert.equal(query(m.container, '.lc-axis-q3').textContent, '1')
|
|
assert.equal(query(m.container, '.lc-axis-mid').textContent, '1')
|
|
assert.equal(query(m.container, '.lc-axis-q1').textContent, '0')
|
|
assert.equal(query(m.container, '.lc-axis-bot').textContent, '0')
|
|
assert.ok(query(m.container, '.lc-grid-mid'), 'total mode keeps the dashed mid grid')
|
|
assert.ok(query(m.container, '.lc-grid-q3'), 'total mode keeps the dashed quarter guides')
|
|
assert.ok(query(m.container, '.lc-grid-q1'), 'total mode keeps the dashed quarter guides')
|
|
assert.ok(query(m.container, '.lc-grid-zero'), 'total mode draws the solid zero baseline at the chart floor')
|
|
await m.unmount()
|
|
})
|
|
})
|
|
|
|
describe('TrendChart entrance rise cap', () => {
|
|
test('a log longer than the cap rises only over its newest columns', async () => {
|
|
const reqs: RequestRecord[] = []
|
|
for (let i = 0; i < RISE_CAP + 10; i++) reqs.push(req(i + 1, { turn: 1, step: i }))
|
|
const m = await mount(h(TrendChart, propsOf(reqs)))
|
|
const stacks = queryAll(m.container, '.lc-bar-stack')
|
|
assert.equal(stacks.length, RISE_CAP + 10)
|
|
const risers = stacks.filter(s => s.className.includes('animate-lc-bar-in'))
|
|
assert.equal(risers.length, RISE_CAP)
|
|
// The window is what rises: every column from its edge on, none before it.
|
|
assert.deepEqual(stacks.slice(0, 10).map(s => s.className.includes('animate-lc-bar-in')), new Array<boolean>(10).fill(false))
|
|
assert.deepEqual(stacks.slice(10).map(s => s.className.includes('animate-lc-bar-in')), new Array<boolean>(RISE_CAP).fill(true))
|
|
// The window's own cascade counts from its edge — raw indices would all sit past the stagger cap and rise in unison.
|
|
assert.equal(stacks[10].style.getPropertyValue('--lc-i'), '0')
|
|
await m.unmount()
|
|
})
|
|
|
|
test('a log that fits the cap keeps its full cascade', async () => {
|
|
const reqs = [req(1, { turn: 1, step: 0 }), req(2, { turn: 1, step: 1 }), req(3, { turn: 2, step: 0 })]
|
|
const m = await mount(h(TrendChart, propsOf(reqs)))
|
|
assert.deepEqual(queryAll(m.container, '.lc-bar-stack').map(s => s.className.includes('animate-lc-bar-in')), [true, true, true])
|
|
await m.unmount()
|
|
})
|
|
|
|
test('the DNA and DNA+delta interiors obey the same cap', async () => {
|
|
// A band that grows by one token per bar, so the DNA+delta arms exist for every bar but the first.
|
|
const reqs: RequestRecord[] = []
|
|
const bands: TrendBand[][] = []
|
|
for (let i = 0; i < RISE_CAP + 5; i++) {
|
|
const tokens = 100 + i
|
|
reqs.push(req(i + 1, { turn: 1, step: i, total: tokens }))
|
|
bands.push([{ key: 'sys', cat: 'system', tokens, off: 0, color: '#123456' } as TrendBand])
|
|
}
|
|
const total = await mount(h(TrendChart, propsOf(reqs, { dna: bands })))
|
|
const totalDivs = queryAll(total.container, '.lc-bar-dna')
|
|
assert.equal(totalDivs.length, RISE_CAP + 5)
|
|
assert.equal(totalDivs.filter(d => d.className.includes('animate-lc-bar-in')).length, RISE_CAP)
|
|
await total.unmount()
|
|
// The first bar carries no baseline, so its delta arm renders nothing at all.
|
|
const delta = await mount(h(TrendChart, propsOf(reqs, { dna: bands, mode: 'delta' })))
|
|
const deltaDivs = queryAll(delta.container, '.lc-bar-dna')
|
|
assert.equal(deltaDivs.length, RISE_CAP + 4)
|
|
assert.equal(deltaDivs.filter(d => d.className.includes('animate-lc-bar-in')).length, RISE_CAP)
|
|
await delta.unmount()
|
|
})
|
|
})
|
|
|
|
describe('TrendChart step granularity, total mode', () => {
|
|
test('stacks per-category segments in CATS order with real colors and proportional px heights', async () => {
|
|
const r1 = req(1, { turn: 1, step: 0, skill: 40, total: 340 })
|
|
const r2 = req(2, { turn: 1, step: 1, system: 200, tools: 100, user: 60, inject: 40, skill: 60, assistant: 80, tool: 120, total: 660 })
|
|
const r3 = req(3, { turn: undefined, step: 0, user: 0, total: 280 })
|
|
const m = await mount(h(TrendChart, propsOf([r1, r2, r3])))
|
|
|
|
const bs = bars(m.container)
|
|
assert.equal(bs.length, 3)
|
|
assert.deepEqual(bs.map(b => b.getAttribute('data-seq')), ['1', '2', '3'])
|
|
|
|
// Seven priced categories, in CATS order, with the shipped colors; max bar segments scale against maxTotal=660.
|
|
const segs1 = queryAll(bs[0], '.lc-bar-stack > div')
|
|
assert.equal(segs1.length, 7)
|
|
for (let i = 0; i < CATS.length; i++) assertColor(segs1[i].style.background, CATS[i].color)
|
|
assert.equal(segs1[0].style.height, `${Math.round(100 / 660 * CHART_H)}px`)
|
|
assert.equal(segs1[4].style.height, `${Math.round(40 / 660 * CHART_H)}px`)
|
|
assert.equal(segs1[6].style.height, `${Math.round(60 / 660 * CHART_H)}px`)
|
|
const segs2 = queryAll(bs[1], '.lc-bar-stack > div')
|
|
assert.equal(segs2[0].style.height, `${Math.round(200 / 660 * CHART_H)}px`)
|
|
|
|
// Zero-value categories are skipped entirely (r3.user = 0 → five segments, no user-green segment).
|
|
const segs3 = queryAll(bs[2], '.lc-bar-stack > div')
|
|
assert.equal(segs3.length, 5)
|
|
assert.ok(![...segs3].some(s => s.style.background.includes('var(--color-green-500)')))
|
|
|
|
assert.equal(query(m.container, '.lc-axis-top').textContent, '660')
|
|
assert.equal(query(m.container, '.lc-axis-q3').textContent, '495')
|
|
assert.equal(query(m.container, '.lc-axis-mid').textContent, '330')
|
|
assert.equal(query(m.container, '.lc-axis-q1').textContent, '165')
|
|
assert.equal(query(m.container, '.lc-axis-bot').textContent, '0')
|
|
|
|
// Turn strip: turn 1 spans two step columns (2*16-2 = 30px), the turnless request lands in group 0 (14px);
|
|
// zebra fills alternate and stay disjoint from the category palette.
|
|
const turns = queryAll(m.container, '.lc-turn')
|
|
assert.equal(turns.length, 2)
|
|
assert.equal(turns[0].style.width, '30px')
|
|
assert.equal(turns[1].style.width, '14px')
|
|
assert.ok(turns[0].style.background.includes('var(--color-neutral-500) 12%'))
|
|
assert.ok(turns[1].style.background.includes('var(--color-neutral-500) 26%'))
|
|
assert.deepEqual(turns.map(t => t.textContent), ['1', '0'])
|
|
await m.unmount()
|
|
})
|
|
|
|
test('plots raw fold figures: the provider prompt never rescales the stack, so constant categories stay flat', async () => {
|
|
const composition = { system: 200, tools: 100, user: 60, inject: 40, assistant: 80, tool: 120, total: 600 }
|
|
const r1 = req(1, { turn: 1, step: 0, ...composition })
|
|
const r2 = req(2, { turn: 1, step: 1, ...composition, prompt: 1200 })
|
|
const r3 = req(3, { turn: 2, step: 0, system: 0, tools: 0, user: 0, inject: 0, assistant: 0, tool: 0, total: 0, prompt: 500 })
|
|
const m = await mount(h(TrendChart, propsOf([r1, r2, r3])))
|
|
|
|
// maxTotal follows the heuristic totals (600), not the provider prompt (1200).
|
|
assert.equal(query(m.container, '.lc-axis-top').textContent, '600')
|
|
const bs = bars(m.container)
|
|
assert.equal(bs.length, 3)
|
|
// Identical compositions plot IDENTICAL segment heights regardless of the reported prompt — a constant
|
|
// system prompt must not ride the provider/heuristic ratio (that per-request rescale faked growth into
|
|
// categories the fold never changed).
|
|
const segs1 = queryAll(bs[0], '.lc-bar-stack > div')
|
|
const segs2 = queryAll(bs[1], '.lc-bar-stack > div')
|
|
assert.equal(segs1.length, 6)
|
|
assert.deepEqual(segs2.map(s => s.style.height), segs1.map(s => s.style.height))
|
|
assert.equal(segs1[0].style.height, `${Math.round(200 / 600 * CHART_H)}px`)
|
|
// A usage-only record (prompt without messages) still renders no segments.
|
|
assert.equal(queryAll(bs[2], '.lc-bar-stack > div').length, 0)
|
|
|
|
// The tip is placed over its bar's visible slice imperatively (transform, not `left`), so it never
|
|
// contributes to the scroller's overflow — see the overlay test below.
|
|
const { spies, handlers } = makeSpies()
|
|
await m.update(h(TrendChart, { ...propsOf([r1, r2, r3]), ...handlers, hoveredSeq: 2 }))
|
|
const tip = query(m.container, '.lc-chart-tip')
|
|
// Rows: identity, then the SAME total the bar is drawn against (the heuristic 600, not the prompt 1200).
|
|
assert.deepEqual(
|
|
queryAll(tip, 'span').map(r => r.textContent),
|
|
[kit.t('tip.step', { t: 1, s: 1, n: 2 }), kit.t('tip.total', { n: '600' })],
|
|
)
|
|
assert.equal(tip.style.transform, 'translate(23px, 0)') // idx 1 * 16 + BAR_W/2, scrollLeft 0
|
|
assert.ok(spies.hover.length === 0, 'hover callback only fires from real mouseover')
|
|
await m.unmount()
|
|
})
|
|
|
|
test('a zero-only history keeps the unit scale (maxTotal floors at 1)', async () => {
|
|
const zero = req(1, { turn: 1, step: 0, system: 0, tools: 0, user: 0, inject: 0, assistant: 0, tool: 0, total: 0 })
|
|
const m = await mount(h(TrendChart, propsOf([zero])))
|
|
assert.equal(query(m.container, '.lc-axis-top').textContent, '1')
|
|
assert.equal(queryAll(bars(m.container)[0], '.lc-bar-stack > div').length, 0)
|
|
await m.unmount()
|
|
})
|
|
})
|
|
|
|
describe('TrendChart delta mode', () => {
|
|
const base = req(1, { turn: 1, step: 0 })
|
|
const grown = req(2, { turn: 1, step: 1, system: 110, tools: 60, user: 40, inject: 30, assistant: 50, tool: 70, total: 360 })
|
|
const shrunk = req(3, { turn: 2, step: 0, system: 90, tools: 40, user: 20, inject: 10, assistant: 30, tool: 50, total: 240 })
|
|
const zeroReq = req(9, { turn: 3, step: 0, system: 0, tools: 0, user: 0, inject: 0, assistant: 0, tool: 0, total: 0 })
|
|
|
|
test('diverging stacks pile positive deltas up and hang negatives down off a solid zero line', async () => {
|
|
const m = await mount(h(TrendChart, propsOf([base, grown, shrunk], { mode: 'delta' })))
|
|
|
|
// maxUp=60 (grown: +10 x6), maxDown=120 (shrunk: -20 x6) → scale 112/180, zero line at upPx=37.
|
|
assert.equal(query(m.container, '.lc-axis-top').textContent, '+60')
|
|
// Quarter marks sit on the uniform scale: +15 above (height ¼), -75 below (height ¾). The +15 mark's
|
|
// label box (top 41) is 9px from the zero label's (13+37=50) — under one 11px label height — so it
|
|
// drops itself instead of overlapping the zero reference; the -75 mark stays.
|
|
assert.equal(queryAll(m.container, '.lc-axis-q3').length, 0)
|
|
assert.equal(query(m.container, '.lc-axis-q1').textContent, '-75')
|
|
assert.equal(query(m.container, '.lc-axis-mid').textContent, '0')
|
|
assert.equal(query(m.container, '.lc-axis-mid').style.top, `${13 + 37}px`)
|
|
assert.equal(query(m.container, '.lc-axis-bot').textContent, '-120')
|
|
assert.equal(query(m.container, '.lc-grid-zero').style.top, `${18 + 37}px`)
|
|
// Each surviving quarter mark keeps its dashed guide; the mark that yielded to the zero label drops it too.
|
|
assert.equal(queryAll(m.container, '.lc-grid-q3').length, 0)
|
|
assert.ok(query(m.container, '.lc-grid-q1'))
|
|
|
|
const bs = bars(m.container)
|
|
assert.equal(queryAll(bs[0], '.lc-bar-up > div').length, 0)
|
|
assert.equal(queryAll(bs[0], '.lc-bar-down > div').length, 0)
|
|
const up2 = queryAll(bs[1], '.lc-bar-up > div')
|
|
assert.equal(up2.length, 6)
|
|
assert.equal(up2[0].style.height, `${Math.round(10 * CHART_H / 180)}px`)
|
|
assertColor(up2[0].style.background, CATS[0].color)
|
|
assert.equal(queryAll(bs[1], '.lc-bar-down > div').length, 0)
|
|
const down3 = queryAll(bs[2], '.lc-bar-down > div')
|
|
assert.equal(down3.length, 6)
|
|
assert.equal(down3[0].style.height, `${Math.round(20 * CHART_H / 180)}px`)
|
|
assert.equal(queryAll(bs[2], '.lc-bar-up > div').length, 0)
|
|
// The up stack rides downPx up from the bottom, the down stack hangs upPx from the zero line.
|
|
assert.equal(query(bs[1], '.lc-bar-up').style.bottom, '75px')
|
|
assert.equal(query(bs[2], '.lc-bar-down').style.top, '37px')
|
|
|
|
// Delta tooltip: signed net change on the metric row, '+' only for positive nets.
|
|
await m.update(h(TrendChart, propsOf([base, grown, shrunk], { mode: 'delta', hoveredSeq: 2 })))
|
|
assert.deepEqual(
|
|
queryAll(query(m.container, '.lc-chart-tip'), 'span').map(r => r.textContent),
|
|
[kit.t('tip.step', { t: 1, s: 1, n: 2 }), kit.t('tip.delta', { n: '+60' })],
|
|
)
|
|
await m.update(h(TrendChart, propsOf([base, grown, shrunk], { mode: 'delta', hoveredSeq: 3 })))
|
|
assert.ok(query(m.container, '.lc-chart-tip').textContent!.includes(kit.t('tip.delta', { n: '-120' })))
|
|
await m.update(h(TrendChart, propsOf([base, grown, shrunk], { mode: 'delta', hoveredSeq: 1 })))
|
|
assert.ok(query(m.container, '.lc-chart-tip').textContent!.includes(kit.t('tip.delta', { n: '0' })))
|
|
await m.unmount()
|
|
})
|
|
|
|
test('growth-only history zeroes the negative axis arm; shrink-only zeroes the positive arm', async () => {
|
|
const up = await mount(h(TrendChart, propsOf([zeroReq, base], { mode: 'delta' })))
|
|
assert.equal(query(up.container, '.lc-axis-top').textContent, '+300')
|
|
// Zero line pinned to the chart bottom (upPx 112) clears both quarter marks: +225 / +75.
|
|
assert.equal(query(up.container, '.lc-axis-q3').textContent, '+225')
|
|
assert.equal(query(up.container, '.lc-axis-q1').textContent, '+75')
|
|
assert.equal(query(up.container, '.lc-axis-bot').textContent, '0')
|
|
assert.ok(query(up.container, '.lc-grid-q3'))
|
|
assert.ok(query(up.container, '.lc-grid-q1'))
|
|
const upSegs = queryAll(bars(up.container)[1], '.lc-bar-up > div')
|
|
assert.equal(upSegs.length, 6)
|
|
assert.equal(upSegs[0].style.height, `${Math.round(100 * CHART_H / 300)}px`)
|
|
await up.unmount()
|
|
|
|
const down = await mount(h(TrendChart, propsOf([base, zeroReq], { mode: 'delta' })))
|
|
// Zero line pinned to the chart top (upPx 0): the quarter marks read -75 / -225.
|
|
assert.equal(query(down.container, '.lc-axis-top').textContent, '0')
|
|
assert.equal(query(down.container, '.lc-axis-q3').textContent, '-75')
|
|
assert.equal(query(down.container, '.lc-axis-q1').textContent, '-225')
|
|
assert.equal(query(down.container, '.lc-axis-bot').textContent, '-300')
|
|
assert.ok(query(down.container, '.lc-grid-q3'))
|
|
assert.ok(query(down.container, '.lc-grid-q1'))
|
|
assert.equal(queryAll(bars(down.container)[1], '.lc-bar-down > div').length, 6)
|
|
await down.unmount()
|
|
})
|
|
|
|
test('a quarter mark landing on the zero label drops itself; the opposite mark keeps its place', async () => {
|
|
// maxUp=90 (+90 system), maxDown=30 (-30 tools) → upPx 84: the zero label (top 97) sits exactly on the
|
|
// ¼-height mark (top 97), which drops itself; the ¾-height mark renders +60 well clear of it.
|
|
const flat = req(1, { turn: 1, step: 0, system: 0, tools: 30, user: 0, inject: 0, assistant: 0, tool: 0, total: 30 })
|
|
const mixed = req(2, { turn: 1, step: 1, system: 90, tools: 0, user: 0, inject: 0, assistant: 0, tool: 0, total: 90 })
|
|
const m = await mount(h(TrendChart, propsOf([flat, mixed], { mode: 'delta' })))
|
|
assert.equal(query(m.container, '.lc-axis-top').textContent, '+90')
|
|
assert.equal(query(m.container, '.lc-axis-q3').textContent, '+60')
|
|
assert.equal(queryAll(m.container, '.lc-axis-q1').length, 0)
|
|
assert.equal(query(m.container, '.lc-axis-mid').textContent, '0')
|
|
assert.equal(query(m.container, '.lc-axis-mid').style.top, `${13 + 84}px`)
|
|
assert.equal(query(m.container, '.lc-axis-bot').textContent, '-30')
|
|
assert.ok(query(m.container, '.lc-grid-q3'))
|
|
assert.equal(queryAll(m.container, '.lc-grid-q1').length, 0)
|
|
await m.unmount()
|
|
})
|
|
})
|
|
|
|
describe('TrendChart turn granularity', () => {
|
|
const t1s0 = req(1, { turn: 1, step: 0 })
|
|
const t1s1 = req(2, { turn: 1, step: 1, total: 360 })
|
|
const t2s0 = req(3, { turn: 2, step: 0, total: 280 })
|
|
|
|
test('turn aggregates render one bar per turn with aggregated labels and turn-count tooltips', async () => {
|
|
const agg = aggregateByTurn([t1s0, t1s1, t2s0])
|
|
const m = await mount(h(TrendChart, propsOf(agg, { granularity: 'turn' })))
|
|
const bs = bars(m.container)
|
|
assert.equal(bs.length, 2)
|
|
const turns = queryAll(m.container, '.lc-turn')
|
|
assert.equal(turns.length, 2)
|
|
// Aggregated groups always occupy exactly one column.
|
|
assert.deepEqual(turns.map(t => t.style.width), ['14px', '14px'])
|
|
assert.deepEqual(turns.map(t => t.textContent), ['1', '2'])
|
|
|
|
// Multi-step aggregate → tip.turn; a single-step aggregate still speaks TURN (the step-count wording), never the step index.
|
|
await m.update(h(TrendChart, propsOf(agg, { granularity: 'turn', hoveredSeq: t1s1.seq })))
|
|
assert.ok(query(m.container, '.lc-chart-tip').textContent!.includes(kit.t('tip.turn', { t: 1, n: 2 })))
|
|
await m.update(h(TrendChart, propsOf(agg, { granularity: 'turn', hoveredSeq: t2s0.seq })))
|
|
assert.ok(query(m.container, '.lc-chart-tip').textContent!.includes(kit.t('tip.turn1', { t: 2 })))
|
|
await m.unmount()
|
|
|
|
// A multi-step aggregate of TURNLESS requests reports turn 0.
|
|
const turnless = aggregateByTurn([req(10, { turn: undefined }), req(11, { turn: undefined })])
|
|
const m2 = await mount(h(TrendChart, propsOf(turnless, { granularity: 'turn', hoveredSeq: 11 })))
|
|
assert.ok(query(m2.container, '.lc-chart-tip').textContent!.includes(kit.t('tip.turn', { t: 0, n: 2 })))
|
|
await m2.unmount()
|
|
|
|
// A turn-mode record missing stepCount (the parent always aggregates, so this is defensive) degrades to 1 step;
|
|
// a missing turn field degrades to turn 0 the same way.
|
|
const m3 = await mount(h(TrendChart, propsOf([req(20, { turn: undefined, step: 3 })], { granularity: 'turn', hoveredSeq: 20 })))
|
|
assert.ok(query(m3.container, '.lc-chart-tip').textContent!.includes(kit.t('tip.turn1', { t: 0 })))
|
|
await m3.unmount()
|
|
})
|
|
|
|
test('turn strip hover/click drive the turn callbacks; activeTurn dims the chart and lights the block', async () => {
|
|
const { spies, handlers } = makeSpies()
|
|
const agg = aggregateByTurn([t1s0, t1s1, t2s0])
|
|
const m = await mount(h(TrendChart, propsOf(agg, { ...handlers, granularity: 'turn' })))
|
|
|
|
const turns = queryAll(m.container, '.lc-turn')
|
|
await hover(turns[0])
|
|
assert.deepEqual(spies.hoverTurn, [1])
|
|
await click(turns[1])
|
|
assert.deepEqual(spies.pickTurn, [2])
|
|
await unhover(turns[0])
|
|
assert.deepEqual(spies.hoverTurn, [1, null])
|
|
|
|
await m.update(h(TrendChart, propsOf(agg, { ...handlers, granularity: 'turn', activeTurn: 1 })))
|
|
assert.ok(query(m.container, '.lc-chart-scroll').className.includes('lc-chart-dim'))
|
|
const bs = bars(m.container)
|
|
assert.ok(bs[0].className.includes('lc-bar-in-turn'))
|
|
assert.ok(!bs[1].className.includes('lc-bar-in-turn'))
|
|
assert.ok(queryAll(m.container, '.lc-turn')[0].className.includes('lc-turn-on'))
|
|
assert.ok(!queryAll(m.container, '.lc-turn')[1].className.includes('lc-turn-on'))
|
|
|
|
await m.update(h(TrendChart, propsOf(agg, { ...handlers, granularity: 'turn', activeTurn: null })))
|
|
assert.ok(!query(m.container, '.lc-chart-scroll').className.includes('lc-chart-dim'))
|
|
assert.ok(!bars(m.container)[0].className.includes('lc-bar-in-turn'))
|
|
await m.unmount()
|
|
})
|
|
})
|
|
|
|
describe('TrendChart step flags', () => {
|
|
test('every 5th step bar plants a flag labeled with its cumulative step number', async () => {
|
|
const reqs = Array.from({ length: 12 }, (_, i) => req(i + 1, { turn: 1, step: i }))
|
|
const m = await mount(h(TrendChart, propsOf(reqs)))
|
|
const flags = queryAll(m.container, '.lc-trend-flag')
|
|
assert.equal(flags.length, 2)
|
|
// Single- and multi-digit labels alike; the flag rides its bar (5th and 10th columns).
|
|
assert.deepEqual(flags.map(f => f.textContent), ['5', '10'])
|
|
const bs = bars(m.container)
|
|
assert.equal(queryAll(bs[0], '.lc-trend-flag').length, 0)
|
|
assert.ok(query(bs[4], '.lc-trend-flag'))
|
|
assert.ok(query(bs[9], '.lc-trend-flag'))
|
|
await m.unmount()
|
|
})
|
|
|
|
test('delta mode keeps the flags; turn granularity plants none (the turn strip numbers that grid)', async () => {
|
|
const reqs = Array.from({ length: 6 }, (_, i) => req(i + 1, { turn: 1, step: i }))
|
|
const m = await mount(h(TrendChart, propsOf(reqs, { mode: 'delta' })))
|
|
assert.deepEqual(queryAll(m.container, '.lc-trend-flag').map(f => f.textContent), ['5'])
|
|
await m.unmount()
|
|
|
|
const turns = Array.from({ length: 6 }, (_, i) => req(i + 1, { turn: i + 1, step: 0 }))
|
|
const m2 = await mount(h(TrendChart, propsOf(aggregateByTurn(turns), { granularity: 'turn' })))
|
|
assert.equal(queryAll(m2.container, '.lc-trend-flag').length, 0)
|
|
await m2.unmount()
|
|
})
|
|
})
|