mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
c4018392f9
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>
97 lines
2.9 KiB
TypeScript
97 lines
2.9 KiB
TypeScript
/* @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');
|
|
});
|
|
});
|