From bb28d252d8f23b539c0f7814102a5fa1e216abf1 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 11 May 2026 17:50:47 +0000 Subject: [PATCH] Vitest: +16 cases on ColorUtils, FixedSizeStack, LocalizeFormattedNumber MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 49 -> 65 passing tests, 4 -> 5 test files. New file: tests/api-utils-extra.test.ts (16 cases) - LocalizeFormattedNumber (3): zero/NaN/null guard, sub-1000 stays, >=1000 inserts thin-space group separators. - ColorUtils (8): makeColorHex, makeColorNumberHex (with zero-pad), convertFromHex (with/without #), int_to_8BitVals/eight_bitVals_to_int roundtrip, int2rgb pure-RGB output, zero-input edge cases. - FixedSizeStack (4): grow then overwrite oldest (ring-buffer semantics), reset clears state, partial-fill behavior of getMax, empty-stack returns Number.MIN_VALUE. The "partial-fill" case documents a subtle quirk: getMax iterates the whole maxSize window including undefined slots, but `undefined > X` is false in JS so the inserted value wins — the test pins that behavior. Note on `usePetPackageWidget` and `useWordQuizWidget` - They were both considered for a state/actions split this turn but their actions mutate internal state (`onClose` resets 5 useState, `vote` reads pollId/question/answerSent). A clean split would require either passing args to the action or hoisting the state to a shared store first. Deferred as follow-up. Verification - yarn test: 5 files / 65 cases / ~1.9s. - yarn eslint on the new test file: 0 errors / 0 warnings. --- tests/api-utils-extra.test.ts | 165 ++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 tests/api-utils-extra.test.ts diff --git a/tests/api-utils-extra.test.ts b/tests/api-utils-extra.test.ts new file mode 100644 index 0000000..afd2d77 --- /dev/null +++ b/tests/api-utils-extra.test.ts @@ -0,0 +1,165 @@ +import { describe, expect, it } from 'vitest'; +import { ColorUtils } from '../src/api/utils/ColorUtils'; +import { FixedSizeStack } from '../src/api/utils/FixedSizeStack'; +import { LocalizeFormattedNumber } from '../src/api/utils/LocalizeFormattedNumber'; + +describe('LocalizeFormattedNumber', () => +{ + it('returns "0" for zero / NaN / null / undefined', () => + { + expect(LocalizeFormattedNumber(0)).toBe('0'); + expect(LocalizeFormattedNumber(NaN)).toBe('0'); + expect(LocalizeFormattedNumber(null)).toBe('0'); + expect(LocalizeFormattedNumber(undefined as unknown as number)).toBe('0'); + }); + + it('keeps numbers under 1000 unchanged', () => + { + expect(LocalizeFormattedNumber(42)).toBe('42'); + expect(LocalizeFormattedNumber(999)).toBe('999'); + }); + + it('inserts a thin space every 3 digits for >=1000', () => + { + expect(LocalizeFormattedNumber(1000)).toBe('1 000'); + expect(LocalizeFormattedNumber(1_234_567)).toBe('1 234 567'); + expect(LocalizeFormattedNumber(10_000_000)).toBe('10 000 000'); + }); +}); + +describe('ColorUtils', () => +{ + describe('makeColorHex', () => + { + it('prepends "#" to the given color string', () => + { + expect(ColorUtils.makeColorHex('ff0000')).toBe('#ff0000'); + expect(ColorUtils.makeColorHex('abc')).toBe('#abc'); + }); + }); + + describe('makeColorNumberHex', () => + { + it('pads to 6 hex chars and prepends "#"', () => + { + expect(ColorUtils.makeColorNumberHex(0xff0000)).toBe('#ff0000'); + expect(ColorUtils.makeColorNumberHex(0x00ff00)).toBe('#00ff00'); + expect(ColorUtils.makeColorNumberHex(0)).toBe('#000000'); + }); + + it('pads short hex values with leading zeros', () => + { + expect(ColorUtils.makeColorNumberHex(0xff)).toBe('#0000ff'); + expect(ColorUtils.makeColorNumberHex(1)).toBe('#000001'); + }); + }); + + describe('convertFromHex', () => + { + it('parses a "#"-prefixed hex string to a number', () => + { + expect(ColorUtils.convertFromHex('#ff0000')).toBe(0xff0000); + expect(ColorUtils.convertFromHex('#000000')).toBe(0); + expect(ColorUtils.convertFromHex('#ffffff')).toBe(0xffffff); + }); + + it('also handles strings without the leading "#"', () => + { + expect(ColorUtils.convertFromHex('00ff00')).toBe(0x00ff00); + }); + }); + + describe('int_to_8BitVals / eight_bitVals_to_int', () => + { + it('roundtrips: int -> [a,r,g,b] -> int', () => + { + const original = 0x12345678; + const [ a, b, c, d ] = ColorUtils.int_to_8BitVals(original); + expect(a).toBe(0x12); + expect(b).toBe(0x34); + expect(c).toBe(0x56); + expect(d).toBe(0x78); + expect(ColorUtils.eight_bitVals_to_int(a, b, c, d)).toBe(original); + }); + + it('roundtrips zero', () => + { + const parts = ColorUtils.int_to_8BitVals(0); + expect(parts).toEqual([ 0, 0, 0, 0 ]); + expect(ColorUtils.eight_bitVals_to_int(0, 0, 0, 0)).toBe(0); + }); + }); + + describe('int2rgb', () => + { + it('produces rgba(r,g,b,1) for an RGB integer', () => + { + expect(ColorUtils.int2rgb(0xff0000)).toBe('rgba(255,0,0,1)'); + expect(ColorUtils.int2rgb(0x00ff00)).toBe('rgba(0,255,0,1)'); + expect(ColorUtils.int2rgb(0x0000ff)).toBe('rgba(0,0,255,1)'); + }); + + it('returns black for 0', () => + { + expect(ColorUtils.int2rgb(0)).toBe('rgba(0,0,0,1)'); + }); + }); +}); + +describe('FixedSizeStack', () => +{ + it('grows up to maxSize then overwrites the oldest entry', () => + { + const stack = new FixedSizeStack(3); + + stack.addValue(10); + stack.addValue(20); + stack.addValue(30); + + expect(stack.getMax()).toBe(30); + expect(stack.getMin()).toBe(10); + + // Capacity hit — 40 overwrites 10 + stack.addValue(40); + expect(stack.getMin()).toBe(20); + expect(stack.getMax()).toBe(40); + + // 50 overwrites 20 + stack.addValue(50); + expect(stack.getMin()).toBe(30); + expect(stack.getMax()).toBe(50); + }); + + it('reset clears all values', () => + { + const stack = new FixedSizeStack(2); + + stack.addValue(100); + stack.addValue(200); + + expect(stack.getMax()).toBe(200); + + stack.reset(); + + stack.addValue(7); + expect(stack.getMax()).toBe(7); + expect(stack.getMin()).toBe(7); + }); + + it('getMax with maxSize > inserted entries returns the inserted value', () => + { + // FixedSizeStack iterates the whole maxSize window but the + // unfilled slots are `undefined` which fail `> currentMax`, so + // the inserted value wins. + const stack = new FixedSizeStack(5); + stack.addValue(42); + + expect(stack.getMax()).toBe(42); + }); + + it('getMax on an empty stack returns Number.MIN_VALUE', () => + { + const stack = new FixedSizeStack(3); + expect(stack.getMax()).toBe(Number.MIN_VALUE); + }); +});