mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-10 17:14:48 +00:00
* fix(app,dash): render pre-history days as no data, not a currency zero Days before the first recorded day in history.daily were painted as a real 0.00/0 calls in the daily activity heatmap and the daily spend charts. That zero is unknown, not measured, so those cells and bars now read "No data recorded" instead of a currency zero. Genuinely idle days within recorded history stay as real zeros. - app: heatmap cells, the daily spend bars, and the daily-by-model columns get a distinct no-data style plus a "No data recorded" hover for days before the first recorded day. - dash: the granular line chart drops buckets before the first recorded day so no flat zero line is drawn before any history exists. - data-start is derived in the UI layer from history.daily; payload shape is unchanged. Verified: app vitest 418 pass (7 new for the no-data behavior); app, dash, and root typecheck clean; dash and app renderer vite builds succeed. * no-data: cap guard, timezone-free dash trim, StackedBars aria Three hardening fixes from adversarial review of the no-data rendering: dataStartKey returns null at the payload's 365-entry history cap, where the oldest retained entry is no longer the true data start; classifying past it would label real aged-out history as no data on long custom ranges. Documented that the install-to-first-use gap reading as no data is literally accurate: nothing was recorded then either. The dash granular chart now trims leading zero-only buckets by value instead of comparing bucket timestamps against date keys, so producer and viewer timezone skew can never drop a real first-day bucket, and an all-zero series lands in the established empty state. StackedBars no-data columns expose their state via aria-label, not only a title on a non-focusable div.
78 lines
3.1 KiB
TypeScript
78 lines
3.1 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { render } from '@testing-library/react'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import type { DailyHistoryEntry } from '../lib/types'
|
|
import { StackedBars } from './StackedBars'
|
|
|
|
function entry(day: number): DailyHistoryEntry {
|
|
return {
|
|
date: `2026-07-${String(day).padStart(2, '0')}`,
|
|
cost: day,
|
|
savingsUSD: 0,
|
|
calls: 1,
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
cacheReadTokens: 0,
|
|
cacheWriteTokens: 0,
|
|
topModels: [],
|
|
}
|
|
}
|
|
|
|
describe('StackedBars', () => {
|
|
it('renders every supplied day and axis ticks every fourth day plus the last', () => {
|
|
const daily = Array.from({ length: 16 }, (_, index) => entry(index + 1))
|
|
const { container } = render(<StackedBars daily={daily} />)
|
|
|
|
expect(container.querySelectorAll('.sbars .c')).toHaveLength(16)
|
|
const ticks = container.querySelectorAll('.sbars-wrap > .ov-xax span')
|
|
expect([...ticks].map(tick => tick.textContent)).toEqual(['Jul 1', 'Jul 5', 'Jul 9', 'Jul 13', 'Jul 16'])
|
|
})
|
|
|
|
it('renders days before recorded history as no data, not a $0.00 column', () => {
|
|
// Zero-filled window spanning a pre-history day and the first recorded day.
|
|
const daily = [
|
|
{ ...entry(23), cost: 0, calls: 0 },
|
|
entry(24),
|
|
]
|
|
const { container } = render(<StackedBars daily={daily} dataStart="2026-07-24" />)
|
|
|
|
const columns = container.querySelectorAll('.sbars .c')
|
|
expect(columns[0]).toHaveClass('nodata')
|
|
expect(columns[0]).toHaveAttribute('title', '2026-07-23 · No data recorded')
|
|
expect(columns[0].querySelector('.nodata-mark')).toBeInTheDocument()
|
|
expect(columns[0].querySelectorAll('.s')).toHaveLength(0)
|
|
|
|
expect(columns[1]).not.toHaveClass('nodata')
|
|
expect(columns[1].getAttribute('title')).toContain('$24.00')
|
|
})
|
|
|
|
it('leaves a genuinely idle day within history as an empty column, not no data', () => {
|
|
const daily = [entry(24), { ...entry(25), cost: 0, calls: 0 }]
|
|
const { container } = render(<StackedBars daily={daily} dataStart="2026-07-24" />)
|
|
|
|
const columns = container.querySelectorAll('.sbars .c')
|
|
expect(columns[1]).not.toHaveClass('nodata')
|
|
expect(columns[1].getAttribute('title')).toBe('2026-07-25 · $0.00')
|
|
expect(columns[1].querySelector('.nodata-mark')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('draws a single cost-only fallback bar and a provider legend when a day has cost but no model breakdown', () => {
|
|
// Provider-filtered days: cost present, topModels empty (the Swift menubar
|
|
// draws these from day.cost). A zero-cost day stays empty.
|
|
const daily = [
|
|
{ ...entry(9), cost: 0 },
|
|
{ ...entry(10), cost: 12 },
|
|
]
|
|
const { container } = render(<StackedBars daily={daily} fallbackLabel="Claude" />)
|
|
|
|
const columns = container.querySelectorAll('.sbars .c')
|
|
expect(columns[0].querySelectorAll('.s')).toHaveLength(0)
|
|
expect(columns[1].querySelectorAll('.s')).toHaveLength(1)
|
|
expect(columns[1].querySelector('.s-other')).toBeInTheDocument()
|
|
|
|
const legend = container.querySelector('.legend')!
|
|
expect(legend.querySelectorAll('span')).toHaveLength(1)
|
|
expect(legend).toHaveTextContent('Claude')
|
|
})
|
|
})
|