import type { Reaction } from '@ably/chat'; import { RoomLifecycle } from '@ably/chat'; import type * as Ably from 'ably'; import { beforeEach, describe, expect, it } from 'vitest'; import { newChatClient } from '../helper/chat'; import { getRandomRoom, waitForRoomStatus } from '../helper/room'; import { RealtimeChannelWithOptions } from '../unified-clients/realtime-extensions'; import { PublicChatClient } from '../unified-clients/RpcChat'; import { CHANNEL_OPTIONS_AGENT_STRING } from '../unified-clients/version'; interface TestContext { chat: PublicChatClient; } const waitForReactions = (foundTypes: string[], expectedTypes: string[]) => { return new Promise((resolve, reject) => { const interval = setInterval(() => { if (foundTypes.length === expectedTypes.length) { clearInterval(interval); clearTimeout(timeout); for (const [idx, foundType] of foundTypes.entries()) { const expectedType = expectedTypes[idx]; try { expect(foundType).toEqual(expectedType); } catch (error: unknown) { reject(error as Error); continue; } } resolve(); } }, 100); const timeout = setTimeout(() => { clearInterval(interval); reject(new Error('Timed out waiting for reactions')); }, 3000); }); }; describe('room-level reactions integration test', () => { beforeEach((context) => { context.chat = newChatClient(); }); // test not supported in UTS yet it.skip('sets the agent version on the channel', async (context) => { const { chat } = context; const room = getRandomRoom(chat); const channel = (await room.messages.channel) as RealtimeChannelWithOptions; expect(channel.channelOptions.params).toEqual(expect.objectContaining({ agent: CHANNEL_OPTIONS_AGENT_STRING })); }); it('sends and receives a reaction', async (context) => { const { chat } = context; const room = getRandomRoom(chat); const expectedReactions = ['like', 'like', 'love', 'hate']; const reactions: string[] = []; room.reactions.subscribe((reaction: Reaction) => { reactions.push(reaction.type); }); // Attach the room await room.attach(); // Send reactions for (const type of expectedReactions) { await room.reactions.send({ type }); } await waitForReactions(reactions, expectedReactions); }); // test not supported in UTS yet it.skip('handles discontinuities', async (context) => { const { chat } = context; const room = getRandomRoom(chat); // Attach the room await room.attach(); // Subscribe discontinuity events const discontinuityErrors: (Ably.ErrorInfo | undefined)[] = []; const { off } = room.reactions.onDiscontinuity((error: Ably.ErrorInfo | undefined) => { discontinuityErrors.push(error); }); const channelSuspendable = (await room.reactions.channel) as Ably.RealtimeChannel & { notifyState(state: 'suspended' | 'attached'): void; }; // Simulate a discontinuity by forcing a channel into suspended state channelSuspendable.notifyState('suspended'); // Wait for the room to go into suspended await waitForRoomStatus(room.status, RoomLifecycle.Suspended); // Force the channel back into attached state - to simulate recovery channelSuspendable.notifyState('attached'); // Wait for the room to go into attached await waitForRoomStatus(room.status, RoomLifecycle.Attached); // Wait for a discontinuity event to be received expect(discontinuityErrors.length).toBe(1); // Unsubscribe from discontinuity events off(); // Simulate a discontinuity by forcing a channel into suspended state channelSuspendable.notifyState('suspended'); // Wait for the room to go into suspended await waitForRoomStatus(room.status, RoomLifecycle.Suspended); // We shouldn't get any more discontinuity events expect(discontinuityErrors.length).toBe(1); // Calling off again should be a no-op off(); }); });