/** * 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>(); // 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>(); 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; 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 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.