Files
wh40k-points-comparator/react-app/src/test/utils.test.js

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)
})
})