The badge-deduplication helper was extracted from
InfoStandWidgetUserView in the prior commit; it's a pure (badges[]) =>
badges[] function that keeps slot indices stable by replacing duplicate
codes with empty strings. Coverage:
- empty input
- unique-only passthrough
- duplicate-replaced-with-empty
- falsy entries (null / undefined / '') normalized to ''
- first-occurrence-wins semantics
- order sensitivity (same multiset, different order -> different output)
Two unrelated cleanups grouped because they're both small and safe.
Dead code removal
- src/components/login/components/RegisterDialog.tsx
- src/components/login/components/ForgotDialog.tsx
- src/components/login/components/shared.ts (only consumed by the two
dialogs above)
These were the older non-Form-Actions versions of the register and
forgot-password dialogs. LoginView.tsx defines its own inline versions
that use `useActionState` + `useFormStatus` (Phase 3 of the React 19
modernization), which are the ones actually rendered. The legacy
files were already documented as dead in docs/ARCHITECTURE.md.
NewsWindow.tsx and the `components/` directory itself stay — NewsWindow
is still imported by LoginView at the bottom of the login flow.
Vitest coverage on FriendlyTime (+12 cases)
- 65 -> 77 passing tests, 5 -> 6 test files.
- LocalizeText is mocked with a deterministic stub
(`${ key }|${ amount }`) so each assertion can verify both the bucket
chosen and the rounded amount. The mock also short-circuits the
transitive renderer-SDK import, which keeps the test runner
decoupled from the renderer install state.
- Buckets covered: seconds / minutes / hours / days / months / years
for both `format` and `shortFormat`. Plus: threshold override,
key-suffix concatenation, half-hour rounding, the raw
`getLocalization` helper.
Verification
- yarn test: 6 files / 77 cases / ~2s.
- yarn eslint on the new test file: 0 errors / 0 warnings.
- yarn tsc: clean on touched files.
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.
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.
Phase 3 of the refactor plan in docs/ARCHITECTURE.md — the foundation
that unblocks every safe refactor below.
Install
- yarn add -D vitest@3 jsdom @testing-library/dom @testing-library/react
@testing-library/jest-dom
Note: pinned to vitest@3 (not the latest 4.x) because yarn 1's peer
resolution breaks on vitest@4's peer link to vite. With vitest@3 the
existing Vite 8 install resolves cleanly.
Configuration
- vitest.config.mts (new): separate from vite.config.mjs because the
dev/build config wires up renderer SDK aliases that point at sibling
working trees (../renderer, ../Nitro_Render_V3). Tests are written
against pure modules that don't pull in the renderer, so the test
runner uses a smaller alias set.
- tests/setup.ts (new): imports @testing-library/jest-dom/vitest so
custom matchers (toBeInTheDocument, etc.) are available without
per-file imports.
- tsconfig.json: include "tests" so eslint stops complaining about
unparseable files; also makes the IDE see the test files.
- package.json scripts: "test" (one-shot) and "test:watch".
Tests
- tests/WiredCreatorTools.helpers.test.ts (18 cases): covers the pure
helpers extracted in 3c68d97 — createEmptyMonitorSnapshot,
formatMonitorLatestOccurrence (5 time-bucket branches),
formatMonitorHistoryOccurrence, formatVariableTimestamp,
formatMonitorSource (4 branches), normalizeMonitorReason. These are
the most boring-but-easy-to-break functions; locking them down first
is high value, near-zero risk.
- tests/navigatorRoomCreatorStore.test.ts (4 cases): exercises the
Zustand store added in the previous commit — initial state, latch
semantics, 5s auto-reset (with fake timers), and the
"second beginCreate restarts the lockout" invariant. Validates that
the store-based replacement of the let-singleton has the same
observable behavior, plus the new invariant that wasn't possible
before (timer composition under StrictMode double-mount).
Side effect: two non-test source files were converted to `import type`
to keep the test bundle from accidentally pulling in the renderer SDK
transitively:
- src/components/wired-tools/WiredCreatorTools.types.ts
(`import type { AvatarInfoFurni }`)
- src/components/wired-tools/WiredCreatorTools.helpers.ts
(`import type { HotelDateTimeParts, MonitorSnapshot }`)
This is harmless — TypeScript already treated them as type-only —
and improves tree-shaking on build as a side benefit.
Verification
- yarn test -> 2 files, 22 tests passing in ~1.0s.
- yarn eslint on tests/ + the two type-only-import files: 0 errors,
0 warnings.
Migration path
- Next adoption targets: cover useDoorbellState reducer (data hook
split), the new useNitroQuery adapter (timeout/cleanup behavior),
and the smaller pure formatters under src/api/.
- React component tests (via @testing-library/react) deferred until
there's a small mock layer for the renderer SDK. The
@testing-library/* deps are already installed so that PR is
unblocked.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q