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}`
+ );
+ }
+}