mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
tests: co-locate every Vitest suite next to its subject under src/
Eliminate the parallel `tests/` tree. Each `*.test.ts` / `*.test.tsx` now sits in the same directory as the module it covers, mirroring its filename (`Foo.ts` ↔ `Foo.test.ts`). The renderer-SDK mock used by component / hook tests moves to `src/__mocks__/nitro-renderer.ts` and the Vitest setup file becomes `src/test-setup.ts` — both still wired through `vitest.config.mts` exactly as before, only the paths changed. All 13 suites + 178/178 cases still pass. The production build is unaffected: rollup only follows imports from `src/index.tsx` and never crosses into `.test.ts` files, so test code is naturally tree-shaken out of the bundle. `yarn build` output is byte-for-byte the same on the user-facing chunks. tsconfig drops the now-redundant `tests` include entry. CLAUDE.md 'Layout convention' replaces the old `tests/` row with three rows documenting the new co-located convention, the `__mocks__/` directory and the `test-setup.ts` entry; ARCHITECTURE.md picks up the same update. The 'DO NOT CHANGE' qualifier on the layout is preserved — this rewrite IS the change, decided deliberately to make tests a first-class part of the source tree rather than a sibling project.
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CloneObject } from './CloneObject';
|
||||
import { ConvertSeconds } from './ConvertSeconds';
|
||||
import { LocalizeShortNumber } from './LocalizeShortNumber';
|
||||
import { GetWiredTimeLocale } from '../wired/GetWiredTimeLocale';
|
||||
import { WiredDateToString } from '../wired/WiredDateToString';
|
||||
import { getPrefixFontStyle, parsePrefixColors, PRESET_PREFIX_FONTS } from './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({});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user