diff --git a/package-lock.json b/package-lock.json index fd94901..bc4216e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", + "got": "^11.8.6", "jest": "^29.6.1", "jest-environment-jsdom": "^29.6.1", "jsdom": "^20.0.0", @@ -2884,6 +2885,20 @@ "node": ">=8" } }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -4106,18 +4121,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/execa/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -4378,14 +4381,12 @@ } }, "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dependencies": { - "pump": "^3.0.0" - }, + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5283,18 +5284,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/jest-changed-files/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/jest-changed-files/node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -7510,18 +7499,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/run-applescript/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/run-applescript/node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", diff --git a/package.json b/package.json index d81aa85..bd8287e 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", + "got": "^11.8.6", "jest": "^29.6.1", "jest-environment-jsdom": "^29.6.1", "jsdom": "^20.0.0", diff --git a/src/hooks/useChannel.test.tsx b/src/hooks/useChannel.test.tsx index 3d03484..9241d2f 100644 --- a/src/hooks/useChannel.test.tsx +++ b/src/hooks/useChannel.test.tsx @@ -2,71 +2,106 @@ import React from 'react'; import { provideSdkInstance } from '../AblyReactHooks'; import { useChannel } from './useChannel'; import { useState } from 'react'; -import { render, screen } from '@testing-library/react'; -import { FakeAblySdk, FakeAblyChannels } from '../fakes/ably'; +import { act, render, screen, waitFor } from '@testing-library/react'; import { Types } from 'ably'; -import { act } from 'react-dom/test-utils'; +import { TestApp } from '../test/testapp'; describe('useChannel', () => { - let channels: FakeAblyChannels; - let ablyClient: FakeAblySdk; - let otherClient: FakeAblySdk; - - beforeEach(() => { - channels = new FakeAblyChannels(['blah']); - ablyClient = new FakeAblySdk().connectTo(channels); - otherClient = new FakeAblySdk().connectTo(channels); - provideSdkInstance(ablyClient as any); + let testApp: TestApp; + + beforeAll(async () => { + testApp = await TestApp.create(); }); + afterAll(async () => { + await testApp.delete(); + }, 10_000); + it('component can useChannel and renders nothing by default', async () => { + const client = await testApp.client(); + provideSdkInstance(client); render(); const messageUl = screen.getAllByRole('messages')[0]; expect(messageUl.childElementCount).toBe(0); + client.close(); }); it('component updates when message arrives', async () => { + const client = await testApp.client(); + const otherClient = await testApp.client(); + provideSdkInstance(client); render(); await act(async () => { - otherClient.channels.get('blah').publish({ text: 'message text' }); + await otherClient.channels + .get('blah') + .publish('event', { text: 'message text' }); }); const messageUl = screen.getAllByRole('messages')[0]; - expect(messageUl.childElementCount).toBe(1); - expect(messageUl.children[0].innerHTML).toBe('message text'); + + await waitFor(() => { + expect(messageUl.childElementCount).toBe(1); + expect(messageUl.children[0].innerHTML).toBe('message text'); + }); + client.close(); + otherClient.close(); }); it('component updates when multiple messages arrive', async () => { + const client = await testApp.client(); + const otherClient = await testApp.client(); + provideSdkInstance(client); render(); await act(async () => { - otherClient.channels.get('blah').publish({ text: 'message text1' }); - otherClient.channels.get('blah').publish({ text: 'message text2' }); + await otherClient.channels + .get('blah') + .publish('event', { text: 'message text1' }); + await otherClient.channels + .get('blah') + .publish('event', { text: 'message text2' }); }); const messageUl = screen.getAllByRole('messages')[0]; - expect(messageUl.children[0].innerHTML).toBe('message text1'); - expect(messageUl.children[1].innerHTML).toBe('message text2'); + + await waitFor(() => { + expect(messageUl.children[0].innerHTML).toBe('message text1'); + expect(messageUl.children[1].innerHTML).toBe('message text2'); + }); + client.close(); + otherClient.close(); }); it('useChannel works with multiple clients', async () => { + const client = await testApp.client(); + const otherClient = await testApp.client(); + provideSdkInstance(client); render( ); await act(async () => { - ablyClient.channels.get('blah').publish({ text: 'message text1' }); - otherClient.channels.get('bleh').publish({ text: 'message text2' }); + await client.channels + .get('blah') + .publish('event', { text: 'message text1' }); + await otherClient.channels + .get('bleh') + .publish('event', { text: 'message text2' }); }); const messageUl = screen.getAllByRole('messages')[0]; - expect(messageUl.children[0].innerHTML).toBe('message text1'); - expect(messageUl.children[1].innerHTML).toBe('message text2'); + + await waitFor(() => { + expect(messageUl.children[0].innerHTML).toBe('message text1'); + expect(messageUl.children[1].innerHTML).toBe('message text2'); + }); + client.close(); + otherClient.close(); }); }); diff --git a/src/hooks/useChannel.ts b/src/hooks/useChannel.ts index 6f030cf..7176336 100644 --- a/src/hooks/useChannel.ts +++ b/src/hooks/useChannel.ts @@ -53,7 +53,7 @@ export function useChannel( // To solve this, we set a timer, and if all the listeners have been removed, we know that the component // has been removed for good and we can detatch the channel. - if (channel.listeners.length === 0) { + if (channel.listeners.length === 0 && channel.state === "attached") { await channel.detach(); } }, 2500); diff --git a/src/hooks/usePresence.test.tsx b/src/hooks/usePresence.test.tsx index 84bab9c..51e3ab2 100644 --- a/src/hooks/usePresence.test.tsx +++ b/src/hooks/usePresence.test.tsx @@ -1,21 +1,34 @@ import React from 'react'; import { provideSdkInstance } from '../AblyReactHooks'; import { usePresence } from './usePresence'; -import { render, screen, act } from '@testing-library/react'; -import { FakeAblySdk, FakeAblyChannels } from '../fakes/ably'; +import { render, screen, act, waitFor } from '@testing-library/react'; +import { Types } from 'ably'; +import { TestApp } from '../test/testapp'; const testChannelName = 'testChannel'; describe('usePresence', () => { - let channels: FakeAblyChannels; - let ablyClient: FakeAblySdk; - let otherClient: FakeAblySdk; - - beforeEach(() => { - channels = new FakeAblyChannels([testChannelName]); - ablyClient = new FakeAblySdk().connectTo(channels); - otherClient = new FakeAblySdk().connectTo(channels); - provideSdkInstance(ablyClient as any); + let testApp: TestApp; + let ablyClient: Types.RealtimePromise; + let otherClient: Types.RealtimePromise; + + beforeAll(async () => { + testApp = await TestApp.create(); + }); + + beforeEach(async () => { + ablyClient = await testApp.client(); + otherClient = await testApp.client(); + provideSdkInstance(ablyClient); + }); + + afterEach(() => { + ablyClient.close(); + otherClient.close(); + }); + + afterAll(async () => { + await testApp.delete(); }); it('presence data is not visible on first render as it runs in an effect', async () => { diff --git a/src/test/testapp.ts b/src/test/testapp.ts new file mode 100644 index 0000000..20ac810 --- /dev/null +++ b/src/test/testapp.ts @@ -0,0 +1,39 @@ +import * as Ably from 'ably'; +import { Types } from 'ably'; +import got from 'got'; + +export class TestApp { + id: string; + key: string; + + static async create() { + const url = 'https://sandbox-rest.ably.io/apps'; + const body = { keys: [{}] }; + + const res: { + appId: string; + keys: { keyStr: string }[]; + } = await got.post(url, { json: body }).json(); + + return new TestApp(res.appId, res.keys[0].keyStr); + } + + constructor(id: string, key: string) { + this.id = id; + this.key = key; + } + + client(clientId?: string): Types.RealtimePromise { + return new Ably.Realtime.Promise({ + key: this.key, + environment: 'sandbox', + clientId: clientId || null, + }); + } + + delete() { + return got.delete( + `https://sandbox-rest.ably.io/apps/${this.id}?key=${this.key}` + ); + } +}