diff --git a/tests/api-utils.test.ts b/tests/api-utils.test.ts new file mode 100644 index 0000000..816f019 --- /dev/null +++ b/tests/api-utils.test.ts @@ -0,0 +1,194 @@ +import { describe, expect, it } from 'vitest'; +import { CloneObject } from '../src/api/utils/CloneObject'; +import { ConvertSeconds } from '../src/api/utils/ConvertSeconds'; +import { LocalizeShortNumber } from '../src/api/utils/LocalizeShortNumber'; +import { GetWiredTimeLocale } from '../src/api/wired/GetWiredTimeLocale'; +import { WiredDateToString } from '../src/api/wired/WiredDateToString'; +import { getPrefixFontStyle, parsePrefixColors, PRESET_PREFIX_FONTS } from '../src/api/utils/PrefixUtils'; + +describe('ConvertSeconds', () => +{ + it('formats zero seconds as the dd:hh:mm:ss zero string', () => + { + expect(ConvertSeconds(0)).toBe('00:00:00:00'); + }); + + it('formats one minute correctly', () => + { + expect(ConvertSeconds(60)).toBe('00:00:01:00'); + }); + + it('formats one hour correctly', () => + { + expect(ConvertSeconds(3600)).toBe('00:01:00:00'); + }); + + it('formats one day correctly', () => + { + expect(ConvertSeconds(86400)).toBe('01:00:00:00'); + }); + + it('formats a mixed value (1d 2h 3m 4s)', () => + { + expect(ConvertSeconds(86400 + 2 * 3600 + 3 * 60 + 4)).toBe('01:02:03:04'); + }); + + it('pads single-digit components with a leading zero', () => + { + expect(ConvertSeconds(9)).toBe('00:00:00:09'); + }); +}); + +describe('LocalizeShortNumber', () => +{ + it('returns "0" for zero, null, undefined, and NaN', () => + { + expect(LocalizeShortNumber(0)).toBe('0'); + expect(LocalizeShortNumber(NaN)).toBe('0'); + expect(LocalizeShortNumber(null)).toBe('0'); + expect(LocalizeShortNumber(undefined as unknown as number)).toBe('0'); + }); + + it('keeps numbers safely under 1000 unchanged (returns as-is)', () => + { + expect(LocalizeShortNumber(42)).toBe('42'); + // Anything that rounds to >= 1.0K (i.e. >= 950) crosses into the K bucket + expect(LocalizeShortNumber(949)).toBe('949'); + }); + + it('rounds 950..999 up into the K bucket (documented quirk)', () => + { + expect(LocalizeShortNumber(950)).toBe('1K'); + expect(LocalizeShortNumber(999)).toBe('1K'); + }); + + it('uses K for thousands', () => + { + expect(LocalizeShortNumber(1500)).toBe('1.5K'); + expect(LocalizeShortNumber(12_345)).toBe('12.3K'); + }); + + it('uses M for millions', () => + { + expect(LocalizeShortNumber(2_500_000)).toBe('2.5M'); + }); + + it('uses B for billions', () => + { + expect(LocalizeShortNumber(3_700_000_000)).toBe('3.7B'); + }); + + it('preserves the sign for negative values', () => + { + expect(LocalizeShortNumber(-1500)).toBe('-1.5K'); + expect(LocalizeShortNumber(-2_500_000)).toBe('-2.5M'); + }); +}); + +describe('CloneObject', () => +{ + it('returns primitives unchanged', () => + { + expect(CloneObject(42)).toBe(42); + expect(CloneObject('hello')).toBe('hello'); + expect(CloneObject(null)).toBe(null); + expect(CloneObject(undefined)).toBe(undefined); + }); + + it('returns a new object instance for object inputs', () => + { + const original = { a: 1, b: 'two' }; + const copy = CloneObject(original); + + expect(copy).not.toBe(original); + expect(copy).toEqual(original); + }); + + it('preserves enumerable own keys', () => + { + const original = { x: 1, y: 2, z: 3 }; + const copy = CloneObject(original); + + expect(copy.x).toBe(1); + expect(copy.y).toBe(2); + expect(copy.z).toBe(3); + }); +}); + +describe('GetWiredTimeLocale', () => +{ + // The renderer encodes time as `value = seconds * 2` so even values + // are whole seconds, odd values are half-seconds. + + it('returns "0" for value 0', () => + { + expect(GetWiredTimeLocale(0)).toBe('0'); + }); + + it('returns whole seconds for even values', () => + { + expect(GetWiredTimeLocale(2)).toBe('1'); + expect(GetWiredTimeLocale(10)).toBe('5'); + expect(GetWiredTimeLocale(60)).toBe('30'); + }); + + it('returns half-second formatting for odd values', () => + { + expect(GetWiredTimeLocale(1)).toBe('0.5'); + expect(GetWiredTimeLocale(3)).toBe('1.5'); + expect(GetWiredTimeLocale(11)).toBe('5.5'); + }); +}); + +describe('WiredDateToString', () => +{ + it('zero-pads single-digit month / day / hour / minute', () => + { + const d = new Date(2024, 0, 5, 7, 9); // Jan 5, 2024, 07:09 + expect(WiredDateToString(d)).toBe('2024/01/05 07:09'); + }); + + it('formats two-digit values without extra padding', () => + { + const d = new Date(2024, 11, 31, 23, 59); // Dec 31, 2024, 23:59 + expect(WiredDateToString(d)).toBe('2024/12/31 23:59'); + }); +}); + +describe('PrefixUtils.parsePrefixColors', () => +{ + it('returns an empty array when text or colors are empty', () => + { + expect(parsePrefixColors('', '#fff')).toEqual([]); + expect(parsePrefixColors('abc', '')).toEqual([]); + }); + + it('maps each text character to the nth color', () => + { + expect(parsePrefixColors('ab', '#f00,#0f0')).toEqual([ '#f00', '#0f0' ]); + }); + + it('reuses the last color when the text is longer than the color list', () => + { + expect(parsePrefixColors('abcd', '#f00,#0f0')).toEqual([ '#f00', '#0f0', '#0f0', '#0f0' ]); + }); +}); + +describe('PrefixUtils.getPrefixFontStyle', () => +{ + it('returns an empty object for the default (empty) font id', () => + { + expect(getPrefixFontStyle('')).toEqual({}); + }); + + it('returns a fontFamily for a known preset', () => + { + const out = getPrefixFontStyle('pixel'); + expect(out.fontFamily).toBe(PRESET_PREFIX_FONTS.find(p => p.id === 'pixel')?.family); + }); + + it('returns an empty object for an unknown font id', () => + { + expect(getPrefixFontStyle('does-not-exist')).toEqual({}); + }); +});