Files
Nitro-V3/src/nitro-renderer.mock.ts
T

614 lines
26 KiB
TypeScript

/**
* 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;
// NitroEvent listeners — registered via GetEventDispatcher() / useNitroEvent.
// Cleared by clearMockEventDispatcher() between test cases.
const listeners = new Map<string, Set<Listener>>();
// MessageEvent listeners — registered via GetCommunication().registerMessageEvent
// (i.e. useMessageEvent). NOT cleared by clearMockEventDispatcher() so that
// useBetween-based hooks (which register effects once and persist the
// singleton across tests) keep their subscriptions alive throughout the
// suite. State isolation between tests is maintained by the useBetween
// instance preserving INITIAL values across renders (each test's renderHook
// shares the same useBetween singleton — tests that check a specific
// post-dispatch state rely on the event changing it, not on a reset).
const msgListeners = 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 })
{
// Fire NitroEvent listeners first, then MessageEvent listeners.
const bucket = listeners.get(event.type);
if(bucket) for(const handler of bucket) handler(event);
const msgBucket = msgListeners.get(event.type);
if(msgBucket) for(const handler of msgBucket) handler(event);
},
hasListeners(type: string)
{
return (listeners.get(type)?.size ?? 0) > 0 ||
(msgListeners.get(type)?.size ?? 0) > 0;
}
};
// Clears only the NitroEvent listener map (GetEventDispatcher / useNitroEvent
// registrations). MessageEvent listeners (useMessageEvent / GetCommunication)
// are intentionally preserved so useBetween-based hooks stay subscribed.
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 RoomObjectPlacementSource = makeEnumProxy('RoomObjectPlacementSource');
export const RoomObjectType = makeEnumProxy('RoomObjectType');
export const RoomObjectVariable = makeEnumProxy('RoomObjectVariable');
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');
// Numeric enums — values mirror the real renderer SDK so comparisons
// (`controllerLevel >= GUILD_ADMIN`, category branching) keep working.
export class RoomControllerLevel
{
static readonly NONE = 0;
static readonly GUEST = 1;
static readonly GUILD_MEMBER = 2;
static readonly GUILD_ADMIN = 3;
static readonly ROOM_OWNER = 4;
static readonly MODERATOR = 5;
}
// Mirrors `packages/api/src/nitro/session/enum/SecurityLevel.ts`. Kept
// around so any consumer that still imports the renderer enum
// (non-deprecated code path) compiles cleanly under the mock.
export class SecurityLevel
{
static readonly NONE = 0;
static readonly CELEBRITY = 1;
static readonly PARTNER = 2;
static readonly BUS_PARTNER = 3;
static readonly EMPLOYEE = 4;
static readonly MODERATOR = 5;
static readonly PLAYER_SUPPORT = 6;
static readonly COMMUNITY = 7;
static readonly ADMINISTRATOR = 8;
static readonly SUPER_USER = 9;
}
export class RoomObjectCategory
{
static readonly MINIMUM = 0;
static readonly ROOM = 10;
static readonly UNIT = 20;
static readonly FLOOR = 30;
static readonly WALL = 40;
static readonly MAXIMUM = 50;
}
// ---------------------------------------------------------------------------
// 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 {}
// MessageEvent — stores the handler so GetCommunication (below) can
// route dispatches through mockEventDispatcher. Each concrete subclass
// exposes a `.type` equal to its constructor name so dispatchEvent
// can match registered listeners.
export class MessageEvent
{
private _callBack: Function | null;
constructor(callBack?: Function)
{
this._callBack = callBack ?? null;
}
public get callBack(): Function | null { return this._callBack; }
// Each concrete subclass is identified by its class name.
public get type(): string { return this.constructor.name; }
// Concrete subclasses override this; the no-arg construction path used
// by makeParserlessEvent in tests leaves it returning null — tests
// override it with (ev as any).getParser = () => parser.
public getParser(): any { return null; }
}
// ---------------------------------------------------------------------------
// IMessageEvent-based event classes used by useDoorState
//
// The real renderer classes take a `callBack` constructor arg and store it
// in MessageEvent._callBack. The communication manager later calls
// `event.callBack(event)` when the matching packet arrives.
//
// In tests we construct them with NO args (makeParserlessEvent does
// `new klass()`) and override `getParser`. GetCommunication (below)
// registers `event.callBack` on mockEventDispatcher under `event.type`
// (the class name). When the test calls
// `mockEventDispatcher.dispatchEvent(ev)`, listeners for that class name
// fire, receiving `ev` — and the implementation reads `ev.getParser()`.
// ---------------------------------------------------------------------------
export class DoorbellMessageEvent extends MessageEvent {}
export class RoomDoorbellAcceptedEvent extends MessageEvent {}
export class FlatAccessDeniedMessageEvent extends MessageEvent {}
export class GenericErrorEvent extends MessageEvent {}
export class GetGuestRoomResultEvent extends MessageEvent {}
// Mentions system — incoming events extend MessageEvent (they expose
// getParser()); the request/mark composers are symbol-only constructors.
export class MentionReceivedEvent extends MessageEvent {}
export class MentionsListEvent extends MessageEvent {}
// ---------------------------------------------------------------------------
// Navigator event classes — MessageEvent subclasses needed by useNavigatorStore
// ---------------------------------------------------------------------------
export class CanCreateRoomEventEvent extends MessageEvent {}
export class FavouriteChangedEvent extends MessageEvent {}
export class FavouritesEvent extends MessageEvent {}
export class FlatCreatedEvent extends MessageEvent {}
export class NavigatorHomeRoomEvent extends MessageEvent {}
export class NavigatorMetadataEvent extends MessageEvent {}
export class NavigatorOpenRoomCreatorEvent extends MessageEvent {}
export class NavigatorSearchesEvent extends MessageEvent {}
export class NavigatorSearchEvent extends MessageEvent {}
export class RoomEnterErrorEvent extends MessageEvent {}
export class RoomEntryInfoMessageEvent extends MessageEvent {}
export class RoomForwardEvent extends MessageEvent {}
export class RoomScoreEvent extends MessageEvent {}
export class RoomSettingsUpdatedEvent extends MessageEvent {}
export class UserEventCatsEvent extends MessageEvent {}
export class UserFlatCatsEvent extends MessageEvent {}
export class UserInfoEvent extends MessageEvent {}
export class UserPermissionsEvent extends MessageEvent {}
// ---------------------------------------------------------------------------
// Notification event classes — MessageEvent subclasses needed by
// useNotificationStore (called via useNotification() inside useNavigatorStore).
// The real renderer classes take a `callBack` constructor arg; the pattern
// here is the same as the Navigator event stubs above.
// ---------------------------------------------------------------------------
export class AchievementNotificationMessageEvent extends MessageEvent {}
export class ActivityPointNotificationMessageEvent extends MessageEvent {}
export class BadgeReceivedEvent extends MessageEvent {}
export class ClubGiftNotificationEvent extends MessageEvent {}
export class ClubGiftSelectedEvent extends MessageEvent {}
export class ConnectionErrorEvent extends MessageEvent {}
export class HabboBroadcastMessageEvent extends MessageEvent {}
export class HotelClosedAndOpensEvent extends MessageEvent {}
export class HotelClosesAndWillOpenAtEvent extends MessageEvent {}
export class HotelWillCloseInMinutesEvent extends MessageEvent {}
export class InfoFeedEnableMessageEvent extends MessageEvent {}
export class MaintenanceStatusMessageEvent extends MessageEvent {}
export class ModeratorCautionEvent extends MessageEvent {}
export class ModeratorMessageEvent extends MessageEvent {}
export class MOTDNotificationEvent extends MessageEvent {}
export class NotificationDialogMessageEvent extends MessageEvent {}
export class PetLevelNotificationEvent extends MessageEvent {}
export class PetReceivedMessageEvent extends MessageEvent {}
export class RespectReceivedEvent extends MessageEvent {}
export class RoomEnterEvent extends MessageEvent {}
export class SimpleAlertMessageEvent extends MessageEvent {}
export class UserBannedMessageEvent extends MessageEvent {}
export class WiredRewardResultMessageEvent extends MessageEvent
{
static readonly PRODUCT_DONATED_CODE = 7;
static readonly BADGE_DONATED_CODE = 8;
}
// RoomEnterEffect — used by useNotificationStore to check if the room-enter
// animation is still running before showing the mod disclaimer bubble.
export const RoomEnterEffect = {
isRunning: () => false,
totalRunningTime: 0
};
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 {}
// RoomDataParser — real static constants needed by useDoorState and its tests.
export class RoomDataParser
{
static readonly DOORBELL_STATE = 1;
static readonly PASSWORD_STATE = 2;
}
export class RoomModerationSettings extends StubClass {}
export class StringDataType extends StubClass {}
// Navigator data/parser stubs
export class NavigatorCategoryDataParser extends StubClass {}
export class NavigatorEventCategoryDataParser extends StubClass {}
export class NavigatorSavedSearch extends StubClass {}
export class NavigatorSearchResultSet extends StubClass {}
export class NavigatorTopLevelContext extends StubClass {}
export class CantConnectMessageParser extends StubClass
{
static readonly REASON_FULL = 1;
static readonly REASON_QUEUE_ERROR = 2;
static readonly REASON_BANNED = 3;
}
export class LegacyExternalInterface
{
static readonly available = false;
static call(..._args: unknown[]): void {}
}
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 FollowFriendMessageComposer extends StubClass {}
export class GetUserEventCatsMessageComposer extends StubClass {}
export class GetUserFlatCatsMessageComposer extends StubClass {}
export class NavigatorSearchComposer extends StubClass {}
export class RequestMentionsComposer extends StubClass {}
export class MarkMentionsReadComposer extends StubClass {}
export class DeleteMentionComposer 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');
// ---------------------------------------------------------------------------
// Floor plan constants
// ---------------------------------------------------------------------------
export const FloorAction = makeEnumProxy('FloorAction');
export const TILE_SIZE = 32;
export const HEIGHT_SCHEME = 'x0123456789abcdefghijklmnopq';
export const MAX_NUM_TILE_PER_AXIS = 64;
export const COLORMAP: object = {
'x': '101010',
'0': '0065ff', '1': '0091ff', '2': '00bcff', '3': '00e8ff',
'4': '00ffea', '5': '00ffbf', '6': '00ff93', '7': '00ff68',
'8': '00ff3d', '9': '19ff00',
'a': '44ff00', 'b': '70ff00', 'c': '9bff00', 'd': 'f2ff00',
'e': 'ffe000', 'f': 'ffb500', 'g': 'ff8900', 'h': 'ff5e00',
'i': 'ff3200', 'j': 'ff0700', 'k': 'ff0023', 'l': 'ff007a',
'm': 'ff00a5', 'n': 'ff00d1', 'o': 'ff00fc',
'p': 'd600ff', 'q': 'aa00ff'
};
// ---------------------------------------------------------------------------
// Floor plan editor — composer stubs and event classes
// ---------------------------------------------------------------------------
// Composer stubs for floor-plan-editor message events
export class GetRoomEntryTileMessageComposer extends StubClass {}
export class GetOccupiedTilesMessageComposer extends StubClass {}
export class UpdateFloorPropertiesMessageComposer extends StubClass
{
public tilemap: string;
public doorX: number;
public doorY: number;
public dir: number;
public thicknessWall: number;
public thicknessFloor: number;
public wallHeight: number;
constructor(tilemap: string, doorX: number, doorY: number, dir: number, thicknessWall: number, thicknessFloor: number, wallHeight: number)
{
super();
this.tilemap = tilemap;
this.doorX = doorX;
this.doorY = doorY;
this.dir = dir;
this.thicknessWall = thicknessWall;
this.thicknessFloor = thicknessFloor;
this.wallHeight = wallHeight;
}
}
// Event class stubs for useMessageEvent registration
export class FloorHeightMapEvent extends StubClass {}
export class RoomVisualizationSettingsEvent extends StubClass {}
export class RoomEntryTileMessageEvent extends StubClass {}
export class RoomOccupiedTilesMessageEvent extends StubClass {}
export const RoomEngineEvent = makeEnumProxy('RoomEngineEvent');
// Link tracker stubs
export type ILinkEventTracker = { linkReceived: (url: string) => void; eventUrlPrefix: string };
export const AddLinkEventTracker = vi.fn();
export const RemoveLinkEventTracker = vi.fn();
// Thickness conversion helpers — mirror the renderer's real mapping
export const convertNumbersForSaving = (v: number): number =>
{
switch(v)
{
case 0: return -2;
case 1: return -1;
case 3: return 1;
default: return 0;
}
};
export const convertSettingToNumber = (v: number): number =>
{
switch(v)
{
case 0.25: return 0;
case 0.5: return 1;
case 2: return 3;
default: return 2;
}
};
// ---------------------------------------------------------------------------
// 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);
// GetCommunication — routes IMessageEvent registration through the
// msgListeners map (separate from the NitroEvent listeners map) so that
// clearMockEventDispatcher() does NOT wipe these subscriptions. This
// keeps useBetween-based hooks (like useDoorState) subscribed across
// test cases without needing to recreate the useBetween singleton.
//
// A WeakMap stores the wrapper fn keyed by the MessageEvent instance so
// that removeMessageEvent can remove the exact listener added by
// registerMessageEvent.
const _msgEventWrappers = new WeakMap<MessageEvent, (ev: any) => void>();
export const GetCommunication = vi.fn(() => ({
registerMessageEvent(event: MessageEvent)
{
if(!event.callBack) return;
const wrapper = (ev: any) => event.callBack!(ev);
_msgEventWrappers.set(event, wrapper);
let bucket = msgListeners.get(event.type);
if(!bucket) { bucket = new Set(); msgListeners.set(event.type, bucket); }
bucket.add(wrapper);
},
removeMessageEvent(event: MessageEvent)
{
const wrapper = _msgEventWrappers.get(event);
if(wrapper) msgListeners.get(event.type)?.delete(wrapper);
},
// Stub for SendMessageComposer which calls GetCommunication().connection.send(...)
connection: { send: vi.fn() }
}));
export const GetConfiguration = vi.fn(stubManager);
export const GetLocalizationManager = vi.fn(stubManager);
export const GetRoomEngine = vi.fn(stubManager);
export const GetRoomMessageHandler = vi.fn(stubManager);
export const GetRoomSessionManager = vi.fn(stubManager);
// RoomPreviewer — only the bits the editor's FloorplanRoomPreview
// component touches. PREVIEW_COUNTER is a static field that the
// real renderer increments to allocate unique preview-room IDs;
// keeping it as a mutable static lets the editor mount/unmount
// repeatedly across tests without colliding.
export class RoomPreviewer
{
static PREVIEW_COUNTER = 0;
constructor(public readonly _engine: unknown, public readonly _id: number) {}
public updatePreviewModel(_model: string, _wallHeight: number, _scale?: boolean): void {}
public modifyRoomCanvas(_w: number, _h: number): void {}
public getRoomCanvas(_w: number, _h: number): unknown { return null; }
public getRenderingCanvas(): unknown { return null; }
public updatePreviewRoomView(): void {}
public changeRoomObjectDirection(): void {}
public changeRoomObjectState(): void {}
public dispose(): void {}
}
export const GetSessionDataManager = vi.fn(stubManager);
export const GetTickerTime = vi.fn(() => 0);
export const GetTicker = vi.fn(stubManager);
export const GetRenderer = vi.fn(stubManager);
export class NitroTicker {}
// TextureUtils — a real-enough stub of the createRenderTexture
// roundtrip. Tests that mount LayoutRoomPreviewerView allocate a
// texture on mount and destroy it on unmount; without a real
// `.destroy` method the unmount cleanup throws.
export const TextureUtils = {
createRenderTexture: (_w: number, _h: number) => ({
destroy: (_options?: unknown) => undefined
}),
generateImage: () => null
};
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.