mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 15:36:18 +00:00
tests: add renderer-SDK mock layer + first 2 component-/hook-level pilots
Foundations for widening Vitest coverage past the pure-helper subset.
The real `@nitrots/nitro-renderer` eagerly loads Pixi v8 and the full
Habbo message parser/composer registry at module-import time, which
jsdom cannot host: any `tests/**` file that transitively pulled a
renderer symbol would throw before a single assertion ran. That's
why the existing 8 suites all stuck to pure modules imported by
concrete path and used `import type` for renderer-side names.
Add a stub at `tests/mocks/renderer-mock.ts`, aliased over the package
via `vitest.config.mts`. It exports:
- Explicit behavioral stubs for the symbols tests actually exercise:
`NitroLogger`, `GetEventDispatcher`, the `mockEventDispatcher`
helper with `addEventListener` / `removeEventListener` /
`dispatchEvent` / `hasListeners`, and `RoomSessionDoorbellEvent`
(signature matches the real `(type, session, userName)` to keep
tsgo happy).
- String-keyed Proxy enums for `NitroEventType`, `RoomObjectCategory`,
`AvatarFigurePartType`, etc. — each access returns a stable unique
string so dispatch and listener agree.
- Lightweight `class StubClass {}` placeholders for the ~30 Pixi and
gameplay classes the `src/api/*` barrel touches at import time
(`NitroAlphaFilter`, `NitroContainer`, `EventDispatcher`, …).
Keeps the cascade from throwing without simulating behavior tests
don't care about.
- Singleton getters (`GetAssetManager`, `GetCommunication`,
`GetSessionDataManager`, …) returning a chainable Proxy so deeply
nested `GetX().y.z(…)` access evaluates to no-op proxies.
Pilots on top of that layer (each one designed to catch a different
class of regression):
- `tests/WidgetErrorBoundary.test.tsx` (4 cases) — happy path,
default silent fallback + `NitroLogger.error` call, custom
fallback node, default `unknown` widget name.
- `tests/useDoorbellState.test.tsx` (7 cases) — initial empty state,
append on `RSDE_DOORBELL`, dedup duplicate names, remove on
`RSDE_ACCEPTED` / `RSDE_REJECTED`, ignore stale events for
never-pending users, full unsubscribe on unmount.
Suite count now 124/124 across 10 files (was 113/113 across 8).
`yarn typecheck` still green.
Docs: CLAUDE.md's Vitest row and "Where everything lives" pointer
updated; `docs/ARCHITECTURE.md` Tests section now lists the new
suites + a description of what the mock layer covers, and the
"Wider Vitest coverage" entry in the next-steps list is reframed
from "needs a renderer mock" to "pick the next adopter".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { RoomSessionDoorbellEvent } from '@nitrots/nitro-renderer';
|
||||
import { act, cleanup, renderHook } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { useDoorbellState } from '../src/hooks/rooms/widgets/useDoorbellState';
|
||||
import { clearMockEventDispatcher, mockEventDispatcher } from './mocks/renderer-mock';
|
||||
|
||||
// Server push helper — mirrors the renderer wire by emitting the same
|
||||
// constants the SUT listens to. The real constructor takes a session
|
||||
// reference too; pass null since the SUT only reads `.userName`.
|
||||
const dispatchDoorbell = (type: string, userName: string) =>
|
||||
{
|
||||
act(() =>
|
||||
{
|
||||
mockEventDispatcher.dispatchEvent(new RoomSessionDoorbellEvent(type, null as any, userName));
|
||||
});
|
||||
};
|
||||
|
||||
describe('useDoorbellState', () =>
|
||||
{
|
||||
beforeEach(() =>
|
||||
{
|
||||
clearMockEventDispatcher();
|
||||
});
|
||||
|
||||
afterEach(() =>
|
||||
{
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('starts with no users pending', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorbellState());
|
||||
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
|
||||
it('appends the username from a DOORBELL event', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorbellState());
|
||||
|
||||
dispatchDoorbell(RoomSessionDoorbellEvent.DOORBELL, 'Alice');
|
||||
|
||||
expect(result.current).toEqual([ 'Alice' ]);
|
||||
});
|
||||
|
||||
it('ignores duplicate DOORBELL events for the same username', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorbellState());
|
||||
|
||||
dispatchDoorbell(RoomSessionDoorbellEvent.DOORBELL, 'Alice');
|
||||
dispatchDoorbell(RoomSessionDoorbellEvent.DOORBELL, 'Alice');
|
||||
|
||||
expect(result.current).toEqual([ 'Alice' ]);
|
||||
});
|
||||
|
||||
it('removes a user on RSDE_ACCEPTED while keeping the others', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorbellState());
|
||||
|
||||
dispatchDoorbell(RoomSessionDoorbellEvent.DOORBELL, 'Alice');
|
||||
dispatchDoorbell(RoomSessionDoorbellEvent.DOORBELL, 'Bob');
|
||||
dispatchDoorbell(RoomSessionDoorbellEvent.RSDE_ACCEPTED, 'Alice');
|
||||
|
||||
expect(result.current).toEqual([ 'Bob' ]);
|
||||
});
|
||||
|
||||
it('removes a user on RSDE_REJECTED', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorbellState());
|
||||
|
||||
dispatchDoorbell(RoomSessionDoorbellEvent.DOORBELL, 'Carol');
|
||||
dispatchDoorbell(RoomSessionDoorbellEvent.RSDE_REJECTED, 'Carol');
|
||||
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
|
||||
it('ignores accept/reject events for users that were never pending', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorbellState());
|
||||
|
||||
dispatchDoorbell(RoomSessionDoorbellEvent.RSDE_ACCEPTED, 'Ghost');
|
||||
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
|
||||
it('unsubscribes from all three events on unmount', () =>
|
||||
{
|
||||
const { unmount } = renderHook(() => useDoorbellState());
|
||||
|
||||
expect(mockEventDispatcher.hasListeners(RoomSessionDoorbellEvent.DOORBELL)).toBe(true);
|
||||
expect(mockEventDispatcher.hasListeners(RoomSessionDoorbellEvent.RSDE_ACCEPTED)).toBe(true);
|
||||
expect(mockEventDispatcher.hasListeners(RoomSessionDoorbellEvent.RSDE_REJECTED)).toBe(true);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(mockEventDispatcher.hasListeners(RoomSessionDoorbellEvent.DOORBELL)).toBe(false);
|
||||
expect(mockEventDispatcher.hasListeners(RoomSessionDoorbellEvent.RSDE_ACCEPTED)).toBe(false);
|
||||
expect(mockEventDispatcher.hasListeners(RoomSessionDoorbellEvent.RSDE_REJECTED)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user