From 11dfb0e9926e9bf09ab53808046f867a1828e52d Mon Sep 17 00:00:00 2001 From: Andy Ford Date: Thu, 31 Oct 2024 19:27:59 +0000 Subject: [PATCH] react tests --- src/react/helper/eventual-room.ts | 31 ------- src/react/helper/use-eventual-room.ts | 89 +++++++++++++++++++ src/react/helper/use-room-context.ts | 3 +- src/react/helper/use-room-status.ts | 7 +- src/react/helper/use-stable-reference.ts | 25 ++++++ src/react/hooks/use-messages.ts | 7 ++ src/react/hooks/use-occupancy.ts | 9 +- src/react/hooks/use-presence-listener.ts | 9 +- src/react/hooks/use-presence.ts | 26 ++++-- src/react/hooks/use-room-reactions.ts | 7 ++ src/react/hooks/use-room.ts | 76 ++-------------- src/react/hooks/use-typing.ts | 7 ++ test/helper/wait-for-eventual-hook.ts | 26 ++++++ test/react/hooks/use-chat-client.test.tsx | 2 +- .../hooks/use-messages.integration.test.tsx | 10 +-- test/react/hooks/use-messages.test.tsx | 38 +++++--- .../hooks/use-occupancy.integration.test.tsx | 4 +- test/react/hooks/use-occupancy.test.tsx | 48 ++++++---- ...use-presence-listener.integration.test.tsx | 2 +- .../hooks/use-presence-listener.test.tsx | 68 +++++++++----- .../hooks/use-presence.integration.test.tsx | 2 +- test/react/hooks/use-presence.test.tsx | 46 ++++++---- .../use-room-reactions.integration.test.tsx | 5 +- test/react/hooks/use-room-reactions.test.tsx | 48 ++++++---- test/react/hooks/use-room.test.tsx | 77 ++++++++-------- .../hooks/use-typing.integration.test.tsx | 4 +- test/react/hooks/use-typing.test.tsx | 47 ++++++---- test/react/providers/room-provider.test.tsx | 33 ++++--- 28 files changed, 481 insertions(+), 275 deletions(-) delete mode 100644 src/react/helper/eventual-room.ts create mode 100644 src/react/helper/use-eventual-room.ts create mode 100644 src/react/helper/use-stable-reference.ts create mode 100644 test/helper/wait-for-eventual-hook.ts diff --git a/src/react/helper/eventual-room.ts b/src/react/helper/eventual-room.ts deleted file mode 100644 index 3efe709b..00000000 --- a/src/react/helper/eventual-room.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Room } from '@ably/chat'; -import { useEffect, useRef, useState } from 'react'; - -import { useLogger } from '../hooks/use-logger.js'; - -export const useEventualRoom = (roomId: string, room: Promise): Room | undefined => { - const [roomState, setRoomState] = useState(); - const logger = useLogger(); - const roomRef = useRef>(room); - useEffect(() => { - roomRef.current = room; - }); - - useEffect(() => { - let unmounted = false; - void roomRef.current - .then((room: Room) => { - if (unmounted) return; - setRoomState(room); - }) - .catch((error: unknown) => { - logger.error('Failed to get room', { roomId, error }); - }); - - return () => { - unmounted = true; - }; - }, [roomId, logger]); - - return roomState; -}; diff --git a/src/react/helper/use-eventual-room.ts b/src/react/helper/use-eventual-room.ts new file mode 100644 index 00000000..16947c81 --- /dev/null +++ b/src/react/helper/use-eventual-room.ts @@ -0,0 +1,89 @@ +import { Room } from '@ably/chat'; +import { useEffect, useState } from 'react'; + +import { useLogger } from '../hooks/use-logger.js'; +import { useRoomContext } from './use-room-context.js'; +import { useStableReference } from './use-stable-reference.js'; + +/** + * Given a room promise, this hook will return the room object once it has been resolved. This is useful + * in hooks like useRoom to provide a direct reference to the room object, as Promises aren't usually the best + * thing to be passing around React components. + * + * @param roomId The roomId of the room + * @param room The room promise that we're waiting to resolve + * @returns The room object if it has resolved, otherwise undefined + */ +export const useEventualRoom = (): Room | undefined => { + const [roomState, setRoomState] = useState(); + const context = useRoomContext('useEventualRoom'); + const logger = useLogger(); + logger.trace('useEventualRoom();', { roomId: context.roomId }); + + useEffect(() => { + logger.debug('useEventualRoom(); running useEffect', { roomId: context.roomId }); + let unmounted = false; + void context.room + .then((room: Room) => { + if (unmounted) { + logger.debug('useEventualRoom(); already unmounted', { roomId: context.roomId }); + return; + } + + logger.debug('useEventualRoom(); resolved', { roomId: context.roomId }); + setRoomState(room); + }) + .catch((error: unknown) => { + logger.error('Failed to get room', { roomId: context.roomId, error }); + }); + + return () => { + logger.debug('useEventualRoom(); cleanup', { roomId: context.roomId }); + unmounted = true; + }; + }, [context, logger]); + + return roomState; +}; + +/** + * Similar to useEventualRoom, but instead of providing the room itself, it provides a property of the room - e.g. + * Messages. We use this to eventually provide access to underlying room interfaces as non-promise values + * in hooks like useMessages. + * @param roomId The roomId of the room + * @param room The room promise that we're waiting to resolve + * @param onResolve A function that will be called when the room promise resolves, and will return the property of the room + * @returns + */ +export const useEventualRoomProperty = (onResolve: (room: Room) => T) => { + const [roomState, setRoomState] = useState(); + const context = useRoomContext('useEventualRoomProperty'); + const logger = useLogger(); + logger.trace('useEventualRoomProperty();', { roomId: context.roomId }); + const onResolveRef = useStableReference(onResolve); + + useEffect(() => { + let unmounted = false; + logger.debug('useEventualRoomProperty(); running useEffect', { roomId: context.roomId }); + void context.room + .then((room: Room) => { + if (unmounted) { + logger.debug('useEventualRoomProperty(); already unmounted', { roomId: context.roomId }); + return; + } + + logger.debug('useEventualRoomProperty(); resolved', { roomId: context.roomId }); + setRoomState(onResolveRef(room)); + }) + .catch((error: unknown) => { + logger.error('Failed to get room', { roomId: context.roomId, error }); + }); + + return () => { + logger.debug('useEventualRoomProperty(); cleanup', { roomId: context.roomId }); + unmounted = true; + }; + }, [context, logger, onResolveRef]); + + return roomState; +}; diff --git a/src/react/helper/use-room-context.ts b/src/react/helper/use-room-context.ts index 5c63bfd0..98e9bfca 100644 --- a/src/react/helper/use-room-context.ts +++ b/src/react/helper/use-room-context.ts @@ -1,3 +1,4 @@ +import * as Ably from 'ably'; import { useContext } from 'react'; import { ChatRoomContext, ChatRoomContextType } from '../contexts/chat-room-context.js'; @@ -5,7 +6,7 @@ import { ChatRoomContext, ChatRoomContextType } from '../contexts/chat-room-cont export const useRoomContext = (callingHook: string): ChatRoomContextType => { const context = useContext(ChatRoomContext); if (!context) { - throw new Error(`\`${callingHook}\`(); must be used within a `); + throw new Ably.ErrorInfo(`${callingHook} hook must be used within a `, 40000, 400); } return context; diff --git a/src/react/helper/use-room-status.ts b/src/react/helper/use-room-status.ts index cd017ad4..550a6589 100644 --- a/src/react/helper/use-room-status.ts +++ b/src/react/helper/use-room-status.ts @@ -27,6 +27,11 @@ export const useRoomStatus = (params?: { context.room, (room: Room) => { logger.debug('useRoomStatus(); subscribing internal listener'); + // Set instantaneous values + setStatus(room.status.current); + setError(room.status.error); + + // Add the subscription const { off } = room.status.onChange((change) => { logger.debug('useRoomStatus(); status change', change); setStatus(change.current); @@ -59,8 +64,6 @@ export const useRoomStatus = (params?: { } logger.debug('useRoomStatus(); setting initial status', { status: room.status.current }); - setStatus(room.status.current); - setError(room.status.error); if (onRoomStatusChangeRef) { logger.debug('useRoomStatus(); sending initial status event'); onRoomStatusChangeRef({ diff --git a/src/react/helper/use-stable-reference.ts b/src/react/helper/use-stable-reference.ts new file mode 100644 index 00000000..54729d2b --- /dev/null +++ b/src/react/helper/use-stable-reference.ts @@ -0,0 +1,25 @@ +import { useCallback, useEffect, useRef } from 'react'; + +/** + * The type of a callback function that can be stored in the reference. + */ +type Callback = (...args: CallbackArguments) => ReturnType; + +/** + * In some cases, we want to use a callback that is always the same, and always persists across renders. + * This function creates a stable reference to a callback, so that it can be used in a `useEffect` or `useCallback` + * without causing unnecessary re-renders. + * + * @param callback The callback to turn into a stable reference + * @returns A stable reference to the callback + */ +export const useStableReference = ( + callback: Callback, +): Callback => { + const ref = useRef>(callback); + useEffect(() => { + ref.current = callback; + }); + + return useCallback((...args: Arguments) => ref.current(...args), []); +}; diff --git a/src/react/hooks/use-messages.ts b/src/react/hooks/use-messages.ts index 02604e00..0fe9b45a 100644 --- a/src/react/hooks/use-messages.ts +++ b/src/react/hooks/use-messages.ts @@ -11,6 +11,7 @@ import { useCallback, useEffect, useState } from 'react'; import { wrapRoomPromise } from '../helper/room-promise.js'; import { useEventListenerRef } from '../helper/use-event-listener-ref.js'; +import { useEventualRoomProperty } from '../helper/use-eventual-room.js'; import { useRoomContext } from '../helper/use-room-context.js'; import { useRoomStatus } from '../helper/use-room-status.js'; import { ChatStatusResponse } from '../types/chat-status-response.js'; @@ -33,6 +34,11 @@ export interface UseMessagesResponse extends ChatStatusResponse { */ readonly get: Messages['get']; + /** + * Provides access to the underlying {@link Messages} instance of the room. + */ + readonly messages?: Messages; + /** * Retrieves the previous messages in the room. * @@ -167,6 +173,7 @@ export const useMessages = (params?: UseMessagesParams): UseMessagesResponse => }, [context, logger, onDiscontinuityRef]); return { + messages: useEventualRoomProperty((room) => room.messages), send, get, getPreviousMessages, diff --git a/src/react/hooks/use-occupancy.ts b/src/react/hooks/use-occupancy.ts index 2d5f359b..cbd1ef98 100644 --- a/src/react/hooks/use-occupancy.ts +++ b/src/react/hooks/use-occupancy.ts @@ -1,8 +1,9 @@ -import { OccupancyListener } from '@ably/chat'; +import { Occupancy, OccupancyListener } from '@ably/chat'; import { useEffect, useState } from 'react'; import { wrapRoomPromise } from '../helper/room-promise.js'; import { useEventListenerRef } from '../helper/use-event-listener-ref.js'; +import { useEventualRoomProperty } from '../helper/use-eventual-room.js'; import { useRoomContext } from '../helper/use-room-context.js'; import { useRoomStatus } from '../helper/use-room-status.js'; import { ChatStatusResponse } from '../types/chat-status-response.js'; @@ -35,6 +36,11 @@ export interface UseOccupancyResponse extends ChatStatusResponse { * The current number of users present in the room, kept up to date by the hook. */ readonly presenceMembers: number; + + /** + * Provides access to the underlying {@link Occupancy} instance of the room. + */ + readonly occupancy?: Occupancy; } /** @@ -122,6 +128,7 @@ export const useOccupancy = (params?: UseOccupancyParams): UseOccupancyResponse }, [listenerRef, context, logger]); return { + occupancy: useEventualRoomProperty((room) => room.occupancy), connectionStatus, connectionError, roomStatus, diff --git a/src/react/hooks/use-presence-listener.ts b/src/react/hooks/use-presence-listener.ts index c64ae06a..a4babfa1 100644 --- a/src/react/hooks/use-presence-listener.ts +++ b/src/react/hooks/use-presence-listener.ts @@ -1,9 +1,10 @@ -import { ErrorCodes, errorInfoIs, PresenceListener, PresenceMember, Room, RoomLifecycle } from '@ably/chat'; +import { ErrorCodes, errorInfoIs, Presence, PresenceListener, PresenceMember, Room, RoomLifecycle } from '@ably/chat'; import * as Ably from 'ably'; import { useCallback, useEffect, useRef, useState } from 'react'; import { wrapRoomPromise } from '../helper/room-promise.js'; import { useEventListenerRef } from '../helper/use-event-listener-ref.js'; +import { useEventualRoomProperty } from '../helper/use-eventual-room.js'; import { useRoomContext } from '../helper/use-room-context.js'; import { useRoomStatus } from '../helper/use-room-status.js'; import { ChatStatusResponse } from '../types/chat-status-response.js'; @@ -44,6 +45,11 @@ export interface UsePresenceListenerResponse extends ChatStatusResponse { */ readonly presenceData: PresenceMember[]; + /** + * Provides access to the underlying {@link Presence} instance of the room. + */ + readonly presence?: Presence; + /** * The error state of the presence listener. * The hook keeps {@link presenceData} up to date asynchronously, so this error state is provided to allow @@ -300,6 +306,7 @@ export const usePresenceListener = (params?: UsePresenceListenerParams): UsePres }, [context, onDiscontinuityRef, logger]); return { + presence: useEventualRoomProperty((room) => room.presence), connectionStatus, connectionError, roomStatus, diff --git a/src/react/hooks/use-presence.ts b/src/react/hooks/use-presence.ts index 755d7369..cce0f145 100644 --- a/src/react/hooks/use-presence.ts +++ b/src/react/hooks/use-presence.ts @@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { wrapRoomPromise } from '../helper/room-promise.js'; import { useEventListenerRef } from '../helper/use-event-listener-ref.js'; +import { useEventualRoomProperty } from '../helper/use-eventual-room.js'; import { useRoomContext } from '../helper/use-room-context.js'; import { useRoomStatus } from '../helper/use-room-status.js'; import { ChatStatusResponse } from '../types/chat-status-response.js'; @@ -32,6 +33,11 @@ export interface UsePresenceResponse extends ChatStatusResponse { */ readonly update: Presence['update']; + /** + * Provides access to the underlying {@link Presence} instance of the room. + */ + readonly presence?: Presence; + /** * Indicates whether the current user is present in the room. */ @@ -92,18 +98,21 @@ export const usePresence = (params?: UsePresenceParams): UsePresenceResponse => // enter the room when the hook is mounted useEffect(() => { - const canJoinPresence = roomStatus === RoomLifecycle.Attached && !INACTIVE_CONNECTION_STATES.has(connectionStatus); - - // wait until the room is attached before attempting to enter, and ensure the connection is active - if (!canJoinPresence) { - logger.debug('usePresence(); skipping enter room', { roomStatus, connectionStatus, roomId: context.roomId }); - return; - } - logger.debug('usePresence(); entering room', { roomId: context.roomId }); return wrapRoomPromise( context.room, (room: Room) => { + const canJoinPresence = + roomStatus === RoomLifecycle.Attached && !INACTIVE_CONNECTION_STATES.has(connectionStatus); + + // wait until the room is attached before attempting to enter, and ensure the connection is active + if (!canJoinPresence) { + logger.debug('usePresence(); skipping enter room', { roomStatus, connectionStatus, roomId: context.roomId }); + return () => { + // no-op + }; + } + room.presence .enter(dataRef.current?.enterWithData) .then(() => { @@ -178,6 +187,7 @@ export const usePresence = (params?: UsePresenceParams): UsePresenceResponse => ); return { + presence: useEventualRoomProperty((room) => room.presence), connectionStatus, connectionError, roomStatus, diff --git a/src/react/hooks/use-room-reactions.ts b/src/react/hooks/use-room-reactions.ts index 2c53dd6b..23117128 100644 --- a/src/react/hooks/use-room-reactions.ts +++ b/src/react/hooks/use-room-reactions.ts @@ -3,6 +3,7 @@ import { useCallback, useEffect } from 'react'; import { wrapRoomPromise } from '../helper/room-promise.js'; import { useEventListenerRef } from '../helper/use-event-listener-ref.js'; +import { useEventualRoomProperty } from '../helper/use-eventual-room.js'; import { useRoomContext } from '../helper/use-room-context.js'; import { useRoomStatus } from '../helper/use-room-status.js'; import { ChatStatusResponse } from '../types/chat-status-response.js'; @@ -29,6 +30,11 @@ export interface UseRoomReactionsResponse extends ChatStatusResponse { * A shortcut to the {@link RoomReactions.send} method. */ readonly send: RoomReactions['send']; + + /** + * Provides access to the underlying {@link RoomReactions} instance of the room. + */ + readonly reactions?: RoomReactions; } /** @@ -94,6 +100,7 @@ export const useRoomReactions = (params?: UseRoomReactionsParams): UseRoomReacti ); return { + reactions: useEventualRoomProperty((room) => room.reactions), connectionStatus, connectionError, roomStatus, diff --git a/src/react/hooks/use-room.ts b/src/react/hooks/use-room.ts index 4487d8cd..9502aad8 100644 --- a/src/react/hooks/use-room.ts +++ b/src/react/hooks/use-room.ts @@ -2,10 +2,11 @@ import { ConnectionStatusChange, Room, RoomLifecycle, RoomStatusChange } from '@ import * as Ably from 'ably'; import { useCallback, useEffect, useState } from 'react'; -import { useEventualRoom } from '../helper/eventual-room.js'; import { wrapRoomPromise } from '../helper/room-promise.js'; import { useEventListenerRef } from '../helper/use-event-listener-ref.js'; +import { useEventualRoom } from '../helper/use-eventual-room.js'; import { useRoomContext } from '../helper/use-room-context.js'; +import { useRoomStatus } from '../helper/use-room-status.js'; import { ChatStatusResponse } from '../types/chat-status-response.js'; import { useChatConnection } from './use-chat-connection.js'; import { useLogger } from './use-logger.js'; @@ -54,17 +55,6 @@ export interface UseRoomResponse extends ChatStatusResponse { detach: () => Promise; } -/** - * Helper function to create a status object from a change event or the room - * status. It makes sure error isn't set at all if it's undefined in the source. - */ -function makeStatusObject(source: { current: RoomLifecycle; error?: Ably.ErrorInfo }) { - return { - status: source.current, - error: source.error, - }; -} - /** * A hook that provides access to the current room. * @@ -82,70 +72,16 @@ export const useRoom = (params?: UseRoomParams): UseRoomResponse => { }); // room error and status callbacks - const [roomStatus, setRoomStatus] = useState<{ - status: RoomLifecycle; - error?: Ably.ErrorInfo; - }>( - makeStatusObject({ - current: RoomLifecycle.Initializing, - }), - ); - - // create stable references for the listeners - const onRoomStatusChangeRef = useEventListenerRef(params?.onStatusChange); - - // Effect that keeps the roomStatus state up to date - useEffect(() => { - const roomPromise = wrapRoomPromise( - context.room, - (room: Room) => { - logger.debug('useRoom(); setting up room status listener', { roomId: roomId }); - const { off } = room.status.onChange((change) => { - setRoomStatus(makeStatusObject(change)); - }); - - return () => { - logger.debug('useRoom(); removing room status listener', { roomId: roomId }); - off(); - }; - }, - logger, - roomId, - ); - - return roomPromise.unmount(); - }, [context, roomId, logger]); - - // Effect that registers and removes the user-provided callback - useEffect(() => { - if (!onRoomStatusChangeRef) return; - - const roomPromise = wrapRoomPromise( - context.room, - (room: Room) => { - logger.debug('useRoom(); setting up user-provided listener', { roomId: roomId }); - const { off } = room.status.onChange(onRoomStatusChangeRef); - - return () => { - logger.debug('useRoom(); removing user-provided listener', { roomId: roomId }); - off(); - }; - }, - logger, - roomId, - ); - - return roomPromise.unmount(); - }, [context, roomId, onRoomStatusChangeRef, logger]); + const roomStatus = useRoomStatus({ + onRoomStatusChange: params?.onStatusChange, + }); - // TODO: Can we even do this if the room isn't defined? - // Would we have to do a queue/backlog of calls somehow? const attach = useCallback(() => context.room.then((room: Room) => room.attach()), [context]); const detach = useCallback(() => context.room.then((room: Room) => room.detach()), [context]); return { roomId: roomId, - room: useEventualRoom(roomId, context.room), + room: useEventualRoom(), attach: attach, detach: detach, roomStatus: roomStatus.status, diff --git a/src/react/hooks/use-typing.ts b/src/react/hooks/use-typing.ts index d594f845..d67e0c8c 100644 --- a/src/react/hooks/use-typing.ts +++ b/src/react/hooks/use-typing.ts @@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from 'react'; import { wrapRoomPromise } from '../helper/room-promise.js'; import { useEventListenerRef } from '../helper/use-event-listener-ref.js'; +import { useEventualRoomProperty } from '../helper/use-eventual-room.js'; import { useRoomContext } from '../helper/use-room-context.js'; import { useRoomStatus } from '../helper/use-room-status.js'; import { ChatStatusResponse } from '../types/chat-status-response.js'; @@ -41,6 +42,11 @@ export interface UseTypingResponse extends ChatStatusResponse { */ readonly currentlyTyping: TypingEvent['currentlyTyping']; + /** + * Provides access to the underlying {@link Typing} instance of the room. + */ + readonly typingIndicators?: Typing; + /** * A state value representing the current error state of the hook, this will be an instance of {@link Ably.ErrorInfo} or `undefined`. * An error can occur during mount when initially fetching the current typing state; this does not mean that further @@ -177,6 +183,7 @@ export const useTyping = (params?: TypingParams): UseTypingResponse => { const stop = useCallback(() => context.room.then((room) => room.typing.stop()), [context]); return { + typingIndicators: useEventualRoomProperty((room) => room.typing), connectionStatus, connectionError, roomStatus, diff --git a/test/helper/wait-for-eventual-hook.ts b/test/helper/wait-for-eventual-hook.ts new file mode 100644 index 00000000..30074b6a --- /dev/null +++ b/test/helper/wait-for-eventual-hook.ts @@ -0,0 +1,26 @@ +import { expect, vi } from 'vitest'; + +export const waitForEventualHookValueToBeDefined = async ( + result: { current: T }, + check: (value: T) => unknown, +): Promise => { + return vi.waitFor( + () => { + expect(check(result.current)).toBeDefined(); + }, + { timeout: 3000 }, + ); +}; + +export const waitForEventualHookValue = async ( + result: { current: HookReturn }, + expected: Value, + getValue: (current: HookReturn) => Value | undefined, +): Promise => { + return vi.waitFor( + () => { + expect(getValue(result.current)).toBe(expected); + }, + { timeout: 3000 }, + ); +}; diff --git a/test/react/hooks/use-chat-client.test.tsx b/test/react/hooks/use-chat-client.test.tsx index 7809652f..e2354a16 100644 --- a/test/react/hooks/use-chat-client.test.tsx +++ b/test/react/hooks/use-chat-client.test.tsx @@ -25,7 +25,7 @@ describe('useChatClient', () => { it('should throw an error if used outside of ChatClientProvider', () => { const TestThrowError: React.FC = () => { - expect(() => useRoom()).toThrowErrorInfo({ + expect(() => useChatClient()).toThrowErrorInfo({ code: 40000, message: 'useChatClient hook must be used within a chat client provider', }); diff --git a/test/react/hooks/use-messages.integration.test.tsx b/test/react/hooks/use-messages.integration.test.tsx index c03fc789..42e74dda 100644 --- a/test/react/hooks/use-messages.integration.test.tsx +++ b/test/react/hooks/use-messages.integration.test.tsx @@ -36,7 +36,7 @@ describe('useMessages', () => { // create a second room and attach it, so we can listen for messages const roomId = randomRoomId(); - const roomTwo = chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); + const roomTwo = await chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); await roomTwo.attach(); // start listening for messages @@ -80,7 +80,7 @@ describe('useMessages', () => { // create a second room so we can send messages from it const roomId = randomRoomId(); - const roomTwo = chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); + const roomTwo = await chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); // start listening for messages const messagesRoomOne: Message[] = []; @@ -135,7 +135,7 @@ describe('useMessages', () => { // create a second room instance so we can send messages from it const roomId = randomRoomId(); - const roomTwo = chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); + const roomTwo = await chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); await roomTwo.attach(); // send a few messages before the first room has subscribed @@ -198,7 +198,7 @@ describe('useMessages', () => { // create a second room instance so we can send messages from it const roomId = randomRoomId(); - const room = chatClient.rooms.get(roomId, RoomOptionsDefaults); + const room = await chatClient.rooms.get(roomId, RoomOptionsDefaults); await room.attach(); let lastSeenMessageText: string | undefined; @@ -332,7 +332,7 @@ describe('useMessages', () => { // create a second room instance so we can send messages from it const roomId = randomRoomId(); - const room = chatClient.rooms.get(roomId, RoomOptionsDefaults); + const room = await chatClient.rooms.get(roomId, RoomOptionsDefaults); await room.attach(); let lastSeenMessageText: string | undefined; diff --git a/test/react/hooks/use-messages.test.tsx b/test/react/hooks/use-messages.test.tsx index eebc68aa..3038f36c 100644 --- a/test/react/hooks/use-messages.test.tsx +++ b/test/react/hooks/use-messages.test.tsx @@ -15,8 +15,10 @@ import { PaginatedResult } from '../../../src/core/query.ts'; import { useMessages } from '../../../src/react/hooks/use-messages.ts'; import { makeTestLogger } from '../../helper/logger.ts'; import { makeRandomRoom } from '../../helper/room.ts'; +import { waitForEventualHookValue, waitForEventualHookValueToBeDefined } from '../../helper/wait-for-eventual-hook.ts'; let mockRoom: Room; +let mockRoomContext: { room: Promise }; let mockCurrentConnectionStatus: ConnectionLifecycle; let mockCurrentRoomStatus: RoomLifecycle; let mockConnectionError: Ably.ErrorInfo; @@ -31,12 +33,13 @@ vi.mock('../../../src/react/hooks/use-chat-connection.js', () => ({ }), })); -vi.mock('../../../src/react/hooks/use-room.js', () => ({ - useRoom: () => ({ - room: mockRoom, - roomStatus: mockCurrentRoomStatus, - roomError: mockRoomError, - }), + +vi.mock('../../../src/react/helper/use-room-context.js', () => ({ + useRoomContext: () => mockRoomContext, +})); + +vi.mock('../../../src/react/helper/use-room-status.js', () => ({ + useRoomStatus: () => ({ status: mockCurrentRoomStatus, error: mockRoomError }), })); vi.mock('../../../src/react/hooks/use-logger.js', () => ({ @@ -45,6 +48,11 @@ vi.mock('../../../src/react/hooks/use-logger.js', () => ({ vi.mock('ably'); +const updateMockRoom = (newRoom: Room) => { + mockRoom = newRoom; + mockRoomContext = { room: Promise.resolve(newRoom) }; +} + describe('useMessages', () => { beforeEach(() => { // create a new mock room before each test, enabling messages @@ -52,14 +60,14 @@ describe('useMessages', () => { testLogger = makeTestLogger(); mockCurrentConnectionStatus = ConnectionLifecycle.Connected; mockCurrentRoomStatus = RoomLifecycle.Attached; - mockRoom = makeRandomRoom({}); + updateMockRoom(makeRandomRoom({})); }); afterEach(() => { cleanup(); }); - it('should provide the messages instance and chat status response metrics', () => { + it('should provide the messages instance and chat status response metrics', async () => { // set the connection and room errors to check that they are correctly provided mockConnectionError = new Ably.ErrorInfo('test error', 40000, 400); mockRoomError = new Ably.ErrorInfo('test error', 40000, 400); @@ -67,7 +75,7 @@ describe('useMessages', () => { const { result } = renderHook(() => useMessages()); // check that the messages instance and metrics are correctly provided - expect(result.current.messages).toBe(mockRoom.messages); + await waitForEventualHookValue(result, mockRoom.messages, (value) => value.messages); // check connection and room metrics are correctly provided expect(result.current.roomStatus).toBe(RoomLifecycle.Attached); @@ -94,6 +102,7 @@ describe('useMessages', () => { }), ); + await waitForEventualHookValueToBeDefined(result, (value) => value.getPreviousMessages); const getPreviousMessages = result.current.getPreviousMessages; // verify that subscribe was called with the mock listener on mount by invoking it @@ -161,23 +170,24 @@ describe('useMessages', () => { expect(getSpy).toHaveBeenCalledWith({ limit: 10 }); }); - it('should handle rerender if the room instance changes', () => { + it('should handle rerender if the room instance changes', async () => { const { result, rerender } = renderHook(() => useMessages()); // check the initial state of the messages instance + await waitForEventualHookValue(result, mockRoom.messages, (value) => value.messages); expect(result.current.messages).toBe(mockRoom.messages); // change the mock room instance - mockRoom = makeRandomRoom({}); + updateMockRoom(makeRandomRoom({})); // re-render to trigger the useEffect rerender(); // check that the messages instance is updated - expect(result.current.messages).toBe(mockRoom.messages); + await waitForEventualHookValue(result, mockRoom.messages, (value) => value.messages); }); - it('should subscribe and unsubscribe to discontinuity events', () => { + it('should subscribe and unsubscribe to discontinuity events', async () => { const mockOff = vi.fn(); const mockDiscontinuityListener = vi.fn(); @@ -193,7 +203,7 @@ describe('useMessages', () => { // check that the listener was subscribed to the discontinuity events by invoking it const errorInfo = new Ably.ErrorInfo('test error', 40000, 400); - expect(discontinuityListener).toBeDefined(); + await vi.waitFor(() => discontinuityListener !== undefined); discontinuityListener?.(errorInfo); expect(mockDiscontinuityListener).toHaveBeenCalledWith(errorInfo); diff --git a/test/react/hooks/use-occupancy.integration.test.tsx b/test/react/hooks/use-occupancy.integration.test.tsx index a408ad2e..21a5dfc1 100644 --- a/test/react/hooks/use-occupancy.integration.test.tsx +++ b/test/react/hooks/use-occupancy.integration.test.tsx @@ -24,8 +24,8 @@ describe('useOccupancy', () => { // create two more rooms and attach to contribute towards occupancy metrics const roomId = randomRoomId(); - const roomTwo = chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); - const roomThree = chatClientThree.rooms.get(roomId, RoomOptionsDefaults); + const roomTwo = await chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); + const roomThree = await chatClientThree.rooms.get(roomId, RoomOptionsDefaults); await roomTwo.attach(); await roomThree.attach(); diff --git a/test/react/hooks/use-occupancy.test.tsx b/test/react/hooks/use-occupancy.test.tsx index 88bd3358..d7d59768 100644 --- a/test/react/hooks/use-occupancy.test.tsx +++ b/test/react/hooks/use-occupancy.test.tsx @@ -10,20 +10,26 @@ import { act, cleanup, renderHook } from '@testing-library/react'; import * as Ably from 'ably'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useRoomContext } from '../../../src/react/helper/use-room-context.ts'; import { useOccupancy } from '../../../src/react/hooks/use-occupancy.ts'; import { makeTestLogger } from '../../helper/logger.ts'; import { makeRandomRoom } from '../../helper/room.ts'; +import { waitForEventualHookValue, waitForEventualHookValueToBeDefined } from '../../helper/wait-for-eventual-hook.ts'; let mockRoom: Room; +let mockRoomContext: { room: Promise }; let mockLogger: ReturnType; // apply mocks for the useChatConnection and useRoom hooks vi.mock('../../../src/react/hooks/use-chat-connection.js', () => ({ useChatConnection: () => ({ currentStatus: ConnectionLifecycle.Connected }), })); +vi.mock('../../../src/react/helper/use-room-context.js', () => ({ + useRoomContext: () => mockRoomContext, +})); -vi.mock('../../../src/react/hooks/use-room.js', () => ({ - useRoom: () => ({ room: mockRoom, roomStatus: RoomLifecycle.Attached }), +vi.mock('../../../src/react/helper/use-room-status.js', () => ({ + useRoomStatus: () => ({ status: RoomLifecycle.Attached}), })); vi.mock('../../../src/react/hooks/use-logger.js', () => ({ @@ -32,23 +38,28 @@ vi.mock('../../../src/react/hooks/use-logger.js', () => ({ vi.mock('ably'); +const updateMockRoom = (newRoom: Room) => { + mockRoom = newRoom; + mockRoomContext = { room: Promise.resolve(newRoom) }; +} + describe('useOccupancy', () => { beforeEach(() => { // create a new mock room before each test, enabling occupancy vi.resetAllMocks(); mockLogger = makeTestLogger(); - mockRoom = makeRandomRoom({ options: { occupancy: {} } }); + updateMockRoom(makeRandomRoom({ options: { occupancy: {} } })); }); afterEach(() => { cleanup(); }); - it('should provide the occupancy instance, associated metrics, and chat status response metrics', () => { + it('should provide the occupancy instance, associated metrics, and chat status response metrics', async () => { const { result } = renderHook(() => useOccupancy()); // check that the occupancy instance and metrics are correctly provided - expect(result.current.occupancy).toBe(mockRoom.occupancy); + await waitForEventualHookValue(result, mockRoom.occupancy, (value) => value.occupancy); expect(result.current.connections).toBe(0); expect(result.current.presenceMembers).toBe(0); @@ -59,7 +70,7 @@ describe('useOccupancy', () => { expect(result.current.connectionError).toBeUndefined(); }); - it('should correctly subscribe and unsubscribe to occupancy events', () => { + it('should correctly subscribe and unsubscribe to occupancy events', async () => { // mock listener and associated unsubscribe function const mockListener = vi.fn(); const mockUnsubscribe = vi.fn(); @@ -85,9 +96,11 @@ describe('useOccupancy', () => { }; // update the mock room with the new occupancy object - mockRoom = { ...mockRoom, occupancy: mockOccupancy }; + updateMockRoom({ ...mockRoom, occupancy: mockOccupancy }); + + const { result, unmount } = renderHook(() => useOccupancy({ listener: mockListener })); - const { unmount } = renderHook(() => useOccupancy({ listener: mockListener })); + await waitForEventualHookValueToBeDefined(result, (value) => value.occupancy); // verify that subscribe was called with the mock listener on mount by triggering an occupancy event mockOccupancy.callAllListeners({ connections: 5, presenceMembers: 3 }); @@ -98,7 +111,7 @@ describe('useOccupancy', () => { expect(mockUnsubscribe).toHaveBeenCalled(); }); - it('should update the occupancy metrics on new occupancy events', () => { + it('should update the occupancy metrics on new occupancy events', async () => { let subscribedListener: OccupancyListener; // spy on the subscribe method of the occupancy instance @@ -116,6 +129,8 @@ describe('useOccupancy', () => { // render the hook and check the initial state of the occupancy metrics const { result } = renderHook(() => useOccupancy()); + await waitForEventualHookValueToBeDefined(result, (value) => value.occupancy); + expect(result.current.connections).toBe(0); expect(result.current.presenceMembers).toBe(0); @@ -129,23 +144,24 @@ describe('useOccupancy', () => { expect(result.current.presenceMembers).toBe(3); }); - it('should handle rerender if the room instance changes', () => { + it('should handle rerender if the room instance changes', async () => { const { result, rerender } = renderHook(() => useOccupancy()); // check the initial state of the occupancy instance + await waitForEventualHookValue(result, mockRoom.occupancy, (value) => value.occupancy); expect(result.current.occupancy).toBe(mockRoom.occupancy); // change the mock room instance - mockRoom = makeRandomRoom({ options: { occupancy: {} } }); + updateMockRoom(makeRandomRoom({ options: { occupancy: {} } })); // re-render to trigger the useEffect rerender(); // check that the occupancy instance is updated - expect(result.current.occupancy).toBe(mockRoom.occupancy); + await waitForEventualHookValue(result, mockRoom.occupancy, (value) => value.occupancy); }); - it('should subscribe and unsubscribe to discontinuity events', () => { + it('should subscribe and unsubscribe to discontinuity events', async () => { const mockOff = vi.fn(); const mockDiscontinuityListener = vi.fn(); @@ -170,10 +186,12 @@ describe('useOccupancy', () => { }; // update the mock room with the new occupancy object - mockRoom = { ...mockRoom, occupancy: mockOccupancy }; + updateMockRoom({ ...mockRoom, occupancy: mockOccupancy }); // render the hook with a discontinuity listener - const { unmount } = renderHook(() => useOccupancy({ onDiscontinuity: mockDiscontinuityListener })); + const { result, unmount } = renderHook(() => useOccupancy({ onDiscontinuity: mockDiscontinuityListener })); + + await waitForEventualHookValueToBeDefined(result, (value) => value.occupancy); // check that the listener was subscribed to the discontinuity events by triggering one const errorInfo = new Ably.ErrorInfo('test', 50000, 500); diff --git a/test/react/hooks/use-presence-listener.integration.test.tsx b/test/react/hooks/use-presence-listener.integration.test.tsx index 192af4f3..48eec4e5 100644 --- a/test/react/hooks/use-presence-listener.integration.test.tsx +++ b/test/react/hooks/use-presence-listener.integration.test.tsx @@ -43,7 +43,7 @@ describe('usePresenceListener', () => { // create a second room and attach it, so we can send presence events with it const roomId = randomRoomId(); - const roomTwo = chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); + const roomTwo = await chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); await roomTwo.attach(); // store the current presence member state diff --git a/test/react/hooks/use-presence-listener.test.tsx b/test/react/hooks/use-presence-listener.test.tsx index d280736d..f2fed5b6 100644 --- a/test/react/hooks/use-presence-listener.test.tsx +++ b/test/react/hooks/use-presence-listener.test.tsx @@ -14,11 +14,14 @@ import * as Ably from 'ably'; import { ErrorInfo } from 'ably'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { InternalRoomStatus } from '../../../src/core/room-status.ts'; import { usePresenceListener } from '../../../src/react/hooks/use-presence-listener.ts'; import { makeTestLogger } from '../../helper/logger.ts'; import { makeRandomRoom } from '../../helper/room.ts'; +import { waitForEventualHookValue, waitForEventualHookValueToBeDefined } from '../../helper/wait-for-eventual-hook.ts'; let mockRoom: Room; +let mockRoomContext: {room: Promise}; let mockLogger: Logger; let mockCurrentConnectionStatus: ConnectionLifecycle; @@ -34,14 +37,12 @@ vi.mock('../../../src/react/hooks/use-chat-connection.js', () => ({ }), })); -vi.mock('../../../src/react/hooks/use-room.js', () => ({ - useRoom: () => { - return { - room: mockRoom, - roomStatus: mockCurrentRoomStatus, - roomError: mockRoomError, - }; - }, +vi.mock('../../../src/react/helper/use-room-context.js', () => ({ + useRoomContext: () => mockRoomContext, +})); + +vi.mock('../../../src/react/helper/use-room-status.js', () => ({ + useRoomStatus: () => ({ status: mockCurrentRoomStatus, error: mockRoomError }), })); vi.mock('../../../src/react/hooks/use-logger.js', () => ({ @@ -50,18 +51,26 @@ vi.mock('../../../src/react/hooks/use-logger.js', () => ({ vi.mock('ably'); +const updateMockRoom = (newRoom: Room) => { + mockRoom = newRoom; + (mockRoom.status as InternalRoomStatus).setStatus({ status: RoomLifecycle.Attached }); + mockRoomContext = { room: Promise.resolve(newRoom) }; +} + describe('usePresenceListener', () => { beforeEach(() => { // create a new mock room before each test - mockRoom = makeRandomRoom({ options: { presence: { subscribe: true } } }); + updateMockRoom(makeRandomRoom({ options: { presence: { subscribe: true } } })); mockLogger = makeTestLogger(); + mockCurrentRoomStatus = RoomLifecycle.Attached; + mockCurrentConnectionStatus = ConnectionLifecycle.Connected; }); afterEach(() => { vi.restoreAllMocks(); cleanup(); }); - it('should provide the room presence instance, presence data and correct chat status response metrics', () => { + it('should provide the room presence instance, presence data and correct chat status response metrics', async () => { mockConnectionError = new Ably.ErrorInfo('test', 500, 50000); mockRoomError = new Ably.ErrorInfo('test', 500, 50000); mockCurrentRoomStatus = RoomLifecycle.Attached; @@ -70,6 +79,7 @@ describe('usePresenceListener', () => { const { result } = renderHook(() => usePresenceListener()); // check that the room presence instance is correctly provided + await waitForEventualHookValue(result, mockRoom.presence, (value) => value.presence); expect(result.current.presence).toBe(mockRoom.presence); expect(result.current.presenceData).toEqual([]); @@ -80,7 +90,7 @@ describe('usePresenceListener', () => { expect(result.current.connectionError).toBe(mockConnectionError); }); - it('should correctly subscribe and unsubscribe to presence', () => { + it('should correctly subscribe and unsubscribe to presence', async () => { // mock listener and associated unsubscribe function const mockListener = vi.fn(); const mockUnsubscribe = vi.fn(); @@ -93,7 +103,9 @@ describe('usePresenceListener', () => { }); vi.spyOn(mockRoom.presence, 'get').mockResolvedValue([]); - const { unmount } = renderHook(() => usePresenceListener({ listener: mockListener })); + const { result, unmount } = renderHook(() => usePresenceListener({ listener: mockListener })); + + await waitForEventualHookValueToBeDefined(result, () => result.current.presence); // verify that subscribe was called with the mock listener on mount by triggering a presence event const testPresenceEvent: PresenceEvent = { @@ -112,20 +124,21 @@ describe('usePresenceListener', () => { expect(mockUnsubscribe).toHaveBeenCalled(); }); - it('should handle rerender if the room instance changes', () => { + it('should handle rerender if the room instance changes', async () => { const { result, rerender } = renderHook(() => usePresenceListener()); // check the initial state of the presence object + await waitForEventualHookValue(result, mockRoom.presence, (value) => value.presence); expect(result.current.presence).toBe(mockRoom.presence); // change the mock room instance - mockRoom = makeRandomRoom({ options: { presence: { subscribe: true } } }); + updateMockRoom(makeRandomRoom({ options: { presence: { subscribe: true } } })); // re-render to trigger the useEffect rerender(); // check that the room presence instance is updated - expect(result.current.presence).toBe(mockRoom.presence); + await waitForEventualHookValue(result, mockRoom.presence, (value) => value.presence); }); it('should set the initial present clients on mount', async () => { @@ -156,6 +169,7 @@ describe('usePresenceListener', () => { // render the hook and check the initial state const { result } = renderHook(() => usePresenceListener()); + await waitForEventualHookValueToBeDefined(result, () => result.current.presence); await waitFor( () => { @@ -254,7 +268,7 @@ describe('usePresenceListener', () => { ); }); - it('should subscribe and unsubscribe to discontinuity events', () => { + it('should subscribe and unsubscribe to discontinuity events', async () => { const mockOff = vi.fn(); const mockDiscontinuityListener = vi.fn(); @@ -270,7 +284,8 @@ describe('usePresenceListener', () => { // check that the listener was subscribed to the discontinuity events by triggering a discontinuity event const errorInfo = new Ably.ErrorInfo('test', 500, 50000); - expect(discontinuityListener).toBeDefined(); + + await waitFor(() => discontinuityListener !== undefined); discontinuityListener?.(errorInfo); // unmount the hook and verify that the listener was unsubscribed @@ -300,6 +315,8 @@ describe('usePresenceListener', () => { return subscribedListener !== undefined; }); + // wait until the + if (!subscribedListener) { expect.fail('subscribedListener is undefined'); } @@ -333,7 +350,10 @@ describe('usePresenceListener', () => { data: undefined, }); - expect(mockRoom.presence.get).toBeCalledTimes(1); + // Wait for our mock room's presence to be called + await waitFor(() => { + expect(mockRoom.presence.get).toBeCalledTimes(1); + }); // wait for the hook to retry the presence.get call await waitFor( @@ -426,8 +446,10 @@ describe('usePresenceListener', () => { data: undefined, }); - // ensure that the first call to presence.get was made - expect(mockRoom.presence.get).toBeCalledTimes(1); + // ensure that the first call to presence.get was made - eventually + await waitFor(() => { + expect(mockRoom.presence.get).toBeCalledTimes(1); + }); // now emit the second presence event, this should trigger the second call to presence.get subscribedListener({ @@ -437,8 +459,10 @@ describe('usePresenceListener', () => { data: undefined, }); - // ensure that the second call to presence.get was made - expect(mockRoom.presence.get).toBeCalledTimes(2); + // ensure that the first call to presence.get was made - eventually + await waitFor(() => { + expect(mockRoom.presence.get).toBeCalledTimes(2); + }); // since we already have a newer event, triggering the first event to resolve // should result in the first event being discarded diff --git a/test/react/hooks/use-presence.integration.test.tsx b/test/react/hooks/use-presence.integration.test.tsx index ec8381be..c456db24 100644 --- a/test/react/hooks/use-presence.integration.test.tsx +++ b/test/react/hooks/use-presence.integration.test.tsx @@ -48,7 +48,7 @@ describe('usePresence', () => { // create a second room and attach it, so we can listen for presence events const roomId = randomRoomId(); - const roomTwo = chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); + const roomTwo = await chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); await roomTwo.attach(); // start listening for presence events on room two diff --git a/test/react/hooks/use-presence.test.tsx b/test/react/hooks/use-presence.test.tsx index a863e6ef..93f1f1b5 100644 --- a/test/react/hooks/use-presence.test.tsx +++ b/test/react/hooks/use-presence.test.tsx @@ -3,11 +3,14 @@ import { act, cleanup, renderHook, waitFor } from '@testing-library/react'; import * as Ably from 'ably'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { InternalRoomStatus } from '../../../src/core/room-status.ts'; import { usePresence } from '../../../src/react/hooks/use-presence.ts'; import { makeTestLogger } from '../../helper/logger.ts'; import { makeRandomRoom } from '../../helper/room.ts'; +import { waitForEventualHookValue } from '../../helper/wait-for-eventual-hook.ts'; let mockRoom: Room; +let mockRoomContext: { room: Promise }; let mockCurrentConnectionStatus: ConnectionLifecycle; let mockCurrentRoomStatus: RoomLifecycle; let mockConnectionError: Ably.ErrorInfo; @@ -22,12 +25,12 @@ vi.mock('../../../src/react/hooks/use-chat-connection.js', () => ({ }), })); -vi.mock('../../../src/react/hooks/use-room.js', () => ({ - useRoom: () => ({ - room: mockRoom, - roomStatus: mockCurrentRoomStatus, - roomError: mockRoomError, - }), +vi.mock('../../../src/react/helper/use-room-context.js', () => ({ + useRoomContext: () => mockRoomContext, +})); + +vi.mock('../../../src/react/helper/use-room-status.js', () => ({ + useRoomStatus: () => ({ status: mockCurrentRoomStatus, error: mockRoomError }), })); vi.mock('../../../src/react/hooks/use-logger.js', () => ({ @@ -36,6 +39,13 @@ vi.mock('../../../src/react/hooks/use-logger.js', () => ({ vi.mock('ably'); +const updateMockRoom = (newRoom: Room) => { + mockRoom = newRoom; + (mockRoom.status as InternalRoomStatus).setStatus({ status: RoomLifecycle.Attached }); + mockRoomContext = { room: Promise.resolve(newRoom) }; +} + + describe('usePresence', () => { beforeEach(() => { // create a new mock room before each test, enabling presence @@ -43,21 +53,21 @@ describe('usePresence', () => { mockLogger = makeTestLogger(); mockCurrentConnectionStatus = ConnectionLifecycle.Connected; mockCurrentRoomStatus = RoomLifecycle.Attached; - mockRoom = makeRandomRoom({ + updateMockRoom(makeRandomRoom({ options: { presence: { enter: true, subscribe: true, }, }, - }); + })); }); afterEach(() => { cleanup(); }); - it('should provide the presence instance and chat status response metrics', () => { + it('should provide the presence instance and chat status response metrics', async () => { // set the connection and room errors to check that they are correctly provided mockConnectionError = new Ably.ErrorInfo('test error', 40000, 400); mockRoomError = new Ably.ErrorInfo('test error', 40000, 400); @@ -65,8 +75,8 @@ describe('usePresence', () => { const { result } = renderHook(() => usePresence()); // check that the presence instance and metrics are correctly provided - expect(result.current.presence).toBe(mockRoom.presence); - expect(result.current.isPresent).toBe(false); + await waitForEventualHookValue(result, mockRoom.presence, (value) => value.presence); + expect(result.current.isPresent).toBe(true); expect(result.current.error).toBeUndefined(); // check connection and room metrics are correctly provided @@ -82,7 +92,7 @@ describe('usePresence', () => { const { result, rerender } = renderHook(() => usePresence({ enterWithData: { test: 'data' } })); // ensure we have entered presence - expect(mockRoom.presence.enter).toHaveBeenCalledWith({ test: 'data' }); + await waitFor(() => { expect(mockRoom.presence.enter).toHaveBeenCalledWith({ test: 'data' }); }); await waitFor(() => result.current.isPresent); @@ -91,14 +101,14 @@ describe('usePresence', () => { expect(result.current.isPresent).toBe(true); // change the mock room instance - mockRoom = makeRandomRoom({ + updateMockRoom(makeRandomRoom({ options: { presence: { enter: true, subscribe: true, }, }, - }); + })); vi.spyOn(mockRoom.presence, 'enter'); @@ -106,7 +116,7 @@ describe('usePresence', () => { rerender(); // ensure we have entered presence on the new room instance - expect(mockRoom.presence.enter).toHaveBeenCalledWith({ test: 'data' }); + await waitFor(() => { expect(mockRoom.presence.enter).toHaveBeenCalledWith({ test: 'data' }); }); await waitFor(() => result.current.isPresent); expect(result.current.isPresent).toBe(true); @@ -164,7 +174,7 @@ describe('usePresence', () => { const { result } = renderHook(() => usePresence({ enterWithData: { test: 'data' } })); // expect the enter method to be called - expect(enterSpy).toHaveBeenCalled(); + await waitFor(() => { expect(enterSpy).toHaveBeenCalled(); }); // wait for the error to be set from the useEffect await waitFor( @@ -212,7 +222,7 @@ describe('usePresence', () => { } }); - it('should subscribe and unsubscribe to discontinuity events', () => { + it('should subscribe and unsubscribe to discontinuity events', async () => { const mockOff = vi.fn(); const mockDiscontinuityListener = vi.fn(); @@ -228,7 +238,7 @@ describe('usePresence', () => { // check that the listener was subscribed to the discontinuity events by triggering the listener const errorInfo = new Ably.ErrorInfo('test', 50000, 500); - expect(discontinuityListener).toBeDefined(); + await waitFor(() => { expect(discontinuityListener).toBeDefined(); }); discontinuityListener?.(errorInfo); expect(mockDiscontinuityListener).toHaveBeenCalledWith(errorInfo); diff --git a/test/react/hooks/use-room-reactions.integration.test.tsx b/test/react/hooks/use-room-reactions.integration.test.tsx index 7b7611fa..d572fe8e 100644 --- a/test/react/hooks/use-room-reactions.integration.test.tsx +++ b/test/react/hooks/use-room-reactions.integration.test.tsx @@ -36,7 +36,7 @@ describe('useRoomReactions', () => { // create a second room and attach it, so we can receive reactions const roomId = randomRoomId(); - const roomTwo = chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); + const roomTwo = await chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); await roomTwo.attach(); // store the received reactions @@ -87,7 +87,7 @@ describe('useRoomReactions', () => { // create a second room and attach it, so we can send a reaction const roomId = randomRoomId(); - const roomTwo = chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); + const roomTwo = await chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); await roomTwo.attach(); // store the received reactions @@ -119,6 +119,7 @@ describe('useRoomReactions', () => { render(); // wait for the room to be attached + chatClientOne.logger.info('got here'); await waitFor( () => { expect(currentRoomStatus).toBe(RoomLifecycle.Attached); diff --git a/test/react/hooks/use-room-reactions.test.tsx b/test/react/hooks/use-room-reactions.test.tsx index ee2ab499..bf91df43 100644 --- a/test/react/hooks/use-room-reactions.test.tsx +++ b/test/react/hooks/use-room-reactions.test.tsx @@ -6,42 +6,57 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { useRoomReactions } from '../../../src/react/hooks/use-room-reactions.ts'; import { makeTestLogger } from '../../helper/logger.ts'; import { makeRandomRoom } from '../../helper/room.ts'; +import { waitForEventualHookValue, waitForEventualHookValueToBeDefined } from '../../helper/wait-for-eventual-hook.ts'; let mockRoom: Room; let mockLogger: ReturnType; +let mockRoomContext: { room: Promise }; // apply mocks for the useChatConnection and useRoom hooks vi.mock('../../../src/react/hooks/use-chat-connection.js', () => ({ useChatConnection: () => ({ currentStatus: ConnectionLifecycle.Connected }), })); -vi.mock('../../../src/react/hooks/use-room.js', () => ({ - useRoom: () => ({ room: mockRoom, roomStatus: RoomLifecycle.Attached }), +vi.mock('../../../src/react/helper/use-room-context.js', () => ({ + useRoomContext: () => { + mockLogger.debug('useRoomContext() called;'); + return mockRoomContext; + }, })); +vi.mock('../../../src/react/helper/use-room-status.js', () => ({ + useRoomStatus: () => ({ status: RoomLifecycle.Attached }), +})); + + vi.mock('../../../src/react/hooks/use-logger.js', () => ({ useLogger: () => mockLogger, })); vi.mock('ably'); +const updateMockRoom = (room: Room) => { + mockRoom = room; + mockRoomContext = { room: Promise.resolve(mockRoom) }; +} + describe('useRoomReactions', () => { beforeEach(() => { // create a new mock room before each test vi.resetAllMocks(); mockLogger = makeTestLogger(); - mockRoom = makeRandomRoom({ options: { reactions: {} } }); + updateMockRoom(makeRandomRoom({ options: { reactions: {} } })); }); afterEach(() => { cleanup(); }); - it('should provide the room reactions instance and correct chat status response metrics', () => { + it('should provide the room reactions instance and correct chat status response metrics', async () => { const { result } = renderHook(() => useRoomReactions()); - // check that the room reactions instance is correctly provided - expect(result.current.reactions).toBe(mockRoom.reactions); + // check that the room reactions instance is correctly provided - eventually + await vi.waitFor(() => { expect(result.current.reactions).toBe(mockRoom.reactions); }); // check connection and room metrics are correctly provided expect(result.current.roomStatus).toBe(RoomLifecycle.Attached); @@ -65,7 +80,7 @@ describe('useRoomReactions', () => { expect(sendSpy).toHaveBeenCalledWith({ type: 'like' }); }); - it('should correctly subscribe and unsubscribe to reactions', () => { + it('should correctly subscribe and unsubscribe to reactions', async () => { // mock listener and associated unsubscribe function const mockListener = vi.fn(); const mockUnsubscribe = vi.fn(); @@ -86,9 +101,10 @@ describe('useRoomReactions', () => { }; // update the mock room with the new reactions object - mockRoom = { ...mockRoom, reactions: mockReactions }; + updateMockRoom({ ...mockRoom, reactions: mockReactions }); - const { unmount } = renderHook(() => useRoomReactions({ listener: mockListener })); + const { result, unmount } = renderHook(() => useRoomReactions({ listener: mockListener })); + await waitForEventualHookValueToBeDefined(result, (value) => value.reactions); // verify that subscribe was called with the mock listener on mount by triggering a reaction event const reaction = { @@ -110,23 +126,24 @@ describe('useRoomReactions', () => { expect(mockUnsubscribe).toHaveBeenCalled(); }); - it('should handle rerender if the room instance changes', () => { + it('should handle rerender if the room instance changes', async () => { const { result, rerender } = renderHook(() => useRoomReactions()); // check the initial state of the reactions object - expect(result.current.reactions).toBe(mockRoom.reactions); + await waitForEventualHookValue(result, mockRoom.reactions, (value) => value.reactions); // change the mock room instance - mockRoom = makeRandomRoom({ options: { reactions: {} } }); + updateMockRoom(makeRandomRoom({ options: { reactions: {} } })); + mockLogger.debug('rerendering with new room instance'); // re-render to trigger the useEffect rerender(); // check that the room reactions instance is updated - expect(result.current.reactions).toBe(mockRoom.reactions); + await waitForEventualHookValue(result, mockRoom.reactions, (value) => value.reactions); }); - it('should subscribe and unsubscribe to discontinuity events', () => { + it('should subscribe and unsubscribe to discontinuity events', async () => { const mockOff = vi.fn(); const mockDiscontinuityListener = vi.fn(); @@ -138,7 +155,8 @@ describe('useRoomReactions', () => { }); // render the hook with a discontinuity listener - const { unmount } = renderHook(() => useRoomReactions({ onDiscontinuity: mockDiscontinuityListener })); + const { result, unmount } = renderHook(() => useRoomReactions({ onDiscontinuity: mockDiscontinuityListener })); + await waitForEventualHookValueToBeDefined(result, (value) => value.reactions); // check that the listener was subscribed to the discontinuity events by triggering one const errorInfo = new Ably.ErrorInfo('test', 50000, 500); diff --git a/test/react/hooks/use-room.test.tsx b/test/react/hooks/use-room.test.tsx index b81e71d6..03c82273 100644 --- a/test/react/hooks/use-room.test.tsx +++ b/test/react/hooks/use-room.test.tsx @@ -32,7 +32,7 @@ describe('useRoom', () => { const TestThrowError: React.FC = () => { expect(() => useRoom()).toThrowErrorInfo({ code: 40000, - message: 'useRoom hook must be used within a ChatRoomProvider', + message: 'useRoom hook must be used within a ', }); return null; }; @@ -48,10 +48,7 @@ describe('useRoom', () => { it('should get the room from the context without error', async () => { const chatClient = newChatClient(); - let setResponse: (response: UseRoomResponse) => void; - const responsePromise = new Promise((resolve) => { - setResponse = resolve; - }); + let latestResponse: UseRoomResponse | undefined; const roomId = randomRoomId(); const TestProvider = () => ( @@ -63,21 +60,20 @@ describe('useRoom', () => { > { - setResponse(response); + latestResponse = response; }} /> ); render(); - const response = await responsePromise; - expect(response.room.roomId).toBe(roomId); - expect(response.attach).toBeTruthy(); - expect(response.detach).toBeTruthy(); - expect(response.roomStatus).toBe(RoomLifecycle.Initializing); + await vi.waitFor(() => { expect(latestResponse.room?.roomId).toBe(roomId); }); + expect(latestResponse?.attach).toBeTruthy(); + expect(latestResponse?.detach).toBeTruthy(); + expect(latestResponse?.roomStatus).toBe(RoomLifecycle.Initializing); }); - it('should return working shortcuts for attach and detach functions', () => { + it('should return working shortcuts for attach and detach functions', async () => { const chatClient = newChatClient(); let called = false; const roomId = randomRoomId(); @@ -91,6 +87,8 @@ describe('useRoom', () => { > { + if (!response.room) return; + vi.spyOn(response.room, 'attach').mockImplementation(() => Promise.resolve()); vi.spyOn(response.room, 'detach').mockImplementation(() => Promise.resolve()); // no awaiting since we don't care about result, just that the relevant function was called @@ -105,15 +103,16 @@ describe('useRoom', () => { ); render(); - expect(called).toBe(true); + + await vi.waitFor(() => called, { timeout: 5000 }); }); - it('should attach, detach and release correctly with the same room twice', () => { + it('should attach, detach and release correctly with the same room twice', async () => { const chatClient = newChatClient(); let called1 = 0; let called2 = 0; const roomId = randomRoomId(); - const room = chatClient.rooms.get(roomId, RoomOptionsDefaults); + const room = await chatClient.rooms.get(roomId, RoomOptionsDefaults); vi.spyOn(room, 'attach').mockImplementation(() => Promise.resolve()); vi.spyOn(room, 'detach').mockImplementation(() => Promise.resolve()); @@ -164,9 +163,10 @@ describe('useRoom', () => { room2={true} />, ); + expect(called1).toBe(1); expect(called2).toBe(1); - expect(room.attach).toHaveBeenCalledTimes(1); + await vi.waitFor(() => { expect(room.attach).toHaveBeenCalledTimes(1); }); expect(room.detach).toHaveBeenCalledTimes(0); expect(chatClient.rooms.release).toHaveBeenCalledTimes(0); @@ -176,9 +176,9 @@ describe('useRoom', () => { room2={true} />, ); - expect(called1).toBe(1); - expect(called2).toBe(2); - expect(room.attach).toHaveBeenCalledTimes(1); + expect(called1).toBe(2); + expect(called2).toBe(3); + await vi.waitFor(() => { expect(room.attach).toHaveBeenCalledTimes(1); }); expect(room.detach).toHaveBeenCalledTimes(0); expect(chatClient.rooms.release).toHaveBeenCalledTimes(0); @@ -188,9 +188,9 @@ describe('useRoom', () => { room2={true} />, ); - expect(called1).toBe(2); - expect(called2).toBe(3); - expect(room.attach).toHaveBeenCalledTimes(1); + expect(called1).toBe(3); + expect(called2).toBe(4); + await vi.waitFor(() => { expect(room.attach).toHaveBeenCalledTimes(1); }); expect(room.detach).toHaveBeenCalledTimes(0); expect(chatClient.rooms.release).toHaveBeenCalledTimes(0); @@ -200,9 +200,9 @@ describe('useRoom', () => { room2={true} />, ); - expect(called1).toBe(2); - expect(called2).toBe(4); - expect(room.attach).toHaveBeenCalledTimes(1); + expect(called1).toBe(3); + expect(called2).toBe(5); + await vi.waitFor(() => { expect(room.attach).toHaveBeenCalledTimes(1); }); expect(room.detach).toHaveBeenCalledTimes(0); expect(chatClient.rooms.release).toHaveBeenCalledTimes(0); @@ -212,18 +212,18 @@ describe('useRoom', () => { room2={false} />, ); - expect(called1).toBe(2); - expect(called2).toBe(4); - expect(room.attach).toHaveBeenCalledTimes(1); + expect(called1).toBe(3); + expect(called2).toBe(5); + await vi.waitFor(() => { expect(room.attach).toHaveBeenCalledTimes(1); }); // room.detach is not called when releasing, detach happens via lifecycleManager but skipping the public API expect(room.detach).toHaveBeenCalledTimes(0); - expect(chatClient.rooms.release).toHaveBeenCalledWith(roomId); + await vi.waitFor(() => { expect(chatClient.rooms.release).toHaveBeenCalledWith(roomId); }); }); it('should correctly set room status callback', async () => { const chatClient = newChatClient(); const roomId = randomRoomId(); - const room = chatClient.rooms.get(roomId, RoomOptionsDefaults); + const room = await chatClient.rooms.get(roomId, RoomOptionsDefaults); let listeners: RoomStatusListener[] = []; @@ -264,6 +264,9 @@ describe('useRoom', () => { { wrapper: WithClient }, ); + // useEffect is async, so we need to wait for it to run + await vi.waitFor(() => { expect(listeners.length).toBe(2); }); + act(() => { for (const l of listeners) l(expectedChange); }); @@ -275,7 +278,7 @@ describe('useRoom', () => { it('should correctly set room status and error state variables', async () => { const chatClient = newChatClient(); const roomId = randomRoomId(); - const room = chatClient.rooms.get(roomId, RoomOptionsDefaults); + const room = await chatClient.rooms.get(roomId, RoomOptionsDefaults); let listeners: RoomStatusListener[] = []; @@ -303,12 +306,15 @@ describe('useRoom', () => { const { result } = renderHook(() => useRoom(), { wrapper: WithClient }); + // Because useEffect adds listeners async, wait until we have a listener + await vi.waitFor(() => { expect(listeners.length).toBe(1); }); + act(() => { const change = { current: RoomLifecycle.Attached, previous: RoomLifecycle.Attaching }; for (const l of listeners) l(change); }); - expect(result.current.roomStatus).toBe(RoomLifecycle.Attached); + await vi.waitFor( () => { expect(result.current.roomStatus).toBe(RoomLifecycle.Attached); }); expect(result.current.roomError).toBeUndefined(); act(() => { @@ -316,7 +322,8 @@ describe('useRoom', () => { for (const l of listeners) l(change); }); - expect(result.current.roomStatus).toBe(RoomLifecycle.Detaching); + + await vi.waitFor( () => { expect(result.current.roomStatus).toBe(RoomLifecycle.Detaching); }); expect(result.current.roomError).toBeUndefined(); const err = new Ably.ErrorInfo('test', 123, 456); @@ -325,8 +332,8 @@ describe('useRoom', () => { for (const l of listeners) l(change); }); - expect(result.current.roomStatus).toBe(RoomLifecycle.Failed); - expect(result.current.roomError).toBe(err); + await vi.waitFor( () => { expect(result.current.roomStatus).toBe(RoomLifecycle.Failed); }); + await vi.waitFor( () => { expect(result.current.roomError).toBe(err); }); await room.detach(); }); }); diff --git a/test/react/hooks/use-typing.integration.test.tsx b/test/react/hooks/use-typing.integration.test.tsx index 5b0ce4d5..3edcf769 100644 --- a/test/react/hooks/use-typing.integration.test.tsx +++ b/test/react/hooks/use-typing.integration.test.tsx @@ -36,7 +36,7 @@ describe('useTyping', () => { // create a second room and attach it, so we can listen for typing events const roomId = randomRoomId(); - const roomTwo = chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); + const roomTwo = await chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); await roomTwo.attach(); // start listening for typing events on room two @@ -83,7 +83,7 @@ describe('useTyping', () => { // create a second room and attach it, so we can send typing events const roomId = randomRoomId(); - const roomTwo = chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); + const roomTwo = await chatClientTwo.rooms.get(roomId, RoomOptionsDefaults); await roomTwo.attach(); // store the received typing events for room one diff --git a/test/react/hooks/use-typing.test.tsx b/test/react/hooks/use-typing.test.tsx index 66cfcb60..d5ff5e87 100644 --- a/test/react/hooks/use-typing.test.tsx +++ b/test/react/hooks/use-typing.test.tsx @@ -3,11 +3,14 @@ import { act, cleanup, renderHook, waitFor } from '@testing-library/react'; import * as Ably from 'ably'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { DefaultStatus, InternalRoomStatus } from '../../../src/core/room-status.ts'; import { useTyping } from '../../../src/react/hooks/use-typing.ts'; import { makeTestLogger } from '../../helper/logger.ts'; import { makeRandomRoom } from '../../helper/room.ts'; +import { waitForEventualHookValue, waitForEventualHookValueToBeDefined } from '../../helper/wait-for-eventual-hook.ts'; let mockRoom: Room; +let mockRoomContext:{ room: Promise }; let mockLogger: Logger; // apply mocks for the useChatConnection and useRoom hooks @@ -15,8 +18,13 @@ vi.mock('../../../src/react/hooks/use-chat-connection.js', () => ({ useChatConnection: () => ({ currentStatus: ConnectionLifecycle.Connected }), })); -vi.mock('../../../src/react/hooks/use-room.js', () => ({ - useRoom: () => ({ room: mockRoom, roomStatus: RoomLifecycle.Attached }), + +vi.mock('../../../src/react/helper/use-room-context.js', () => ({ + useRoomContext: () => mockRoomContext, +})); + +vi.mock('../../../src/react/helper/use-room-status.js', () => ({ + useRoomStatus: () => ({ status: RoomLifecycle.Attached}), })); vi.mock('../../../src/react/hooks/use-logger.js', () => ({ @@ -25,11 +33,17 @@ vi.mock('../../../src/react/hooks/use-logger.js', () => ({ vi.mock('ably'); +const updateMockRoom = (newRoom: Room) => { + mockRoom = newRoom; + (mockRoom.status as InternalRoomStatus).setStatus({status: RoomLifecycle.Attached}); + mockRoomContext = { room: Promise.resolve(newRoom) }; +} + describe('useTyping', () => { beforeEach(() => { // create a new mock room before each test, enabling typing vi.resetAllMocks(); - mockRoom = makeRandomRoom({ options: { typing: { timeoutMs: 500 } } }); + updateMockRoom(makeRandomRoom({ options: { typing: { timeoutMs: 500 } } })); mockLogger = makeTestLogger(); }); @@ -37,11 +51,11 @@ describe('useTyping', () => { cleanup(); }); - it('should provide the typing instance and chat status response metrics', () => { + it('should provide the typing instance and chat status response metrics', async () => { const { result } = renderHook(() => useTyping()); // check that the typing instance is correctly provided - expect(result.current.typingIndicators).toBe(mockRoom.typing); + await waitForEventualHookValue(result, mockRoom.typing, (value) => value.typingIndicators); // check connection and room metrics are correctly provided expect(result.current.roomStatus).toBe(RoomLifecycle.Attached); @@ -50,7 +64,7 @@ describe('useTyping', () => { expect(result.current.connectionError).toBeUndefined(); }); - it('should correctly subscribe and unsubscribe to typing events', () => { + it('should correctly subscribe and unsubscribe to typing events', async () => { // mock listener and associated unsubscribe function const mockListener = vi.fn(); const mockUnsubscribe = vi.fn(); @@ -67,9 +81,10 @@ describe('useTyping', () => { }; // update the mock room with the new typing object - mockRoom = { ...mockRoom, typing: mockTyping }; + updateMockRoom({ ...mockRoom, status: new DefaultStatus(mockLogger), typing: mockTyping }); + const { result, unmount } = renderHook(() => useTyping({ listener: mockListener })); - const { unmount } = renderHook(() => useTyping({ listener: mockListener })); + await waitForEventualHookValueToBeDefined(result, (value) => value.typingIndicators); // verify that subscribe was called with the mock listener on mount by triggering an event const typingEvent = { currentlyTyping: new Set() }; @@ -253,29 +268,29 @@ describe('useTyping', () => { ); }); - it('should handle rerender if the room instance changes', () => { + it('should handle rerender if the room instance changes', async () => { const { result, rerender } = renderHook(() => useTyping()); // check the initial state of the typing instance - expect(result.current.typingIndicators).toBe(mockRoom.typing); + await waitForEventualHookValue(result, mockRoom.typing, (value) => value.typingIndicators); // change the mock room instance - mockRoom = makeRandomRoom({ + updateMockRoom(makeRandomRoom({ options: { typing: { timeoutMs: 500, }, }, - }); + })); // re-render to trigger the useEffect rerender(); // check that the typing instance is updated - expect(result.current.typingIndicators).toBe(mockRoom.typing); + await waitForEventualHookValue(result, mockRoom.typing, (value) => value.typingIndicators); }); - it('should subscribe and unsubscribe to discontinuity events', () => { + it('should subscribe and unsubscribe to discontinuity events', async () => { const mockOff = vi.fn(); const mockDiscontinuityListener = vi.fn(); @@ -291,7 +306,9 @@ describe('useTyping', () => { // check that the listener was subscribed to the discontinuity events by triggering one const discontinuityEvent = new Ably.ErrorInfo('test', 50000, 500); - expect(discontinuityListener).toBeDefined(); + await waitFor(() => { + expect(discontinuityListener).toBeDefined(); + }); discontinuityListener?.(discontinuityEvent); expect(mockDiscontinuityListener).toHaveBeenCalledWith(discontinuityEvent); diff --git a/test/react/providers/room-provider.test.tsx b/test/react/providers/room-provider.test.tsx index 1f93bd2a..4fbd2e9e 100644 --- a/test/react/providers/room-provider.test.tsx +++ b/test/react/providers/room-provider.test.tsx @@ -3,6 +3,7 @@ import { cleanup, render } from '@testing-library/react'; import React from 'react'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import { useRoom } from '../../../src/react/index.ts'; import { ChatClientProvider } from '../../../src/react/providers/chat-client-provider.tsx'; import { ChatRoomProvider } from '../../../src/react/providers/chat-room-provider.tsx'; import { newChatClient } from '../../helper/chat.ts'; @@ -15,9 +16,14 @@ describe('ChatRoomProvider', () => { cleanup(); }); - it('should create a provider without error', () => { + it('should create a provider without error', async () => { const chatClient = newChatClient() as unknown as ChatClient; + let roomResolved = false; const TestComponent = () => { + const { room } = useRoom(); + if (room) { + roomResolved = true; + } return
; }; const roomId = randomRoomId(); @@ -38,13 +44,14 @@ describe('ChatRoomProvider', () => { render(); // Try to get the client to get a room with different options, should fail - expect(() => chatClient.rooms.get(roomId, RoomOptionsDefaults)).toThrowErrorInfoWithCode(40000); + await vi.waitFor(() => { expect(roomResolved).toBeTruthy(); }); + await expect(() => chatClient.rooms.get(roomId, RoomOptionsDefaults)).rejects.toBeErrorInfoWithCode(40000); // Now try it with the right options, should be fine - expect(() => chatClient.rooms.get(roomId, { reactions: RoomOptionsDefaults.reactions })).toBeTruthy(); + await chatClient.rooms.get(roomId, { reactions: RoomOptionsDefaults.reactions }); }); - it('should correctly release rooms', () => { + it('should correctly release rooms', async () => { const chatClient = newChatClient() as unknown as ChatClient; const TestComponent = () => { return
; @@ -67,7 +74,7 @@ describe('ChatRoomProvider', () => { const r = render(); // Try to get the client to get a room with different options, should fail - expect(() => chatClient.rooms.get(roomId, RoomOptionsDefaults)).toThrowErrorInfoWithCode(40000); + await expect(() => chatClient.rooms.get(roomId, RoomOptionsDefaults)).rejects.toBeErrorInfoWithCode(40000); // Now try it with the right options, should be fine expect(() => chatClient.rooms.get(roomId, { reactions: RoomOptionsDefaults.reactions })); @@ -80,14 +87,14 @@ describe('ChatRoomProvider', () => { expect(() => chatClient.rooms.get(roomId, RoomOptionsDefaults)).toBeTruthy(); }); - it('should attach and detach correctly', () => { + it('should attach and detach correctly', async () => { const chatClient = newChatClient() as unknown as ChatClient; const TestComponent = () => { return
; }; const roomId = randomRoomId(); - const room = chatClient.rooms.get(roomId, { reactions: RoomOptionsDefaults.reactions }); + const room = await chatClient.rooms.get(roomId, { reactions: RoomOptionsDefaults.reactions }); expect(room).toBeTruthy(); vi.spyOn(room, 'attach'); @@ -110,24 +117,24 @@ describe('ChatRoomProvider', () => { const r = render(); // Make sure the room is attaching - expect(room.attach).toHaveBeenCalled(); + await vi.waitFor(() => { expect(room.attach).toHaveBeenCalled(); }); r.unmount(); // Make sure the room is detaching - expect(room.detach).toHaveBeenCalled(); + await vi.waitFor(() => { expect(room.detach).toHaveBeenCalled(); }); // Try to get the client to get a room with different options, should fail - expect(() => chatClient.rooms.get(roomId, RoomOptionsDefaults)).toThrowErrorInfoWithCode(40000); + await expect(() => chatClient.rooms.get(roomId, RoomOptionsDefaults)).rejects.toBeErrorInfoWithCode(40000); }); - it('should not attach, detach, or release when not configured to do so', () => { + it('should not attach, detach, or release when not configured to do so', async () => { const chatClient = newChatClient() as unknown as ChatClient; const TestComponent = () => { return
; }; const roomId = randomRoomId(); - const room = chatClient.rooms.get(roomId, { reactions: RoomOptionsDefaults.reactions }); + const room = await chatClient.rooms.get(roomId, { reactions: RoomOptionsDefaults.reactions }); expect(room).toBeTruthy(); vi.spyOn(room, 'attach'); @@ -157,7 +164,7 @@ describe('ChatRoomProvider', () => { expect(room.detach).toHaveBeenCalledTimes(0); // Try to get the client to get a room with different options, should fail (since it should not be released) - expect(() => chatClient.rooms.get(roomId, RoomOptionsDefaults)).toThrowErrorInfoWithCode(40000); + await expect(() => chatClient.rooms.get(roomId, RoomOptionsDefaults)).rejects.toBeErrorInfoWithCode(40000); void chatClient.rooms.release(roomId); });