mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
6793de2106
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
150 lines
5.1 KiB
TypeScript
150 lines
5.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
createEmptyMonitorSnapshot,
|
|
formatMonitorHistoryOccurrence,
|
|
formatMonitorLatestOccurrence,
|
|
formatMonitorSource,
|
|
formatVariableTimestamp,
|
|
normalizeMonitorReason
|
|
} from '../src/components/wired-tools/WiredCreatorTools.helpers';
|
|
|
|
describe('WiredCreatorTools helpers', () =>
|
|
{
|
|
describe('createEmptyMonitorSnapshot', () =>
|
|
{
|
|
it('returns a zeroed-out snapshot with empty logs and history arrays', () =>
|
|
{
|
|
const snap = createEmptyMonitorSnapshot();
|
|
|
|
expect(snap.usageCurrentWindow).toBe(0);
|
|
expect(snap.usageLimitPerWindow).toBe(0);
|
|
expect(snap.isHeavy).toBe(false);
|
|
expect(snap.killedRemainingSeconds).toBe(0);
|
|
expect(snap.logs).toEqual([]);
|
|
expect(snap.history).toEqual([]);
|
|
});
|
|
|
|
it('returns fresh arrays each call (no shared state)', () =>
|
|
{
|
|
const a = createEmptyMonitorSnapshot();
|
|
const b = createEmptyMonitorSnapshot();
|
|
|
|
expect(a.logs).not.toBe(b.logs);
|
|
expect(a.history).not.toBe(b.history);
|
|
});
|
|
});
|
|
|
|
describe('formatMonitorLatestOccurrence', () =>
|
|
{
|
|
const NOW = 1_700_000_000_000;
|
|
|
|
it('returns "/" when no occurrence has been recorded yet', () =>
|
|
{
|
|
expect(formatMonitorLatestOccurrence(0, NOW)).toBe('/');
|
|
expect(formatMonitorLatestOccurrence(-1, NOW)).toBe('/');
|
|
});
|
|
|
|
it('returns "Just now" for diffs under 5 seconds', () =>
|
|
{
|
|
const occurredAt = NOW / 1000;
|
|
expect(formatMonitorLatestOccurrence(occurredAt, NOW)).toBe('Just now');
|
|
});
|
|
|
|
it('returns "<n>s ago" for diffs under a minute', () =>
|
|
{
|
|
const tenSecondsAgo = (NOW - 10_000) / 1000;
|
|
expect(formatMonitorLatestOccurrence(tenSecondsAgo, NOW)).toBe('10s ago');
|
|
});
|
|
|
|
it('returns "<n>m ago" for diffs under an hour', () =>
|
|
{
|
|
const fiveMinutesAgo = (NOW - 5 * 60 * 1000) / 1000;
|
|
expect(formatMonitorLatestOccurrence(fiveMinutesAgo, NOW)).toBe('5m ago');
|
|
});
|
|
|
|
it('returns "<n>h ago" for diffs under a day', () =>
|
|
{
|
|
const threeHoursAgo = (NOW - 3 * 60 * 60 * 1000) / 1000;
|
|
expect(formatMonitorLatestOccurrence(threeHoursAgo, NOW)).toBe('3h ago');
|
|
});
|
|
|
|
it('returns "<n>d ago" for older diffs', () =>
|
|
{
|
|
const twoDaysAgo = (NOW - 2 * 24 * 60 * 60 * 1000) / 1000;
|
|
expect(formatMonitorLatestOccurrence(twoDaysAgo, NOW)).toBe('2d ago');
|
|
});
|
|
});
|
|
|
|
describe('formatMonitorHistoryOccurrence', () =>
|
|
{
|
|
it('returns "/" for non-positive timestamps', () =>
|
|
{
|
|
expect(formatMonitorHistoryOccurrence(0)).toBe('/');
|
|
expect(formatMonitorHistoryOccurrence(-5)).toBe('/');
|
|
});
|
|
|
|
it('returns a non-empty formatted string for a real timestamp', () =>
|
|
{
|
|
const out = formatMonitorHistoryOccurrence(1_700_000_000);
|
|
expect(out).not.toBe('/');
|
|
expect(out.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('formatVariableTimestamp', () =>
|
|
{
|
|
it('returns "/" for zero, negative, or falsy values', () =>
|
|
{
|
|
expect(formatVariableTimestamp(0)).toBe('/');
|
|
expect(formatVariableTimestamp(-1)).toBe('/');
|
|
expect(formatVariableTimestamp(null)).toBe('/');
|
|
});
|
|
|
|
it('formats a positive epoch-seconds value as a locale string', () =>
|
|
{
|
|
const out = formatVariableTimestamp(1_700_000_000);
|
|
expect(out).not.toBe('/');
|
|
expect(out.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('formatMonitorSource', () =>
|
|
{
|
|
it('falls back to "Room monitor" when both label and id are missing', () =>
|
|
{
|
|
expect(formatMonitorSource('', 0)).toBe('Room monitor');
|
|
expect(formatMonitorSource('', -1)).toBe('Room monitor');
|
|
});
|
|
|
|
it('returns just the label when there is no source id', () =>
|
|
{
|
|
expect(formatMonitorSource('wired-trigger', 0)).toBe('wired-trigger');
|
|
});
|
|
|
|
it('appends "(#<id>)" when source id is positive', () =>
|
|
{
|
|
expect(formatMonitorSource('on-walk', 42)).toBe('on-walk (#42)');
|
|
});
|
|
|
|
it('uses "wired" as default label when only the id is set', () =>
|
|
{
|
|
expect(formatMonitorSource('', 7)).toBe('wired (#7)');
|
|
});
|
|
});
|
|
|
|
describe('normalizeMonitorReason', () =>
|
|
{
|
|
it('returns the trimmed reason when one is provided', () =>
|
|
{
|
|
expect(normalizeMonitorReason(' loop detected ')).toBe('loop detected');
|
|
});
|
|
|
|
it('falls back to a placeholder when the reason is empty or whitespace', () =>
|
|
{
|
|
expect(normalizeMonitorReason('')).toContain('No detailed reason');
|
|
expect(normalizeMonitorReason(' ')).toContain('No detailed reason');
|
|
expect(normalizeMonitorReason(null)).toContain('No detailed reason');
|
|
});
|
|
});
|
|
});
|