173 lines
6.0 KiB
JavaScript
173 lines
6.0 KiB
JavaScript
import { describe, it, expect } from 'vitest'
|
|
import { buildChartGeometry } from '../utils/history.js'
|
|
import { sizeShort } from '../utils/format.js'
|
|
|
|
describe('sizeShort', () => {
|
|
it('strips " model" suffix', () => {
|
|
expect(sizeShort('1 model')).toBe('1')
|
|
})
|
|
it('strips " models" suffix', () => {
|
|
expect(sizeShort('10 models')).toBe('10')
|
|
})
|
|
it('leaves other strings unchanged', () => {
|
|
expect(sizeShort('Squad')).toBe('Squad')
|
|
})
|
|
})
|
|
|
|
import { pctLabel, ptsLabel, changeColor } from '../utils/format.js'
|
|
|
|
describe('pctLabel', () => {
|
|
it('prefixes positive values with +', () => {
|
|
expect(pctLabel(12.34)).toBe('+12.3%')
|
|
})
|
|
it('renders negative values without prefix', () => {
|
|
expect(pctLabel(-5)).toBe('-5.0%')
|
|
})
|
|
it('renders zero without prefix', () => {
|
|
expect(pctLabel(0)).toBe('0.0%')
|
|
})
|
|
it('renders em-dash for null', () => {
|
|
expect(pctLabel(null)).toBe('—')
|
|
})
|
|
it('renders em-dash for undefined', () => {
|
|
expect(pctLabel(undefined)).toBe('—')
|
|
})
|
|
})
|
|
|
|
describe('ptsLabel', () => {
|
|
it('prefixes positive values with +', () => {
|
|
expect(ptsLabel(10)).toBe('+10')
|
|
})
|
|
it('renders negative values as bare -N', () => {
|
|
expect(ptsLabel(-5)).toBe('-5')
|
|
})
|
|
it('renders zero without prefix', () => {
|
|
expect(ptsLabel(0)).toBe('0')
|
|
})
|
|
it('renders em-dash for null', () => {
|
|
expect(ptsLabel(null)).toBe('—')
|
|
})
|
|
})
|
|
|
|
describe('changeColor', () => {
|
|
it('returns red for positive', () => {
|
|
expect(changeColor(1)).toBe('#f85149')
|
|
})
|
|
it('returns green for negative', () => {
|
|
expect(changeColor(-1)).toBe('#3fb950')
|
|
})
|
|
it('returns text.secondary for zero', () => {
|
|
expect(changeColor(0)).toBe('text.secondary')
|
|
})
|
|
it('returns text.secondary for null', () => {
|
|
expect(changeColor(null)).toBe('text.secondary')
|
|
})
|
|
})
|
|
|
|
import { readFiltersFromUrl, writeFiltersToUrl } from '../utils/url.js'
|
|
|
|
describe('readFiltersFromUrl', () => {
|
|
it('returns defaults when search is empty', () => {
|
|
window.history.replaceState(null, '', '/?')
|
|
expect(readFiltersFromUrl()).toEqual({ q: '', faction: '', dir: '' })
|
|
})
|
|
it('reads q, faction, dir from query string', () => {
|
|
window.history.replaceState(null, '', '/?q=foo&faction=space-marines&dir=up')
|
|
expect(readFiltersFromUrl()).toEqual({ q: 'foo', faction: 'space-marines', dir: 'up' })
|
|
})
|
|
it('returns empty strings for missing keys', () => {
|
|
window.history.replaceState(null, '', '/?q=foo')
|
|
expect(readFiltersFromUrl()).toEqual({ q: 'foo', faction: '', dir: '' })
|
|
})
|
|
})
|
|
|
|
describe('writeFiltersToUrl', () => {
|
|
it('writes all three params', () => {
|
|
writeFiltersToUrl({ q: 'foo', faction: 'space-marines', dir: 'up' })
|
|
expect(window.location.search).toBe('?q=foo&faction=space-marines&dir=up')
|
|
})
|
|
it('clears the query string when all are empty', () => {
|
|
writeFiltersToUrl({ q: '', faction: '', dir: '' })
|
|
expect(window.location.search).toBe('')
|
|
})
|
|
it('writes only the present keys (others stay empty)', () => {
|
|
writeFiltersToUrl({ q: 'foo', faction: '', dir: '' })
|
|
expect(window.location.search).toBe('?q=foo')
|
|
})
|
|
})
|
|
|
|
import { computeMovers } from '../utils/movers.js'
|
|
|
|
const mk = (name, change_pct) => ({ name, change_pct })
|
|
|
|
describe('computeMovers', () => {
|
|
it('returns empty arrays for empty input', () => {
|
|
expect(computeMovers([])).toEqual({ drops: [], rises: [] })
|
|
})
|
|
it('returns top 5 drops (largest negative change_pct first)', () => {
|
|
const input = [
|
|
mk('A', -1), mk('B', -50), mk('C', -20), mk('D', -5),
|
|
mk('E', -10), mk('F', -30), mk('G', -2),
|
|
]
|
|
expect(computeMovers(input).drops.map(u => u.name)).toEqual(['B', 'F', 'C', 'E', 'D'])
|
|
})
|
|
it('returns top 5 rises (largest positive change_pct first)', () => {
|
|
const input = [
|
|
mk('A', 1), mk('B', 50), mk('C', 20), mk('D', 5),
|
|
mk('E', 10), mk('F', 30), mk('G', 2),
|
|
]
|
|
expect(computeMovers(input).rises.map(u => u.name)).toEqual(['B', 'F', 'C', 'E', 'D'])
|
|
})
|
|
it('excludes units with change_pct === null from both lists', () => {
|
|
const input = [mk('A', -10), mk('B', null), mk('C', 10)]
|
|
const { drops, rises } = computeMovers(input)
|
|
expect(drops.map(u => u.name)).toEqual(['A'])
|
|
expect(rises.map(u => u.name)).toEqual(['C'])
|
|
})
|
|
it('excludes units with change_pct === 0 from both lists', () => {
|
|
const input = [mk('A', -10), mk('B', 0), mk('C', 10)]
|
|
const { drops, rises } = computeMovers(input)
|
|
expect(drops.map(u => u.name)).toEqual(['A'])
|
|
expect(rises.map(u => u.name)).toEqual(['C'])
|
|
})
|
|
})
|
|
|
|
const DIMS = { W: 640, H: 360, padL: 70, padR: 24, padT: 24, padB: 48 }
|
|
|
|
describe('buildChartGeometry', () => {
|
|
it('returns empty linePath for empty history', () => {
|
|
const g = buildChartGeometry([], DIMS)
|
|
expect(g.linePath).toBe('')
|
|
expect(g.areaPath).toBe('')
|
|
expect(g.yTicks).toBeDefined()
|
|
})
|
|
it('returns empty areaPath for single-point history', () => {
|
|
const g = buildChartGeometry([{ pts: 50, date: '2026-01-01', version: 'v1' }], DIMS)
|
|
expect(g.linePath).not.toBe('')
|
|
expect(g.areaPath).toBe('')
|
|
})
|
|
it('produces a multi-segment path for 3+ points', () => {
|
|
const g = buildChartGeometry([
|
|
{ pts: 50, date: '2024-01-01', version: 'v1' },
|
|
{ pts: 60, date: '2025-01-01', version: 'v2' },
|
|
{ pts: 40, date: '2026-01-01', version: 'v3' },
|
|
], DIMS)
|
|
expect(g.linePath.split('L').length).toBe(3) // 1 M + 2 L
|
|
expect(g.areaPath).not.toBe('')
|
|
})
|
|
it('produces y-axis ticks spanning the data range', () => {
|
|
const g = buildChartGeometry([
|
|
{ pts: 10, date: '2024-01-01', version: 'v1' },
|
|
{ pts: 100, date: '2026-01-01', version: 'v2' },
|
|
], DIMS)
|
|
expect(g.yTicks.length).toBeGreaterThan(1)
|
|
const values = g.yTicks.map(t => t.v)
|
|
expect(Math.min(...values)).toBeLessThanOrEqual(10)
|
|
expect(Math.max(...values)).toBeGreaterThanOrEqual(100)
|
|
})
|
|
it('chartW and chartH are W minus horizontal/vertical padding', () => {
|
|
const g = buildChartGeometry([], DIMS)
|
|
expect(g.chartW).toBe(DIMS.W - DIMS.padL - DIMS.padR)
|
|
expect(g.chartH).toBe(DIMS.H - DIMS.padT - DIMS.padB)
|
|
})
|
|
}) |