codeburn/app/renderer/components/ActivityHeatmap.test.tsx
Resham Joshi dbd93fee51
app,dash: render pre-history days as no data, not a currency zero (#765)
* 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.
2026-07-20 08:03:24 -07:00

50 lines
2.4 KiB
TypeScript

// @vitest-environment jsdom
import { fireEvent, render } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { DailyHistoryEntry } from '../lib/types'
import { ActivityHeatmap } from './ActivityHeatmap'
function entry(date: string, cost: number, calls: number): DailyHistoryEntry {
return { date, cost, savingsUSD: 0, calls, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, topModels: [] }
}
// A fixed "now" so the 26-week window and the day-under-test are deterministic.
beforeEach(() => vi.setSystemTime(new Date(2026, 6, 20, 12, 0, 0)))
afterEach(() => vi.useRealTimers())
describe('ActivityHeatmap no-data days (before recorded history)', () => {
// History begins 2026-07-10; earlier days predate any recorded data.
const daily = [entry('2026-07-10', 4, 20), entry('2026-07-15', 6, 30)]
it('marks days before the first recorded day as no data, not a currency zero', () => {
const { container } = render(<ActivityHeatmap daily={daily} />)
const preData = container.querySelector('[data-date="2026-07-05"]')!
expect(preData).toHaveClass('nodata')
expect(preData).toHaveAttribute('data-active', 'false')
expect(preData.getAttribute('aria-label')).toContain('no data recorded')
expect(preData.getAttribute('aria-label')).not.toContain('$0.00')
})
it('keeps a genuinely idle day within recorded history as a real zero', () => {
const { container } = render(<ActivityHeatmap daily={daily} />)
const idle = container.querySelector('[data-date="2026-07-12"]')!
expect(idle).not.toHaveClass('nodata')
expect(idle.getAttribute('aria-label')).toContain('$0.00, 0 calls')
})
it('shows "No data recorded" on hover for a pre-history day', () => {
const { container } = render(<ActivityHeatmap daily={daily} />)
fireEvent.mouseEnter(container.querySelector('[data-date="2026-07-05"]')!)
const tip = document.querySelector('.chart-tip')!
expect(tip.textContent).toContain('No data recorded')
expect(tip.textContent).not.toContain('$0.00')
})
it('shows the currency value on hover for an idle day within history', () => {
const { container } = render(<ActivityHeatmap daily={daily} />)
fireEvent.mouseEnter(container.querySelector('[data-date="2026-07-12"]')!)
const tip = document.querySelector('.chart-tip')!
expect(tip.textContent).toContain('$0.00')
})
})