mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +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:
@@ -261,7 +261,7 @@ into `configurePreviewServer` so `yarn preview` keeps working.
|
||||
| God-hook split (state + actions + shim) | `doorbell`, `poll`, `furni-chooser`, `user-chooser`, `friend-request`, `chat-input` |
|
||||
| God-hook split (`useBetween` singleton + state filter + actions filter + shim) | `wired-tools`, `translation`, `notification`, `friends` |
|
||||
| `WidgetErrorBoundary` | `RoomWidgetsView` umbrella |
|
||||
| Vitest | 113/113 cases on pure helpers + the Zustand store |
|
||||
| Vitest | 124/124 cases — 113 on pure helpers + Zustand store, plus the first 2 component-/hook-level pilots (WidgetErrorBoundary, useDoorbellState) on top of the new renderer-SDK mock at `tests/mocks/renderer-mock.ts` |
|
||||
| Form Actions | Login / Register / Forgot (LoginView.tsx) |
|
||||
| Cherry-picked from `duckietm` PR #126 | `UserAccountSettingsView` (reset password / email / username under user settings), plus the wear-badge popup `canShowWearButton` gating |
|
||||
|
||||
@@ -271,7 +271,7 @@ into `configurePreviewServer` so `yarn preview` keeps working.
|
||||
| Split `useChatWidget` / `useAvatarInfoWidget` | Both state-driven via events with no clean imperative actions to extract — skip-motivated. Already touched today for the InfoStand listener move. |
|
||||
| Split `usePetPackageWidget` / `useWordQuizWidget` / `useChatCommandSelector` | Their "actions" mutate internal state or are tightly interdependent — skip-motivated. |
|
||||
| Hoist Wired Creator Tools shared state to a Zustand slice | Would remove ~25 props passed to the 3 tab sub-components. (Wired-tools split done as singleton-filter; Zustand slice is the next step.) |
|
||||
| Wider Vitest coverage (React components) | `@testing-library/*` is installed; needs a small renderer-SDK mock layer first. |
|
||||
| Widen the component / hook test coverage | Mock layer is in place (`tests/mocks/renderer-mock.ts`) and the first 2 pilots pass. Good follow-up targets: other `*State` hooks built on event reducers, `LoginView` Form Actions happy/error paths, OfferView with `useNitroQuery`. |
|
||||
|
||||
## Known open logic bugs
|
||||
|
||||
@@ -323,3 +323,11 @@ Fix shapes documented; both are reasonable PRs on their own.
|
||||
- Asset middleware: `nitroAssetsServer()` in `vite.config.mjs`
|
||||
- Configuration pre-init: `src/bootstrap.ts` (`await GetConfiguration().init()`
|
||||
before `import('./index')`)
|
||||
- Renderer-SDK mock for Vitest: `tests/mocks/renderer-mock.ts`
|
||||
(aliased over `@nitrots/nitro-renderer` via `vitest.config.mts`).
|
||||
Hosts the explicit `NitroLogger` mock, the `mockEventDispatcher` /
|
||||
`clearMockEventDispatcher` helpers used by hook tests, the
|
||||
`RoomSessionDoorbellEvent` stub, and a long list of placeholder
|
||||
classes/enums kept around just so the `src/api/*` barrel cascade
|
||||
imports without throwing. **Grow this file when a new test needs a
|
||||
symbol; prefer real deterministic stubs over `vi.fn()`.**
|
||||
|
||||
+44
-13
@@ -484,7 +484,7 @@ Status after this round of work:
|
||||
- Vitest 3 + jsdom + `@testing-library/react` + `@testing-library/jest-dom`
|
||||
configured. Separate `vitest.config.mts` so the runner doesn't drag in
|
||||
the renderer SDK aliases from `vite.config.mjs`.
|
||||
- **113 cases passing** across 8 test files:
|
||||
- **124 cases passing** across 10 test files. Pure-module suites:
|
||||
- `WiredCreatorTools.helpers.test.ts` (18) — formatters + snapshot
|
||||
factory.
|
||||
- `navigatorRoomCreatorStore.test.ts` (4) — Zustand store invariants
|
||||
@@ -504,13 +504,40 @@ Status after this round of work:
|
||||
bail-out branches (state-not-AvatarInfoUser, mismatched
|
||||
user/roomIndex, equal-after-dedup) + the figure / favorite-group
|
||||
apply paths.
|
||||
- **Pure-module convention**: tests live in `tests/` and import from
|
||||
concrete file paths (e.g. `../src/api/catalog/CatalogType`) rather
|
||||
than the api barrel, so jsdom doesn't transitively load the renderer
|
||||
SDK's Pixi-bound modules. Renderer event type imports use
|
||||
`import type { … }` so they're erased at compile time and don't
|
||||
trigger the runtime module load either.
|
||||
- `yarn test` + `yarn test:watch` scripts added.
|
||||
|
||||
Component-/hook-level suites (on the new renderer-SDK mock):
|
||||
- `WidgetErrorBoundary.test.tsx` (4) — happy path + caught render
|
||||
error logged via `NitroLogger.error` + custom fallback +
|
||||
`unknown` default name.
|
||||
- `useDoorbellState.test.tsx` (7) — initial empty state, append on
|
||||
`DOORBELL`, dedup duplicates, remove on `RSDE_ACCEPTED` /
|
||||
`RSDE_REJECTED`, ignore stale events, unsubscribe on unmount.
|
||||
|
||||
- **Renderer-SDK mock at `tests/mocks/renderer-mock.ts`** —
|
||||
`vitest.config.mts` aliases `@nitrots/nitro-renderer` over this file
|
||||
so jsdom-hosted tests never load Pixi or the message
|
||||
parser/composer registry. The mock exports:
|
||||
- Explicit, behavioral stubs for the symbols tests actually
|
||||
exercise: `NitroLogger`, `GetEventDispatcher`,
|
||||
`mockEventDispatcher` / `clearMockEventDispatcher` helpers, the
|
||||
`RoomSessionDoorbellEvent` class (signature mirrors the real
|
||||
`(type, session, userName)` so `tsgo` stays happy).
|
||||
- String-keyed `Proxy` enums for every `*EventType` /
|
||||
`*FigurePartType` / `RoomObjectCategory` etc. — each access
|
||||
returns a stable unique string so dispatch + listener agree.
|
||||
- Lightweight `class StubClass {}` placeholders for the ~30 Pixi
|
||||
and gameplay classes the `src/api/*` barrel touches at import
|
||||
time (`NitroAlphaFilter`, `NitroContainer`, `EventDispatcher`,
|
||||
etc.). Keeps the cascade from throwing without simulating
|
||||
behavior tests don't care about.
|
||||
- Singleton getters (`GetAssetManager`, `GetCommunication`,
|
||||
`GetSessionDataManager`, …) returning a chainable proxy so
|
||||
`GetX().y.z` evaluates to a no-op proxy instead of crashing.
|
||||
- **Pure-module convention** (still applies for non-component tests):
|
||||
import from concrete file paths so jsdom doesn't transitively load
|
||||
the renderer SDK; use `import type { … }` for type-only renderer
|
||||
imports.
|
||||
- `yarn test` + `yarn test:watch` scripts.
|
||||
|
||||
### Logic bug fixes
|
||||
- Doorbell close button didn't close while users were pending
|
||||
@@ -648,11 +675,15 @@ Remaining order of value/risk for the next contributor:
|
||||
token; the `LayoutFurniImageView` / `LayoutAvatarImageView` async
|
||||
fetch race needs a request-id ref (or is solved by migrating the
|
||||
image fetch to `useNitroQuery` keyed on props).
|
||||
6. **Wider Vitest coverage** — next worthwhile targets: the
|
||||
`useNitroQuery` adapter (timeout + cleanup + accept-filter
|
||||
behavior, needs a stub for `@nitrots/nitro-renderer`),
|
||||
`useDoorbellState`/`useUserChooserState` event-reducer logic
|
||||
(needs the same renderer stub).
|
||||
6. **Widen the component/hook Vitest coverage.** The renderer-SDK
|
||||
mock layer is in place (`tests/mocks/renderer-mock.ts`) and the
|
||||
first two pilots — `WidgetErrorBoundary` and `useDoorbellState` —
|
||||
pass. Good follow-up targets: other `*State` hooks built on event
|
||||
reducers (`useFurniChooserState`, `useUserChooserState`,
|
||||
`useFriendRequestState`, `useChatInputState`), the `useNitroQuery`
|
||||
adapter (timeout + cleanup + accept-filter behavior), and the
|
||||
`LoginView` Form Actions happy/error paths. Each new test will
|
||||
likely need to add 1-3 named exports to the renderer mock.
|
||||
|
||||
Skipped intentionally and documented in commit messages:
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { NitroLogger } from '@nitrots/nitro-renderer';
|
||||
import { cleanup, render, screen } from '@testing-library/react';
|
||||
import { FC } from 'react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { WidgetErrorBoundary } from '../src/common/error-boundary/WidgetErrorBoundary';
|
||||
|
||||
// `import { NitroLogger } from '@nitrots/nitro-renderer'` resolves to
|
||||
// `tests/mocks/renderer-mock.ts` via the alias in vitest.config.mts.
|
||||
// The SUT imports the same path, so both reach the same vi.fn instance.
|
||||
|
||||
describe('WidgetErrorBoundary', () =>
|
||||
{
|
||||
beforeEach(() =>
|
||||
{
|
||||
vi.clearAllMocks();
|
||||
// react-error-boundary lets React's "uncaught error" log through
|
||||
// by default — silence it so jsdom doesn't dump a stack trace
|
||||
// every time we deliberately throw below.
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() =>
|
||||
{
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders its children when nothing throws', () =>
|
||||
{
|
||||
render(
|
||||
<WidgetErrorBoundary name="HappyPath">
|
||||
<span data-testid="child">visible</span>
|
||||
</WidgetErrorBoundary>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('child')).toHaveTextContent('visible');
|
||||
});
|
||||
|
||||
it('swallows a render-time error to a silent fallback and logs it', () =>
|
||||
{
|
||||
const Boom: FC = () =>
|
||||
{
|
||||
throw new Error('kaboom');
|
||||
};
|
||||
|
||||
const { container } = render(
|
||||
<WidgetErrorBoundary name="Boom">
|
||||
<Boom />
|
||||
</WidgetErrorBoundary>
|
||||
);
|
||||
|
||||
// Default fallback is `() => null` → boundary subtree is empty.
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
|
||||
expect(NitroLogger.error).toHaveBeenCalledTimes(1);
|
||||
const [ message, err ] = (NitroLogger.error as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
expect(message).toBe('[Widget:Boom] crashed');
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
expect((err as Error).message).toBe('kaboom');
|
||||
});
|
||||
|
||||
it('renders a custom fallback node when provided', () =>
|
||||
{
|
||||
const Boom: FC = () =>
|
||||
{
|
||||
throw new Error('explode');
|
||||
};
|
||||
|
||||
render(
|
||||
<WidgetErrorBoundary name="WithFallback" fallback={ <div data-testid="fb">offline</div> }>
|
||||
<Boom />
|
||||
</WidgetErrorBoundary>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('fb')).toHaveTextContent('offline');
|
||||
});
|
||||
|
||||
it('uses "unknown" as the widget name when the prop is omitted', () =>
|
||||
{
|
||||
const Boom: FC = () =>
|
||||
{
|
||||
throw new Error('anonymous');
|
||||
};
|
||||
|
||||
render(
|
||||
<WidgetErrorBoundary>
|
||||
<Boom />
|
||||
</WidgetErrorBoundary>
|
||||
);
|
||||
|
||||
expect(NitroLogger.error).toHaveBeenCalledTimes(1);
|
||||
expect((NitroLogger.error as ReturnType<typeof vi.fn>).mock.calls[0][0]).toBe('[Widget:unknown] crashed');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* Stub of `@nitrots/nitro-renderer` for Vitest.
|
||||
*
|
||||
* The real package eagerly loads Pixi v8 + a few hundred Habbo message
|
||||
* parsers/composers at module import time, which jsdom cannot host:
|
||||
* any `tests/**` file that transitively pulls a renderer symbol throws
|
||||
* before a single assertion runs.
|
||||
*
|
||||
* This module replaces it via `resolve.alias` in `vitest.config.mts`.
|
||||
* We provide explicit named exports for the symbols a test currently
|
||||
* needs (logger, event dispatcher, doorbell event class); everything
|
||||
* else mentioned in the comments below is a generic stub kept just to
|
||||
* keep the side-effectful imports happy as `src/api/index.ts` and
|
||||
* friends are pulled in transitively by the barrel cascade.
|
||||
*
|
||||
* Grow this file as new tests require additional symbols. Prefer adding
|
||||
* a real (deterministic) stub over wiring `vi.fn()` — it keeps the
|
||||
* mocks readable and avoids state bleed between cases.
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Logger
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const NitroLogger = {
|
||||
log: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
enableContexts: vi.fn(),
|
||||
setDebug: vi.fn()
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Event dispatcher
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// `GetEventDispatcher()` in the real SDK returns the renderer-wide event
|
||||
// bus. Tests use `mockEventDispatcher.dispatchEvent(event)` to simulate
|
||||
// a server push. `clearMockEventDispatcher()` resets the listener map
|
||||
// between cases so subscriptions from a previous test don't leak.
|
||||
|
||||
type Listener = (event: any) => void;
|
||||
|
||||
const listeners = new Map<string, Set<Listener>>();
|
||||
|
||||
export const mockEventDispatcher = {
|
||||
addEventListener(type: string, handler: Listener)
|
||||
{
|
||||
let bucket = listeners.get(type);
|
||||
|
||||
if(!bucket)
|
||||
{
|
||||
bucket = new Set();
|
||||
listeners.set(type, bucket);
|
||||
}
|
||||
|
||||
bucket.add(handler);
|
||||
},
|
||||
removeEventListener(type: string, handler: Listener)
|
||||
{
|
||||
listeners.get(type)?.delete(handler);
|
||||
},
|
||||
dispatchEvent(event: { type: string })
|
||||
{
|
||||
const bucket = listeners.get(event.type);
|
||||
|
||||
if(!bucket) return;
|
||||
|
||||
for(const handler of bucket) handler(event);
|
||||
},
|
||||
hasListeners(type: string)
|
||||
{
|
||||
return (listeners.get(type)?.size ?? 0) > 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const clearMockEventDispatcher = () =>
|
||||
{
|
||||
listeners.clear();
|
||||
};
|
||||
|
||||
export const GetEventDispatcher = vi.fn(() => mockEventDispatcher);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Event type enums (string-keyed Proxies)
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// The real `*EventType` is a `class { static readonly FOO = '...'; … }`
|
||||
// with stable wire strings. Tests only need each constant to be a
|
||||
// unique string so dispatch + listener agree.
|
||||
|
||||
const makeEnumProxy = (label: string) => new Proxy({}, {
|
||||
get: (_, prop) => typeof prop === 'string' ? `mock:${ label }:${ prop }` : undefined
|
||||
}) as Record<string, string>;
|
||||
|
||||
export const NitroEventType = makeEnumProxy('NitroEventType');
|
||||
export const MouseEventType = makeEnumProxy('MouseEventType');
|
||||
export const TouchEventType = makeEnumProxy('TouchEventType');
|
||||
export const RoomObjectCategory = makeEnumProxy('RoomObjectCategory');
|
||||
export const RoomObjectPlacementSource = makeEnumProxy('RoomObjectPlacementSource');
|
||||
export const RoomObjectType = makeEnumProxy('RoomObjectType');
|
||||
export const RoomObjectVariable = makeEnumProxy('RoomObjectVariable');
|
||||
export const RoomControllerLevel = makeEnumProxy('RoomControllerLevel');
|
||||
export const RoomTradingLevelEnum = makeEnumProxy('RoomTradingLevelEnum');
|
||||
export const HabboClubLevelEnum = makeEnumProxy('HabboClubLevelEnum');
|
||||
export const FurnitureType = makeEnumProxy('FurnitureType');
|
||||
export const PetType = makeEnumProxy('PetType');
|
||||
export const AvatarFigurePartType = makeEnumProxy('AvatarFigurePartType');
|
||||
export const AvatarScaleType = makeEnumProxy('AvatarScaleType');
|
||||
export const AvatarSetType = makeEnumProxy('AvatarSetType');
|
||||
export const AvatarAction = makeEnumProxy('AvatarAction');
|
||||
export const RoomWidgetEnumItemExtradataParameter = makeEnumProxy('RoomWidgetEnumItemExtradataParameter');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Doorbell event class
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export class RoomSessionDoorbellEvent
|
||||
{
|
||||
// Wire strings copied from the real class so any consumer that
|
||||
// ignores the indirection through the `.DOORBELL` static still
|
||||
// matches.
|
||||
static readonly DOORBELL = 'RSDE_DOORBELL';
|
||||
static readonly RSDE_ACCEPTED = 'RSDE_ACCEPTED';
|
||||
static readonly RSDE_REJECTED = 'RSDE_REJECTED';
|
||||
|
||||
// Mirrors the real constructor signature `(type, session, userName)`
|
||||
// so `tsgo` is happy. Tests can pass `null` for the session: the
|
||||
// SUT only reads `.userName` and `.type`.
|
||||
constructor(public readonly type: string, public readonly _session: unknown, public readonly userName: string) {}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Generic classes — placeholders for symbols that need to exist as
|
||||
// constructors so module-level `new X(...)` calls don't crash during
|
||||
// the barrel cascade, but whose behavior tests don't yet exercise.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class StubClass
|
||||
{
|
||||
constructor(..._args: unknown[]) {}
|
||||
}
|
||||
|
||||
export class NitroAlphaFilter extends StubClass {}
|
||||
export class NitroContainer extends StubClass {}
|
||||
export class NitroRectangle extends StubClass {}
|
||||
export class NitroSprite extends StubClass {}
|
||||
export class NitroTexture extends StubClass {}
|
||||
export class NitroSoundEvent extends StubClass {}
|
||||
export class NitroEvent extends StubClass {}
|
||||
export class MessageEvent extends StubClass {}
|
||||
export class RoomEngineObjectEvent extends StubClass {}
|
||||
export class CreateLinkEvent extends StubClass {}
|
||||
export class EventDispatcher extends StubClass {}
|
||||
export class AdvancedMap extends StubClass {}
|
||||
export class AvatarFigureContainer extends StubClass {}
|
||||
export class Vector3d extends StubClass {}
|
||||
export class ObjectDataFactory extends StubClass {}
|
||||
export class RoomDataParser extends StubClass {}
|
||||
export class RoomModerationSettings extends StubClass {}
|
||||
export class StringDataType extends StubClass {}
|
||||
export class SellablePetPaletteData extends StubClass {}
|
||||
export class PetFigureData extends StubClass {}
|
||||
export class PetData extends StubClass {}
|
||||
export class NodeData extends StubClass {}
|
||||
export class ItemDataStructure extends StubClass {}
|
||||
export class HabboGroupEntryData extends StubClass {}
|
||||
export class FriendParser extends StubClass {}
|
||||
export class FriendCategoryData extends StubClass {}
|
||||
export class FriendRequestData extends StubClass {}
|
||||
export class FurnitureListItemParser extends StubClass {}
|
||||
export class BotData extends StubClass {}
|
||||
export class AchievementData extends StubClass {}
|
||||
export class CatalogPageMessageProductData extends StubClass {}
|
||||
export class GiftWrappingConfigurationParser extends StubClass {}
|
||||
export class WiredFilter extends StubClass {}
|
||||
export class HabboWebTools extends StubClass {}
|
||||
|
||||
// Composers — symbol-only constructors; only their identity matters in the
|
||||
// codebase ("did the SUT call SendMessageComposer(new FooComposer(args))").
|
||||
export class AddFavouriteRoomMessageComposer extends StubClass {}
|
||||
export class DeleteFavouriteRoomMessageComposer extends StubClass {}
|
||||
export class DesktopViewComposer extends StubClass {}
|
||||
export class FurniturePlacePaintComposer extends StubClass {}
|
||||
export class GetGuestRoomMessageComposer extends StubClass {}
|
||||
export class GetProductOfferComposer extends StubClass {}
|
||||
export class GroupFavoriteComposer extends StubClass {}
|
||||
export class GroupInformationComposer extends StubClass {}
|
||||
export class GroupJoinComposer extends StubClass {}
|
||||
export class GroupUnfavoriteComposer extends StubClass {}
|
||||
export class UserProfileComposer extends StubClass {}
|
||||
|
||||
// `ChooserSelectionFilter` is used as a string enum in some call sites.
|
||||
export const ChooserSelectionFilter = makeEnumProxy('ChooserSelectionFilter');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Singleton getters
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const stubManager = () =>
|
||||
{
|
||||
const sentinel = new Proxy(() => undefined, {
|
||||
get(target, prop)
|
||||
{
|
||||
if(prop === 'then') return undefined;
|
||||
const cached = (target as any)[prop];
|
||||
if(cached !== undefined) return cached;
|
||||
// Most fields read from a real manager are either methods
|
||||
// (return functions) or sub-objects (return proxies). We
|
||||
// return another callable proxy so chained access works.
|
||||
const value = stubManager();
|
||||
(target as any)[prop] = value;
|
||||
return value;
|
||||
},
|
||||
apply()
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
return sentinel;
|
||||
};
|
||||
|
||||
export const GetAssetManager = vi.fn(stubManager);
|
||||
export const GetAvatarRenderManager = vi.fn(stubManager);
|
||||
export const GetCommunication = vi.fn(stubManager);
|
||||
export const GetConfiguration = vi.fn(stubManager);
|
||||
export const GetLocalizationManager = vi.fn(stubManager);
|
||||
export const GetRoomEngine = vi.fn(stubManager);
|
||||
export const GetRoomSessionManager = vi.fn(stubManager);
|
||||
export const GetSessionDataManager = vi.fn(stubManager);
|
||||
export const GetTickerTime = vi.fn(() => 0);
|
||||
export const TextureUtils = stubManager();
|
||||
export const NitroVersion = stubManager();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type-only re-exports (interfaces erase at compile time, but listing them
|
||||
// here documents what the codebase imports through the type channel).
|
||||
//
|
||||
// IAvatarFigureContainer · IEventDispatcher · IFigurePart · IFigurePartSet ·
|
||||
// IFurnitureData · IFurnitureItemData · IGraphicAsset · IMessageComposer ·
|
||||
// IMessageEvent · IObjectData · IPartColor · IPollQuestion · IProductData ·
|
||||
// IRoomEngine · IRoomModerationSettings · IRoomObject · IRoomObjectController ·
|
||||
// IRoomObjectSpriteVisualization · IRoomPetData · IRoomSession · IRoomUserData
|
||||
//
|
||||
// No need to alias them — TypeScript only consults them in the type
|
||||
// system, and the production `tsconfig.json` resolves them against the
|
||||
// real renderer via `node_modules/@nitrots/nitro-renderer/src`.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Catch-all
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Anything else still resolves to `undefined`. If a test fails with
|
||||
// "X is not a constructor" / "X.SOMETHING is not a function", add the
|
||||
// missing symbol above with a real stub. Avoid the temptation to
|
||||
// blanket-mock everything — explicit stubs surface intent and let
|
||||
// failing tests pinpoint what behavior they actually rely on.
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
+7
-2
@@ -5,8 +5,12 @@ import { resolve } from 'path';
|
||||
* Test runner config — kept separate from vite.config.mjs because the
|
||||
* dev/build config wires up the renderer SDK via filesystem aliases that
|
||||
* point at sibling working trees (`../renderer`, `../Nitro_Render_V3`).
|
||||
* Tests are deliberately written against pure modules (helpers, stores)
|
||||
* that don't pull in the renderer.
|
||||
*
|
||||
* Test files were originally written against pure modules (helpers,
|
||||
* stores) that don't pull in the renderer. We now also support
|
||||
* component-level tests by aliasing `@nitrots/nitro-renderer` to a
|
||||
* hand-written stub at `tests/mocks/renderer-mock.ts` so jsdom doesn't
|
||||
* try to evaluate Pixi + the full message parser/composer registry.
|
||||
*/
|
||||
export default defineConfig({
|
||||
test: {
|
||||
@@ -18,6 +22,7 @@ export default defineConfig({
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@nitrots/nitro-renderer': resolve(__dirname, 'tests/mocks/renderer-mock.ts'),
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user