test(web): add tests for utility functions (#102)

Add tests for utility modules:
- formatters/number.ts: formatHp, formatAge
- mapStyles.ts: REGION_STYLES, getRegionTextStyle
- procedural.ts: getClusteredTileVariant
- theme.ts: GRADE_COLORS, getEntityColor

Total: 52 new tests added

Closes #87
This commit is contained in:
Zihao Xu
2026-01-25 18:01:38 -08:00
committed by GitHub
parent c3ac584a90
commit 406d62f983
4 changed files with 379 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
import { describe, it, expect } from 'vitest'
import { formatHp, formatAge } from '@/utils/formatters/number'
describe('formatters/number', () => {
describe('formatHp', () => {
it('should format integer HP values', () => {
expect(formatHp(100, 200)).toBe('100 / 200')
})
it('should floor decimal current HP', () => {
expect(formatHp(99.7, 200)).toBe('99 / 200')
expect(formatHp(99.2, 200)).toBe('99 / 200')
})
it('should handle full HP', () => {
expect(formatHp(100, 100)).toBe('100 / 100')
})
it('should handle zero HP', () => {
expect(formatHp(0, 100)).toBe('0 / 100')
})
it('should handle large HP values', () => {
expect(formatHp(12345, 99999)).toBe('12345 / 99999')
})
it('should handle negative HP', () => {
expect(formatHp(-10, 100)).toBe('-10 / 100')
})
})
describe('formatAge', () => {
it('should format age and lifespan', () => {
expect(formatAge(25, 100)).toBe('25 / 100')
})
it('should handle zero age', () => {
expect(formatAge(0, 100)).toBe('0 / 100')
})
it('should handle age equal to lifespan', () => {
expect(formatAge(100, 100)).toBe('100 / 100')
})
it('should handle large age values', () => {
expect(formatAge(1000, 5000)).toBe('1000 / 5000')
})
it('should handle age exceeding lifespan', () => {
expect(formatAge(150, 100)).toBe('150 / 100')
})
})
})

View File

@@ -0,0 +1,64 @@
import { describe, it, expect } from 'vitest'
import { REGION_STYLES, getRegionTextStyle } from '@/utils/mapStyles'
describe('mapStyles', () => {
describe('REGION_STYLES', () => {
it('should have sect style', () => {
expect(REGION_STYLES.sect).toBeDefined()
expect(REGION_STYLES.sect.fontSize).toBe(60)
expect(REGION_STYLES.sect.fill).toBe('#ffcc00')
})
it('should have city style', () => {
expect(REGION_STYLES.city).toBeDefined()
expect(REGION_STYLES.city.fontSize).toBe(72)
expect(REGION_STYLES.city.fill).toBe('#ccffcc')
})
it('should have default style', () => {
expect(REGION_STYLES.default).toBeDefined()
expect(REGION_STYLES.default.fontSize).toBe(72)
expect(REGION_STYLES.default.fill).toBe('#ffffff')
})
it('should have consistent font family', () => {
const expectedFont = '"Microsoft YaHei", sans-serif'
expect(REGION_STYLES.sect.fontFamily).toBe(expectedFont)
expect(REGION_STYLES.city.fontFamily).toBe(expectedFont)
expect(REGION_STYLES.default.fontFamily).toBe(expectedFont)
})
it('should have drop shadow on all styles', () => {
expect(REGION_STYLES.sect.dropShadow).toBeDefined()
expect(REGION_STYLES.city.dropShadow).toBeDefined()
expect(REGION_STYLES.default.dropShadow).toBeDefined()
})
})
describe('getRegionTextStyle', () => {
it('should return sect style for sect type', () => {
const style = getRegionTextStyle('sect')
expect(style).toBe(REGION_STYLES.sect)
})
it('should return city style for city type', () => {
const style = getRegionTextStyle('city')
expect(style).toBe(REGION_STYLES.city)
})
it('should return default style for default type', () => {
const style = getRegionTextStyle('default')
expect(style).toBe(REGION_STYLES.default)
})
it('should return default style for unknown type', () => {
const style = getRegionTextStyle('unknown')
expect(style).toBe(REGION_STYLES.default)
})
it('should return default style for empty string', () => {
const style = getRegionTextStyle('')
expect(style).toBe(REGION_STYLES.default)
})
})
})

View File

@@ -0,0 +1,116 @@
import { describe, it, expect } from 'vitest'
import { getClusteredTileVariant } from '@/utils/procedural'
describe('procedural', () => {
describe('getClusteredTileVariant', () => {
it('should return startIndex when count is 0', () => {
const result = getClusteredTileVariant(5, 5, 0)
expect(result).toBe(0)
})
it('should return startIndex when count is 1', () => {
const result = getClusteredTileVariant(5, 5, 1)
expect(result).toBe(0)
})
it('should return startIndex when count is 1 with custom startIndex', () => {
const result = getClusteredTileVariant(5, 5, 1, 10)
expect(result).toBe(10)
})
it('should return value within valid range for count > 1', () => {
const count = 9
for (let x = 0; x < 20; x++) {
for (let y = 0; y < 20; y++) {
const result = getClusteredTileVariant(x, y, count)
expect(result).toBeGreaterThanOrEqual(0)
expect(result).toBeLessThan(count)
}
}
})
it('should return value within valid range with custom startIndex', () => {
const count = 5
const startIndex = 10
for (let x = 0; x < 20; x++) {
for (let y = 0; y < 20; y++) {
const result = getClusteredTileVariant(x, y, count, startIndex)
expect(result).toBeGreaterThanOrEqual(startIndex)
expect(result).toBeLessThanOrEqual(startIndex + count - 1)
}
}
})
it('should be deterministic for same coordinates', () => {
const x = 42
const y = 17
const count = 9
const result1 = getClusteredTileVariant(x, y, count)
const result2 = getClusteredTileVariant(x, y, count)
const result3 = getClusteredTileVariant(x, y, count)
expect(result1).toBe(result2)
expect(result2).toBe(result3)
})
it('should produce different values for different coordinates', () => {
const count = 9
const results = new Set<number>()
// Sample 100 different coordinates.
for (let i = 0; i < 100; i++) {
const result = getClusteredTileVariant(i * 7, i * 13, count)
results.add(result)
}
// Should produce multiple different values (not all the same).
expect(results.size).toBeGreaterThan(1)
})
it('should show clustering behavior - nearby coordinates tend to have similar values', () => {
const count = 9
let similarCount = 0
const samples = 50
for (let i = 0; i < samples; i++) {
const x = Math.floor(Math.random() * 100)
const y = Math.floor(Math.random() * 100)
const center = getClusteredTileVariant(x, y, count)
const neighbor = getClusteredTileVariant(x + 1, y, count)
// Count how often neighbors have similar values (within 2).
if (Math.abs(center - neighbor) <= 2) {
similarCount++
}
}
// With clustering, we expect neighbors to be similar more often than random.
// Random would give similarity ~44% of the time (5/9 * 9 = 55% chance of diff >= 3).
// Clustering should give higher similarity rate.
expect(similarCount).toBeGreaterThan(samples * 0.3)
})
it('should handle negative coordinates', () => {
const count = 9
const result = getClusteredTileVariant(-10, -20, count)
expect(result).toBeGreaterThanOrEqual(0)
expect(result).toBeLessThan(count)
})
it('should handle large coordinates', () => {
const count = 9
const result = getClusteredTileVariant(10000, 20000, count)
expect(result).toBeGreaterThanOrEqual(0)
expect(result).toBeLessThan(count)
})
it('should handle floating point coordinates', () => {
const count = 9
const result = getClusteredTileVariant(5.5, 7.3, count)
expect(result).toBeGreaterThanOrEqual(0)
expect(result).toBeLessThan(count)
})
})
})

View File

@@ -0,0 +1,146 @@
import { describe, it, expect } from 'vitest'
import { GRADE_COLORS, getEntityColor } from '@/utils/theme'
import type { EffectEntity } from '@/types/core'
describe('theme', () => {
describe('GRADE_COLORS', () => {
it('should have purple colors for upper grade items', () => {
expect(GRADE_COLORS['上品']).toBe('#c488fd')
expect(GRADE_COLORS['宝物']).toBe('#c488fd')
expect(GRADE_COLORS['SR']).toBe('#c488fd')
expect(GRADE_COLORS['Upper']).toBe('#c488fd')
})
it('should have green colors for middle grade items', () => {
expect(GRADE_COLORS['中品']).toBe('#88fdc4')
expect(GRADE_COLORS['R']).toBe('#88fdc4')
expect(GRADE_COLORS['Middle']).toBe('#88fdc4')
})
it('should have gold colors for artifact grade items', () => {
expect(GRADE_COLORS['法宝']).toBe('#fddc88')
expect(GRADE_COLORS['SSR']).toBe('#fddc88')
expect(GRADE_COLORS['Artifact']).toBe('#fddc88')
})
it('should have default color', () => {
expect(GRADE_COLORS['Default']).toBe('#cccccc')
})
it('should have realm colors', () => {
expect(GRADE_COLORS['练气']).toBe('#cccccc')
expect(GRADE_COLORS['筑基']).toBe('#88fdc4')
expect(GRADE_COLORS['金丹']).toBe('#c488fd')
expect(GRADE_COLORS['元婴']).toBe('#fddc88')
})
})
describe('getEntityColor', () => {
it('should return undefined for null entity', () => {
expect(getEntityColor(null)).toBeUndefined()
})
it('should return undefined for undefined entity', () => {
expect(getEntityColor(undefined)).toBeUndefined()
})
it('should return undefined for empty entity', () => {
expect(getEntityColor({})).toBeUndefined()
})
describe('RGB array color', () => {
it('should convert RGB array to rgb string', () => {
const entity: Partial<EffectEntity> = {
color: [255, 128, 64]
}
expect(getEntityColor(entity)).toBe('rgb(255,128,64)')
})
it('should handle RGB array with zeros', () => {
const entity: Partial<EffectEntity> = {
color: [0, 0, 0]
}
expect(getEntityColor(entity)).toBe('rgb(0,0,0)')
})
it('should ignore array with wrong length', () => {
const entity = {
color: [255, 128] as any
}
expect(getEntityColor(entity)).toBeUndefined()
})
})
describe('string color', () => {
it('should return string color directly', () => {
const entity: Partial<EffectEntity> = {
color: '#ff0000'
}
expect(getEntityColor(entity)).toBe('#ff0000')
})
it('should return named color directly', () => {
const entity: Partial<EffectEntity> = {
color: 'red'
}
expect(getEntityColor(entity)).toBe('red')
})
})
describe('grade-based color', () => {
it('should return color based on grade', () => {
const entity: Partial<EffectEntity> = {
grade: '上品'
}
expect(getEntityColor(entity)).toBe('#c488fd')
})
it('should return color based on rarity when no grade', () => {
const entity: Partial<EffectEntity> = {
rarity: 'SR'
}
expect(getEntityColor(entity)).toBe('#c488fd')
})
it('should prefer grade over rarity', () => {
const entity: Partial<EffectEntity> = {
grade: '上品',
rarity: '中品'
}
expect(getEntityColor(entity)).toBe('#c488fd')
})
it('should match partial grade strings', () => {
const entity: Partial<EffectEntity> = {
grade: '上品丹药'
}
expect(getEntityColor(entity)).toBe('#c488fd')
})
it('should return undefined for unknown grade', () => {
const entity: Partial<EffectEntity> = {
grade: 'unknown'
}
expect(getEntityColor(entity)).toBeUndefined()
})
})
describe('priority', () => {
it('should prefer RGB array over grade', () => {
const entity: Partial<EffectEntity> = {
color: [100, 200, 150],
grade: '上品'
}
expect(getEntityColor(entity)).toBe('rgb(100,200,150)')
})
it('should prefer string color over grade', () => {
const entity: Partial<EffectEntity> = {
color: '#custom',
grade: '上品'
}
expect(getEntityColor(entity)).toBe('#custom')
})
})
})
})