mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-11 12:07:32 +01:00
Adds a React/TypeScript frontend (web/) with a Python stdlib HTTP server backend. The UI mirrors the CLI `stats` command in the browser, with real-time sliders for ProtonDB rating, HLTB confidence thresholds, daily play time, per-game time cap, playtime mode, no-HLTB-data fallback, and a target-date planner. A parity badge confirms the client-side totals reproduce the CLI defaults exactly (786 / 67031.1h / 143017.2h / 238447.9h). Python side: - `_web_dataset.py`: offline projection of HLTB/ProtonDB/snapshot caches into a compact, secret-free JSON payload; 100% branch coverage - `_web_server.py`: zero-dependency stdlib HTTP server serving the built Vite bundle and the /api/dataset endpoint; 100% branch coverage - `main.py`: new `serve` command wiring Frontend (Vitest + RTL, 100% branch coverage enforced): - TypeScript port of ProtonDB compound rating rule with full parity - Pure client-side filtering via estimate.ts (no server round-trips) - SVG completion timeline chart, sortable/searchable game table - Steam dark theme; responsive layout Pre-commit: adds `vitest-coverage` hook at pre-push stage requiring 100% branch coverage on the React codebase. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
69 lines
1.6 KiB
TypeScript
69 lines
1.6 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
daysUntil,
|
|
fmtEta,
|
|
fmtHours,
|
|
fmtHoursPrecise,
|
|
fmtPlaytime,
|
|
isoDate,
|
|
} from './format'
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
describe('fmtHours', () => {
|
|
it('returns a dash for non-positive values', () => {
|
|
expect(fmtHours(0)).toBe('—')
|
|
expect(fmtHours(-3)).toBe('—')
|
|
})
|
|
it('rounds with thousands separators', () => {
|
|
expect(fmtHours(67031.1)).toBe('67,031 h')
|
|
})
|
|
})
|
|
|
|
describe('fmtHoursPrecise', () => {
|
|
it('returns a dash for non-positive values', () => {
|
|
expect(fmtHoursPrecise(-1)).toBe('—')
|
|
})
|
|
it('keeps one decimal place', () => {
|
|
expect(fmtHoursPrecise(44.28)).toBe('44.3')
|
|
})
|
|
})
|
|
|
|
describe('fmtPlaytime', () => {
|
|
it('reports untouched at zero', () => {
|
|
expect(fmtPlaytime(0)).toBe('untouched')
|
|
})
|
|
it('converts minutes to hours', () => {
|
|
expect(fmtPlaytime(90)).toBe('1.5 h')
|
|
})
|
|
})
|
|
|
|
describe('fmtEta', () => {
|
|
it('returns N/A for null', () => {
|
|
expect(fmtEta(null)).toBe('N/A')
|
|
})
|
|
it('returns days and the target date', () => {
|
|
vi.useFakeTimers()
|
|
vi.setSystemTime(new Date('2026-05-29T12:00:00'))
|
|
const out = fmtEta(10)
|
|
expect(out).toContain('10 days')
|
|
expect(out).toContain('2026-06-08')
|
|
})
|
|
})
|
|
|
|
describe('isoDate', () => {
|
|
it('formats a date as YYYY-MM-DD', () => {
|
|
expect(isoDate(new Date(2026, 0, 5))).toBe('2026-01-05')
|
|
})
|
|
})
|
|
|
|
describe('daysUntil', () => {
|
|
it('counts whole days to a future date', () => {
|
|
vi.useFakeTimers()
|
|
vi.setSystemTime(new Date('2026-05-29T12:00:00'))
|
|
expect(daysUntil('2026-06-08')).toBe(10)
|
|
})
|
|
})
|