mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
9d2e4a7324
22 -> 49 passing tests, 2 -> 3 test files. Targets are functions with zero external dependencies (no renderer SDK, no network, no DOM). They were picked because: - they're easy to break by accident in a refactor (rounding edge cases, zero-padding rules); - their behavior is documented by tests once and for all, including the surprising bit about LocalizeShortNumber rounding 950..999 into the "1K" bucket (kept as an explicit "documented quirk" assertion rather than fixed — the current behavior is what the rest of the app expects). New file: tests/api-utils.test.ts (27 cases) - ConvertSeconds: zero, 1m / 1h / 1d, mixed, single-digit padding (6). - LocalizeShortNumber: zero/NaN/null guard, sub-1000 stays as-is, K/M/B buckets, negative numbers, the 950..999 rounding quirk (7). - CloneObject: primitives, identity preservation, key fidelity (3). - GetWiredTimeLocale: even (whole sec), odd (half sec), zero (3). - WiredDateToString: zero-pad rules, two-digit values (2). - PrefixUtils.parsePrefixColors: empty inputs, mapping, color reuse (3). - PrefixUtils.getPrefixFontStyle: default empty id, known preset, unknown id (3). Verification - yarn test: 3 files / 49 cases / ~1.1s. - yarn eslint on tests/: 0 errors / 0 warnings. - All test targets are stable pure functions; the assertions double as documentation for callers.
195 lines
5.7 KiB
TypeScript
195 lines
5.7 KiB
TypeScript
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({});
|
|
});
|
|
});
|