From 9e727e2ef18498e47f10566a3c6336819bcf8061 Mon Sep 17 00:00:00 2001 From: Lawrence Forooghian Date: Tue, 21 Nov 2023 15:14:31 -0300 Subject: [PATCH] Make RequireJS work in modules.test.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This gives these tests access to the same test helper APIs that the other ones use. And, it means that we can access test helpers inside the body of a `describe` block, which I’ll need in an upcoming commit in order to call the shared delta tests added in b6d860c. --- test/browser/modules.test.js | 1078 +++++++++++++------------- test/common/modules/shared_helper.js | 10 +- test/support/browser_setup.js | 12 +- 3 files changed, 553 insertions(+), 547 deletions(-) diff --git a/test/browser/modules.test.js b/test/browser/modules.test.js index c7bdc90432..775bc05e4e 100644 --- a/test/browser/modules.test.js +++ b/test/browser/modules.test.js @@ -22,694 +22,698 @@ import { MessageInteractions, } from '../../build/modules/index.js'; -describe('browser/modules', function () { - this.timeout(10 * 1000); - const expect = chai.expect; - const BufferUtils = BaseRest.Platform.BufferUtils; - let ablyClientOptions; - let testResourcesPath; - let loadTestData; - let testMessageEquality; - let randomString; - let getTestApp; - - before((done) => { - ablyClientOptions = window.ablyHelpers.ablyClientOptions; - testResourcesPath = window.ablyHelpers.testResourcesPath; - testMessageEquality = window.ablyHelpers.testMessageEquality; - randomString = window.ablyHelpers.randomString; - getTestApp = window.ablyHelpers.getTestApp; - - loadTestData = async (dataPath) => { +function registerAblyModulesTests(helper) { + describe('browser/modules', function () { + this.timeout(10 * 1000); + const expect = chai.expect; + const BufferUtils = BaseRest.Platform.BufferUtils; + const ablyClientOptions = helper.ablyClientOptions; + const testResourcesPath = helper.testResourcesPath; + const testMessageEquality = helper.testMessageEquality; + const randomString = helper.randomString; + const getTestApp = helper.getTestApp; + const loadTestData = async (dataPath) => { return new Promise((resolve, reject) => { - window.ablyHelpers.loadTestData(dataPath, (err, testData) => (err ? reject(err) : resolve(testData))); + helper.loadTestData(dataPath, (err, testData) => (err ? reject(err) : resolve(testData))); }); }; - window.ablyHelpers.setupApp(done); - }); + before((done) => { + helper.setupApp(done); + }); - describe('without any modules', () => { - for (const clientClass of [BaseRest, BaseRealtime]) { - describe(clientClass.name, () => { - it('throws an error due to the absence of an HTTP module', () => { - expect(() => new clientClass(ablyClientOptions(), {})).to.throw( - 'No HTTP request module provided. Provide at least one of the FetchRequest or XHRRequest modules.' - ); + describe('without any modules', () => { + for (const clientClass of [BaseRest, BaseRealtime]) { + describe(clientClass.name, () => { + it('throws an error due to the absence of an HTTP module', () => { + expect(() => new clientClass(ablyClientOptions(), {})).to.throw( + 'No HTTP request module provided. Provide at least one of the FetchRequest or XHRRequest modules.' + ); + }); }); - }); - } - }); + } + }); - describe('Rest', () => { - const restScenarios = [ - { - description: 'use push admin functionality', - action: (client) => client.push.admin.publish({ clientId: 'foo' }, { data: { bar: 'baz' } }), - }, - { description: 'call `time()`', action: (client) => client.time() }, - { description: 'call `request(...)`', action: (client) => client.request('get', '/channels/channel', 2) }, - { - description: 'call `batchPublish(...)`', - action: (client) => client.batchPublish({ channels: ['channel'], messages: { data: { foo: 'bar' } } }), - }, - { - description: 'call `batchPresence(...)`', - action: (client) => client.batchPresence(['channel']), - }, - { - description: 'call `auth.revokeTokens(...)`', - getAdditionalClientOptions: () => { - const testApp = getTestApp(); - return { key: testApp.keys[4].keyStr /* this key has revocableTokens enabled */ }; + describe('Rest', () => { + const restScenarios = [ + { + description: 'use push admin functionality', + action: (client) => client.push.admin.publish({ clientId: 'foo' }, { data: { bar: 'baz' } }), }, - action: (client) => client.auth.revokeTokens([{ type: 'clientId', value: 'foo' }]), - }, - { - description: 'call channel’s `history()`', - action: (client) => client.channels.get('channel').history(), - }, - { - description: 'call channel’s `presence.history()`', - additionalRealtimeModules: { RealtimePresence }, - action: (client) => client.channels.get('channel').presence.history(), - }, - { - description: 'call channel’s `status()`', - action: (client) => client.channels.get('channel').status(), - }, - ]; - - describe('BaseRest without explicit Rest', () => { - for (const scenario of restScenarios) { - it(`allows you to ${scenario.description}`, async () => { - const client = new BaseRest(ablyClientOptions(scenario.getAdditionalClientOptions?.()), { FetchRequest }); + { description: 'call `time()`', action: (client) => client.time() }, + { description: 'call `request(...)`', action: (client) => client.request('get', '/channels/channel', 2) }, + { + description: 'call `batchPublish(...)`', + action: (client) => client.batchPublish({ channels: ['channel'], messages: { data: { foo: 'bar' } } }), + }, + { + description: 'call `batchPresence(...)`', + action: (client) => client.batchPresence(['channel']), + }, + { + description: 'call `auth.revokeTokens(...)`', + getAdditionalClientOptions: () => { + const testApp = getTestApp(); + return { key: testApp.keys[4].keyStr /* this key has revocableTokens enabled */ }; + }, + action: (client) => client.auth.revokeTokens([{ type: 'clientId', value: 'foo' }]), + }, + { + description: 'call channel’s `history()`', + action: (client) => client.channels.get('channel').history(), + }, + { + description: 'call channel’s `presence.history()`', + additionalRealtimeModules: { RealtimePresence }, + action: (client) => client.channels.get('channel').presence.history(), + }, + { + description: 'call channel’s `status()`', + action: (client) => client.channels.get('channel').status(), + }, + ]; - let thrownError = null; - try { - await scenario.action(client); - } catch (error) { - thrownError = error; - } + describe('BaseRest without explicit Rest', () => { + for (const scenario of restScenarios) { + it(`allows you to ${scenario.description}`, async () => { + const client = new BaseRest(ablyClientOptions(scenario.getAdditionalClientOptions?.()), { FetchRequest }); - expect(thrownError).to.be.null; - }); - } - }); + let thrownError = null; + try { + await scenario.action(client); + } catch (error) { + thrownError = error; + } - describe('BaseRealtime with Rest', () => { - for (const scenario of restScenarios) { - it(`allows you to ${scenario.description}`, async () => { - const client = new BaseRealtime(ablyClientOptions(scenario.getAdditionalClientOptions?.()), { - WebSocketTransport, - FetchRequest, - Rest, - ...scenario.additionalRealtimeModules, + expect(thrownError).to.be.null; }); + } + }); - let thrownError = null; - try { - await scenario.action(client); - } catch (error) { - thrownError = error; - } + describe('BaseRealtime with Rest', () => { + for (const scenario of restScenarios) { + it(`allows you to ${scenario.description}`, async () => { + const client = new BaseRealtime(ablyClientOptions(scenario.getAdditionalClientOptions?.()), { + WebSocketTransport, + FetchRequest, + Rest, + ...scenario.additionalRealtimeModules, + }); - expect(thrownError).to.be.null; - }); - } - }); + let thrownError = null; + try { + await scenario.action(client); + } catch (error) { + thrownError = error; + } - describe('BaseRealtime without Rest', () => { - it('still allows publishing and subscribing', async () => { - const client = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, FetchRequest }); + expect(thrownError).to.be.null; + }); + } + }); - const channel = client.channels.get('channel'); - await channel.attach(); + describe('BaseRealtime without Rest', () => { + it('still allows publishing and subscribing', async () => { + const client = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, FetchRequest }); - const recievedMessagePromise = new Promise((resolve) => { - channel.subscribe((message) => { - resolve(message); + const channel = client.channels.get('channel'); + await channel.attach(); + + const recievedMessagePromise = new Promise((resolve) => { + channel.subscribe((message) => { + resolve(message); + }); }); + + await channel.publish({ data: { foo: 'bar' } }); + + const receivedMessage = await recievedMessagePromise; + expect(receivedMessage.data).to.eql({ foo: 'bar' }); }); - await channel.publish({ data: { foo: 'bar' } }); + for (const scenario of restScenarios) { + it(`throws an error when attempting to ${scenario.description}`, async () => { + const client = new BaseRealtime(ablyClientOptions(scenario.getAdditionalClientOptions?.()), { + WebSocketTransport, + FetchRequest, + ...scenario.additionalRealtimeModules, + }); - const receivedMessage = await recievedMessagePromise; - expect(receivedMessage.data).to.eql({ foo: 'bar' }); - }); + let thrownError = null; + try { + await scenario.action(client); + } catch (error) { + thrownError = error; + } - for (const scenario of restScenarios) { - it(`throws an error when attempting to ${scenario.description}`, async () => { - const client = new BaseRealtime(ablyClientOptions(scenario.getAdditionalClientOptions?.()), { - WebSocketTransport, - FetchRequest, - ...scenario.additionalRealtimeModules, + expect(thrownError).not.to.be.null; + expect(thrownError.message).to.equal('Rest module not provided'); }); + } + }); + }); + + describe('Crypto standalone functions', () => { + it('generateRandomKey', async () => { + const key = await generateRandomKey(); + expect(key).to.be.an('ArrayBuffer'); + }); + + it('getDefaultCryptoParams', async () => { + const key = await generateRandomKey(); + const params = getDefaultCryptoParams({ key }); + expect(params).to.be.an('object'); + }); + }); + + describe('Message standalone functions', () => { + async function testDecodesMessageData(functionUnderTest) { + const testData = await loadTestData(testResourcesPath + 'crypto-data-128.json'); + + const item = testData.items[1]; + const decoded = await functionUnderTest(item.encoded); + + expect(decoded.data).to.be.an('ArrayBuffer'); + } + + describe('decodeMessage', () => { + it('decodes a message’s data', async () => { + testDecodesMessageData(decodeMessage); + }); + + it('throws an error when given channel options with a cipher', async () => { + const testData = await loadTestData(testResourcesPath + 'crypto-data-128.json'); + const key = BufferUtils.base64Decode(testData.key); + const iv = BufferUtils.base64Decode(testData.iv); let thrownError = null; try { - await scenario.action(client); + await decodeMessage(testData.items[0].encrypted, { cipher: { key, iv } }); } catch (error) { thrownError = error; } expect(thrownError).not.to.be.null; - expect(thrownError.message).to.equal('Rest module not provided'); + expect(thrownError.message).to.equal('Crypto module not provided'); }); - } - }); - }); - - describe('Crypto standalone functions', () => { - it('generateRandomKey', async () => { - const key = await generateRandomKey(); - expect(key).to.be.an('ArrayBuffer'); - }); + }); - it('getDefaultCryptoParams', async () => { - const key = await generateRandomKey(); - const params = getDefaultCryptoParams({ key }); - expect(params).to.be.an('object'); - }); - }); + describe('decodeEncryptedMessage', async () => { + it('decodes a message’s data', async () => { + testDecodesMessageData(decodeEncryptedMessage); + }); - describe('Message standalone functions', () => { - async function testDecodesMessageData(functionUnderTest) { - const testData = await loadTestData(testResourcesPath + 'crypto-data-128.json'); + it('decrypts a message', async () => { + const testData = await loadTestData(testResourcesPath + 'crypto-data-128.json'); - const item = testData.items[1]; - const decoded = await functionUnderTest(item.encoded); + const key = BufferUtils.base64Decode(testData.key); + const iv = BufferUtils.base64Decode(testData.iv); - expect(decoded.data).to.be.an('ArrayBuffer'); - } + for (const item of testData.items) { + const [decodedFromEncoded, decodedFromEncrypted] = await Promise.all([ + decodeMessage(item.encoded), + decodeEncryptedMessage(item.encrypted, { cipher: { key, iv } }), + ]); - describe('decodeMessage', () => { - it('decodes a message’s data', async () => { - testDecodesMessageData(decodeMessage); + testMessageEquality(decodedFromEncoded, decodedFromEncrypted); + } + }); }); - it('throws an error when given channel options with a cipher', async () => { + async function testDecodesMessagesData(functionUnderTest) { const testData = await loadTestData(testResourcesPath + 'crypto-data-128.json'); - const key = BufferUtils.base64Decode(testData.key); - const iv = BufferUtils.base64Decode(testData.iv); - - let thrownError = null; - try { - await decodeMessage(testData.items[0].encrypted, { cipher: { key, iv } }); - } catch (error) { - thrownError = error; - } - expect(thrownError).not.to.be.null; - expect(thrownError.message).to.equal('Crypto module not provided'); - }); - }); + const items = [testData.items[1], testData.items[3]]; + const decoded = await functionUnderTest(items.map((item) => item.encoded)); - describe('decodeEncryptedMessage', async () => { - it('decodes a message’s data', async () => { - testDecodesMessageData(decodeEncryptedMessage); - }); + expect(decoded[0].data).to.be.an('ArrayBuffer'); + expect(decoded[1].data).to.be.an('array'); + } - it('decrypts a message', async () => { - const testData = await loadTestData(testResourcesPath + 'crypto-data-128.json'); + describe('decodeMessages', () => { + it('decodes messages’ data', async () => { + testDecodesMessagesData(decodeMessages); + }); - const key = BufferUtils.base64Decode(testData.key); - const iv = BufferUtils.base64Decode(testData.iv); + it('throws an error when given channel options with a cipher', async () => { + const testData = await loadTestData(testResourcesPath + 'crypto-data-128.json'); + const key = BufferUtils.base64Decode(testData.key); + const iv = BufferUtils.base64Decode(testData.iv); - for (const item of testData.items) { - const [decodedFromEncoded, decodedFromEncrypted] = await Promise.all([ - decodeMessage(item.encoded), - decodeEncryptedMessage(item.encrypted, { cipher: { key, iv } }), - ]); + let thrownError = null; + try { + await decodeMessages( + testData.items.map((item) => item.encrypted), + { cipher: { key, iv } } + ); + } catch (error) { + thrownError = error; + } - testMessageEquality(decodedFromEncoded, decodedFromEncrypted); - } + expect(thrownError).not.to.be.null; + expect(thrownError.message).to.equal('Crypto module not provided'); + }); }); - }); - async function testDecodesMessagesData(functionUnderTest) { - const testData = await loadTestData(testResourcesPath + 'crypto-data-128.json'); + describe('decodeEncryptedMessages', () => { + it('decodes messages’ data', async () => { + testDecodesMessagesData(decodeEncryptedMessages); + }); - const items = [testData.items[1], testData.items[3]]; - const decoded = await functionUnderTest(items.map((item) => item.encoded)); + it('decrypts messages', async () => { + const testData = await loadTestData(testResourcesPath + 'crypto-data-128.json'); - expect(decoded[0].data).to.be.an('ArrayBuffer'); - expect(decoded[1].data).to.be.an('array'); - } + const key = BufferUtils.base64Decode(testData.key); + const iv = BufferUtils.base64Decode(testData.iv); - describe('decodeMessages', () => { - it('decodes messages’ data', async () => { - testDecodesMessagesData(decodeMessages); + const [decodedFromEncoded, decodedFromEncrypted] = await Promise.all([ + decodeMessages(testData.items.map((item) => item.encoded)), + decodeEncryptedMessages( + testData.items.map((item) => item.encrypted), + { cipher: { key, iv } } + ), + ]); + + for (let i = 0; i < decodedFromEncoded.length; i++) { + testMessageEquality(decodedFromEncoded[i], decodedFromEncrypted[i]); + } + }); }); + }); - it('throws an error when given channel options with a cipher', async () => { - const testData = await loadTestData(testResourcesPath + 'crypto-data-128.json'); - const key = BufferUtils.base64Decode(testData.key); - const iv = BufferUtils.base64Decode(testData.iv); - - let thrownError = null; - try { - await decodeMessages( - testData.items.map((item) => item.encrypted), - { cipher: { key, iv } } - ); - } catch (error) { - thrownError = error; + describe('Crypto', () => { + describe('without Crypto', () => { + async function testThrowsAnErrorWhenGivenChannelOptionsWithACipher(clientClassConfig) { + const client = new clientClassConfig.clientClass(ablyClientOptions(), { + ...clientClassConfig.additionalModules, + FetchRequest, + }); + const key = await generateRandomKey(); + expect(() => client.channels.get('channel', { cipher: { key } })).to.throw('Crypto module not provided'); } - expect(thrownError).not.to.be.null; - expect(thrownError.message).to.equal('Crypto module not provided'); + for (const clientClassConfig of [ + { clientClass: BaseRest }, + { clientClass: BaseRealtime, additionalModules: { WebSocketTransport } }, + ]) { + describe(clientClassConfig.clientClass.name, () => { + it('throws an error when given channel options with a cipher', async () => { + await testThrowsAnErrorWhenGivenChannelOptionsWithACipher(clientClassConfig); + }); + }); + } }); - }); - describe('decodeEncryptedMessages', () => { - it('decodes messages’ data', async () => { - testDecodesMessagesData(decodeEncryptedMessages); - }); + describe('with Crypto', () => { + async function testIsAbleToPublishEncryptedMessages(clientClassConfig) { + const clientOptions = ablyClientOptions(); - it('decrypts messages', async () => { - const testData = await loadTestData(testResourcesPath + 'crypto-data-128.json'); + const key = await generateRandomKey(); - const key = BufferUtils.base64Decode(testData.key); - const iv = BufferUtils.base64Decode(testData.iv); + // Publish the message on a channel configured to use encryption, and receive it on one not configured to use encryption - const [decodedFromEncoded, decodedFromEncrypted] = await Promise.all([ - decodeMessages(testData.items.map((item) => item.encoded)), - decodeEncryptedMessages( - testData.items.map((item) => item.encrypted), - { cipher: { key, iv } } - ), - ]); + const rxClient = new BaseRealtime(clientOptions, { WebSocketTransport, FetchRequest }); + const rxChannel = rxClient.channels.get('channel'); + await rxChannel.attach(); - for (let i = 0; i < decodedFromEncoded.length; i++) { - testMessageEquality(decodedFromEncoded[i], decodedFromEncrypted[i]); - } - }); - }); - }); + const rxMessagePromise = new Promise((resolve, _) => rxChannel.subscribe((message) => resolve(message))); - describe('Crypto', () => { - describe('without Crypto', () => { - async function testThrowsAnErrorWhenGivenChannelOptionsWithACipher(clientClassConfig) { - const client = new clientClassConfig.clientClass(ablyClientOptions(), { - ...clientClassConfig.additionalModules, - FetchRequest, - }); - const key = await generateRandomKey(); - expect(() => client.channels.get('channel', { cipher: { key } })).to.throw('Crypto module not provided'); - } + const encryptionChannelOptions = { cipher: { key } }; - for (const clientClassConfig of [ - { clientClass: BaseRest }, - { clientClass: BaseRealtime, additionalModules: { WebSocketTransport } }, - ]) { - describe(clientClassConfig.clientClass.name, () => { - it('throws an error when given channel options with a cipher', async () => { - await testThrowsAnErrorWhenGivenChannelOptionsWithACipher(clientClassConfig); + const txMessage = { name: 'message', data: 'data' }; + const txClient = new clientClassConfig.clientClass(clientOptions, { + ...clientClassConfig.additionalModules, + FetchRequest, + Crypto, }); - }); - } - }); + const txChannel = txClient.channels.get('channel', encryptionChannelOptions); + await txChannel.publish(txMessage); - describe('with Crypto', () => { - async function testIsAbleToPublishEncryptedMessages(clientClassConfig) { - const clientOptions = ablyClientOptions(); - - const key = await generateRandomKey(); + const rxMessage = await rxMessagePromise; - // Publish the message on a channel configured to use encryption, and receive it on one not configured to use encryption + // Verify that the message was published with encryption + expect(rxMessage.encoding).to.equal('utf-8/cipher+aes-256-cbc'); - const rxClient = new BaseRealtime(clientOptions, { WebSocketTransport, FetchRequest }); - const rxChannel = rxClient.channels.get('channel'); - await rxChannel.attach(); - - const rxMessagePromise = new Promise((resolve, _) => rxChannel.subscribe((message) => resolve(message))); + // Verify that the message was correctly encrypted + const rxMessageDecrypted = await decodeEncryptedMessage(rxMessage, encryptionChannelOptions); + testMessageEquality(rxMessageDecrypted, txMessage); + } - const encryptionChannelOptions = { cipher: { key } }; + for (const clientClassConfig of [ + { clientClass: BaseRest }, + { clientClass: BaseRealtime, additionalModules: { WebSocketTransport } }, + ]) { + describe(clientClassConfig.clientClass.name, () => { + it('is able to publish encrypted messages', async () => { + await testIsAbleToPublishEncryptedMessages(clientClassConfig); + }); + }); + } + }); + }); - const txMessage = { name: 'message', data: 'data' }; - const txClient = new clientClassConfig.clientClass(clientOptions, { - ...clientClassConfig.additionalModules, - FetchRequest, - Crypto, + describe('MsgPack', () => { + async function testRestUsesContentType(rest, expectedContentType) { + const channelName = 'channel'; + const channel = rest.channels.get(channelName); + const contentTypeUsedForPublishPromise = new Promise((resolve, reject) => { + rest.http.do = (method, path, headers, body, params, callback) => { + if (!(method == 'post' && path == `/channels/${channelName}/messages`)) { + return; + } + resolve(headers['content-type']); + callback(null); + }; }); - const txChannel = txClient.channels.get('channel', encryptionChannelOptions); - await txChannel.publish(txMessage); - const rxMessage = await rxMessagePromise; + await channel.publish('message', 'body'); - // Verify that the message was published with encryption - expect(rxMessage.encoding).to.equal('utf-8/cipher+aes-256-cbc'); - - // Verify that the message was correctly encrypted - const rxMessageDecrypted = await decodeEncryptedMessage(rxMessage, encryptionChannelOptions); - testMessageEquality(rxMessageDecrypted, txMessage); + const contentTypeUsedForPublish = await contentTypeUsedForPublishPromise; + expect(contentTypeUsedForPublish).to.equal(expectedContentType); } - for (const clientClassConfig of [ - { clientClass: BaseRest }, - { clientClass: BaseRealtime, additionalModules: { WebSocketTransport } }, - ]) { - describe(clientClassConfig.clientClass.name, () => { - it('is able to publish encrypted messages', async () => { - await testIsAbleToPublishEncryptedMessages(clientClassConfig); - }); + async function testRealtimeUsesFormat(realtime, expectedFormat) { + const formatUsedForConnectionPromise = new Promise((resolve, reject) => { + realtime.connection.connectionManager.connectImpl = (transportParams) => { + resolve(transportParams.format); + }; }); - } - }); - }); + realtime.connect(); - describe('MsgPack', () => { - async function testRestUsesContentType(rest, expectedContentType) { - const channelName = 'channel'; - const channel = rest.channels.get(channelName); - const contentTypeUsedForPublishPromise = new Promise((resolve, reject) => { - rest.http.do = (method, path, headers, body, params, callback) => { - if (!(method == 'post' && path == `/channels/${channelName}/messages`)) { - return; - } - resolve(headers['content-type']); - callback(null); - }; - }); - - await channel.publish('message', 'body'); - - const contentTypeUsedForPublish = await contentTypeUsedForPublishPromise; - expect(contentTypeUsedForPublish).to.equal(expectedContentType); - } + const formatUsedForConnection = await formatUsedForConnectionPromise; + expect(formatUsedForConnection).to.equal(expectedFormat); + } - async function testRealtimeUsesFormat(realtime, expectedFormat) { - const formatUsedForConnectionPromise = new Promise((resolve, reject) => { - realtime.connection.connectionManager.connectImpl = (transportParams) => { - resolve(transportParams.format); - }; - }); - realtime.connect(); - - const formatUsedForConnection = await formatUsedForConnectionPromise; - expect(formatUsedForConnection).to.equal(expectedFormat); - } - - // TODO once https://github.com/ably/ably-js/issues/1424 is fixed, this should also test the case where the useBinaryProtocol option is not specified - describe('with useBinaryProtocol client option', () => { - describe('without MsgPack', () => { - describe('BaseRest', () => { - it('uses JSON', async () => { - const client = new BaseRest(ablyClientOptions({ useBinaryProtocol: true }), { FetchRequest }); - await testRestUsesContentType(client, 'application/json'); + // TODO once https://github.com/ably/ably-js/issues/1424 is fixed, this should also test the case where the useBinaryProtocol option is not specified + describe('with useBinaryProtocol client option', () => { + describe('without MsgPack', () => { + describe('BaseRest', () => { + it('uses JSON', async () => { + const client = new BaseRest(ablyClientOptions({ useBinaryProtocol: true }), { FetchRequest }); + await testRestUsesContentType(client, 'application/json'); + }); }); - }); - describe('BaseRealtime', () => { - it('uses JSON', async () => { - const client = new BaseRealtime(ablyClientOptions({ useBinaryProtocol: true, autoConnect: false }), { - WebSocketTransport, - FetchRequest, + describe('BaseRealtime', () => { + it('uses JSON', async () => { + const client = new BaseRealtime(ablyClientOptions({ useBinaryProtocol: true, autoConnect: false }), { + WebSocketTransport, + FetchRequest, + }); + await testRealtimeUsesFormat(client, 'json'); }); - await testRealtimeUsesFormat(client, 'json'); }); }); - }); - describe('with MsgPack', () => { - describe('BaseRest', () => { - it('uses MessagePack', async () => { - const client = new BaseRest(ablyClientOptions({ useBinaryProtocol: true }), { - FetchRequest, - MsgPack, + describe('with MsgPack', () => { + describe('BaseRest', () => { + it('uses MessagePack', async () => { + const client = new BaseRest(ablyClientOptions({ useBinaryProtocol: true }), { + FetchRequest, + MsgPack, + }); + await testRestUsesContentType(client, 'application/x-msgpack'); }); - await testRestUsesContentType(client, 'application/x-msgpack'); }); - }); - describe('BaseRealtime', () => { - it('uses MessagePack', async () => { - const client = new BaseRealtime(ablyClientOptions({ useBinaryProtocol: true, autoConnect: false }), { - WebSocketTransport, - FetchRequest, - MsgPack, + describe('BaseRealtime', () => { + it('uses MessagePack', async () => { + const client = new BaseRealtime(ablyClientOptions({ useBinaryProtocol: true, autoConnect: false }), { + WebSocketTransport, + FetchRequest, + MsgPack, + }); + await testRealtimeUsesFormat(client, 'msgpack'); }); - await testRealtimeUsesFormat(client, 'msgpack'); }); }); }); }); - }); - describe('RealtimePresence', () => { - describe('BaseRealtime without RealtimePresence', () => { - it('throws an error when attempting to access the `presence` property', () => { - const client = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, FetchRequest }); - const channel = client.channels.get('channel'); + describe('RealtimePresence', () => { + describe('BaseRealtime without RealtimePresence', () => { + it('throws an error when attempting to access the `presence` property', () => { + const client = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, FetchRequest }); + const channel = client.channels.get('channel'); - expect(() => channel.presence).to.throw('RealtimePresence module not provided'); - }); - - it('doesn’t break when it receives a PRESENCE ProtocolMessage', async () => { - const rxClient = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, FetchRequest }); - const rxChannel = rxClient.channels.get('channel'); + expect(() => channel.presence).to.throw('RealtimePresence module not provided'); + }); - await rxChannel.attach(); + it('doesn’t break when it receives a PRESENCE ProtocolMessage', async () => { + const rxClient = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, FetchRequest }); + const rxChannel = rxClient.channels.get('channel'); - const receivedMessagePromise = new Promise((resolve) => rxChannel.subscribe(resolve)); + await rxChannel.attach(); - const txClient = new BaseRealtime(ablyClientOptions({ clientId: randomString() }), { - WebSocketTransport, - FetchRequest, - RealtimePresence, - }); - const txChannel = txClient.channels.get('channel'); + const receivedMessagePromise = new Promise((resolve) => rxChannel.subscribe(resolve)); - await txChannel.publish('message', 'body'); - await txChannel.presence.enter(); + const txClient = new BaseRealtime(ablyClientOptions({ clientId: randomString() }), { + WebSocketTransport, + FetchRequest, + RealtimePresence, + }); + const txChannel = txClient.channels.get('channel'); - // The idea being here that in order for receivedMessagePromise to resolve, rxClient must have first processed the PRESENCE ProtocolMessage that resulted from txChannel.presence.enter() + await txChannel.publish('message', 'body'); + await txChannel.presence.enter(); - await receivedMessagePromise; - }); - }); + // The idea being here that in order for receivedMessagePromise to resolve, rxClient must have first processed the PRESENCE ProtocolMessage that resulted from txChannel.presence.enter() - describe('BaseRealtime with RealtimePresence', () => { - it('offers realtime presence functionality', async () => { - const rxChannel = new BaseRealtime(ablyClientOptions(), { - WebSocketTransport, - FetchRequest, - RealtimePresence, - }).channels.get('channel'); - const txClientId = randomString(); - const txChannel = new BaseRealtime(ablyClientOptions({ clientId: txClientId }), { - WebSocketTransport, - FetchRequest, - RealtimePresence, - }).channels.get('channel'); - - let resolveRxPresenceMessagePromise; - const rxPresenceMessagePromise = new Promise((resolve, reject) => { - resolveRxPresenceMessagePromise = resolve; + await receivedMessagePromise; }); - await rxChannel.presence.subscribe('enter', resolveRxPresenceMessagePromise); - await txChannel.presence.enter(); - - const rxPresenceMessage = await rxPresenceMessagePromise; - expect(rxPresenceMessage.clientId).to.equal(txClientId); }); - }); - }); - describe('PresenceMessage standalone functions', () => { - describe('decodePresenceMessage', () => { - it('decodes a presence message’s data', async () => { - const buffer = BufferUtils.utf8Encode('foo'); - const encodedMessage = { data: BufferUtils.base64Encode(buffer), encoding: 'base64' }; + describe('BaseRealtime with RealtimePresence', () => { + it('offers realtime presence functionality', async () => { + const rxChannel = new BaseRealtime(ablyClientOptions(), { + WebSocketTransport, + FetchRequest, + RealtimePresence, + }).channels.get('channel'); + const txClientId = randomString(); + const txChannel = new BaseRealtime(ablyClientOptions({ clientId: txClientId }), { + WebSocketTransport, + FetchRequest, + RealtimePresence, + }).channels.get('channel'); - const decodedMessage = await decodePresenceMessage(encodedMessage); + let resolveRxPresenceMessagePromise; + const rxPresenceMessagePromise = new Promise((resolve, reject) => { + resolveRxPresenceMessagePromise = resolve; + }); + await rxChannel.presence.subscribe('enter', resolveRxPresenceMessagePromise); + await txChannel.presence.enter(); - expect(BufferUtils.areBuffersEqual(decodedMessage.data, buffer)).to.be.true; - expect(decodedMessage.encoding).to.be.null; + const rxPresenceMessage = await rxPresenceMessagePromise; + expect(rxPresenceMessage.clientId).to.equal(txClientId); + }); }); }); - describe('decodeMessages', () => { - it('decodes presence messages’ data', async () => { - const buffers = ['foo', 'bar'].map((data) => BufferUtils.utf8Encode(data)); - const encodedMessages = buffers.map((buffer) => ({ - data: BufferUtils.base64Encode(buffer), - encoding: 'base64', - })); - - const decodedMessages = await decodePresenceMessages(encodedMessages); + describe('PresenceMessage standalone functions', () => { + describe('decodePresenceMessage', () => { + it('decodes a presence message’s data', async () => { + const buffer = BufferUtils.utf8Encode('foo'); + const encodedMessage = { data: BufferUtils.base64Encode(buffer), encoding: 'base64' }; - for (let i = 0; i < decodedMessages.length; i++) { - const decodedMessage = decodedMessages[i]; + const decodedMessage = await decodePresenceMessage(encodedMessage); - expect(BufferUtils.areBuffersEqual(decodedMessage.data, buffers[i])).to.be.true; + expect(BufferUtils.areBuffersEqual(decodedMessage.data, buffer)).to.be.true; expect(decodedMessage.encoding).to.be.null; - } + }); }); - }); - describe('constructPresenceMessage', () => { - it('creates a PresenceMessage instance', async () => { - const extras = { foo: 'bar' }; - const presenceMessage = constructPresenceMessage({ extras }); + describe('decodeMessages', () => { + it('decodes presence messages’ data', async () => { + const buffers = ['foo', 'bar'].map((data) => BufferUtils.utf8Encode(data)); + const encodedMessages = buffers.map((buffer) => ({ + data: BufferUtils.base64Encode(buffer), + encoding: 'base64', + })); - expect(presenceMessage.constructor.name).to.contain('PresenceMessage'); - expect(presenceMessage.extras).to.equal(extras); - }); - }); - }); + const decodedMessages = await decodePresenceMessages(encodedMessages); - describe('Transports', () => { - describe('BaseRealtime', () => { - describe('without a transport module', () => { - it('throws an error due to absence of a transport module', () => { - expect(() => new BaseRealtime(ablyClientOptions(), { FetchRequest })).to.throw( - 'no requested transports available' - ); + for (let i = 0; i < decodedMessages.length; i++) { + const decodedMessage = decodedMessages[i]; + + expect(BufferUtils.areBuffersEqual(decodedMessage.data, buffers[i])).to.be.true; + expect(decodedMessage.encoding).to.be.null; + } }); }); - for (const scenario of [ - { moduleMapKey: 'WebSocketTransport', transportModule: WebSocketTransport, transportName: 'web_socket' }, - { moduleMapKey: 'XHRPolling', transportModule: XHRPolling, transportName: 'xhr_polling' }, - { moduleMapKey: 'XHRStreaming', transportModule: XHRStreaming, transportName: 'xhr_streaming' }, - ]) { - describe(`with the ${scenario.moduleMapKey} module`, () => { - it(`is able to use the ${scenario.transportName} transport`, async () => { - const realtime = new BaseRealtime( - ablyClientOptions({ autoConnect: false, transports: [scenario.transportName] }), - { - FetchRequest, - [scenario.moduleMapKey]: scenario.transportModule, - } - ); - - let firstTransportCandidate; - const connectionManager = realtime.connection.connectionManager; - const originalTryATransport = connectionManager.tryATransport; - realtime.connection.connectionManager.tryATransport = (transportParams, candidate, callback) => { - if (!firstTransportCandidate) { - firstTransportCandidate = candidate; - } - originalTryATransport.bind(connectionManager)(transportParams, candidate, callback); - }; + describe('constructPresenceMessage', () => { + it('creates a PresenceMessage instance', async () => { + const extras = { foo: 'bar' }; + const presenceMessage = constructPresenceMessage({ extras }); - realtime.connect(); + expect(presenceMessage.constructor.name).to.contain('PresenceMessage'); + expect(presenceMessage.extras).to.equal(extras); + }); + }); + }); - await realtime.connection.once('connected'); - expect(firstTransportCandidate).to.equal(scenario.transportName); + describe('Transports', () => { + describe('BaseRealtime', () => { + describe('without a transport module', () => { + it('throws an error due to absence of a transport module', () => { + expect(() => new BaseRealtime(ablyClientOptions(), { FetchRequest })).to.throw( + 'no requested transports available' + ); }); }); - } + + for (const scenario of [ + { moduleMapKey: 'WebSocketTransport', transportModule: WebSocketTransport, transportName: 'web_socket' }, + { moduleMapKey: 'XHRPolling', transportModule: XHRPolling, transportName: 'xhr_polling' }, + { moduleMapKey: 'XHRStreaming', transportModule: XHRStreaming, transportName: 'xhr_streaming' }, + ]) { + describe(`with the ${scenario.moduleMapKey} module`, () => { + it(`is able to use the ${scenario.transportName} transport`, async () => { + const realtime = new BaseRealtime( + ablyClientOptions({ autoConnect: false, transports: [scenario.transportName] }), + { + FetchRequest, + [scenario.moduleMapKey]: scenario.transportModule, + } + ); + + let firstTransportCandidate; + const connectionManager = realtime.connection.connectionManager; + const originalTryATransport = connectionManager.tryATransport; + realtime.connection.connectionManager.tryATransport = (transportParams, candidate, callback) => { + if (!firstTransportCandidate) { + firstTransportCandidate = candidate; + } + originalTryATransport.bind(connectionManager)(transportParams, candidate, callback); + }; + + realtime.connect(); + + await realtime.connection.once('connected'); + expect(firstTransportCandidate).to.equal(scenario.transportName); + }); + }); + } + }); }); - }); - describe('HTTP request implementations', () => { - describe('with multiple HTTP request implementations', () => { - it('prefers XHR', async () => { - let usedXHR = false; + describe('HTTP request implementations', () => { + describe('with multiple HTTP request implementations', () => { + it('prefers XHR', async () => { + let usedXHR = false; - const XHRRequestSpy = class XHRRequestSpy extends XHRRequest { - static createRequest(...args) { - usedXHR = true; - return super.createRequest(...args); - } - }; + const XHRRequestSpy = class XHRRequestSpy extends XHRRequest { + static createRequest(...args) { + usedXHR = true; + return super.createRequest(...args); + } + }; - const rest = new BaseRest(ablyClientOptions(), { FetchRequest, XHRRequest: XHRRequestSpy }); - await rest.time(); + const rest = new BaseRest(ablyClientOptions(), { FetchRequest, XHRRequest: XHRRequestSpy }); + await rest.time(); - expect(usedXHR).to.be.true; + expect(usedXHR).to.be.true; + }); }); }); - }); - describe('MessageInteractions', () => { - describe('BaseRealtime', () => { - describe('without MessageInteractions', () => { - it('is able to subscribe to and unsubscribe from channel events, as long as a MessageFilter isn’t passed', async () => { - const realtime = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, FetchRequest }); - const channel = realtime.channels.get('channel'); - await channel.attach(); + describe('MessageInteractions', () => { + describe('BaseRealtime', () => { + describe('without MessageInteractions', () => { + it('is able to subscribe to and unsubscribe from channel events, as long as a MessageFilter isn’t passed', async () => { + const realtime = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, FetchRequest }); + const channel = realtime.channels.get('channel'); + await channel.attach(); - const subscribeReceivedMessagePromise = new Promise((resolve) => channel.subscribe(resolve)); + const subscribeReceivedMessagePromise = new Promise((resolve) => channel.subscribe(resolve)); - await channel.publish('message', 'body'); + await channel.publish('message', 'body'); - const subscribeReceivedMessage = await subscribeReceivedMessagePromise; - expect(subscribeReceivedMessage.data).to.equal('body'); - }); + const subscribeReceivedMessage = await subscribeReceivedMessagePromise; + expect(subscribeReceivedMessage.data).to.equal('body'); + }); - it('throws an error when attempting to subscribe to channel events using a MessageFilter', async () => { - const realtime = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, FetchRequest }); - const channel = realtime.channels.get('channel'); + it('throws an error when attempting to subscribe to channel events using a MessageFilter', async () => { + const realtime = new BaseRealtime(ablyClientOptions(), { WebSocketTransport, FetchRequest }); + const channel = realtime.channels.get('channel'); - let thrownError = null; - try { - await channel.subscribe({ clientId: 'someClientId' }, () => {}); - } catch (error) { - thrownError = error; - } + let thrownError = null; + try { + await channel.subscribe({ clientId: 'someClientId' }, () => {}); + } catch (error) { + thrownError = error; + } - expect(thrownError).not.to.be.null; - expect(thrownError.message).to.equal('MessageInteractions module not provided'); + expect(thrownError).not.to.be.null; + expect(thrownError.message).to.equal('MessageInteractions module not provided'); + }); }); - }); - describe('with MessageInteractions', () => { - it('can take a MessageFilter argument when subscribing to and unsubscribing from channel events', async () => { - const realtime = new BaseRealtime(ablyClientOptions(), { - WebSocketTransport, - FetchRequest, - MessageInteractions, - }); - const channel = realtime.channels.get('channel'); + describe('with MessageInteractions', () => { + it('can take a MessageFilter argument when subscribing to and unsubscribing from channel events', async () => { + const realtime = new BaseRealtime(ablyClientOptions(), { + WebSocketTransport, + FetchRequest, + MessageInteractions, + }); + const channel = realtime.channels.get('channel'); - await channel.attach(); + await channel.attach(); - // Test `subscribe` with a filter: send two messages with different clientIds, and check that unfiltered subscription receives both messages but clientId-filtered subscription only receives the matching one. - const messageFilter = { clientId: 'someClientId' }; // note that `unsubscribe` compares filter by reference, I found that a bit surprising + // Test `subscribe` with a filter: send two messages with different clientIds, and check that unfiltered subscription receives both messages but clientId-filtered subscription only receives the matching one. + const messageFilter = { clientId: 'someClientId' }; // note that `unsubscribe` compares filter by reference, I found that a bit surprising - const filteredSubscriptionReceivedMessages = []; - channel.subscribe(messageFilter, (message) => { - filteredSubscriptionReceivedMessages.push(message); - }); + const filteredSubscriptionReceivedMessages = []; + channel.subscribe(messageFilter, (message) => { + filteredSubscriptionReceivedMessages.push(message); + }); - const unfilteredSubscriptionReceivedFirstTwoMessagesPromise = new Promise((resolve) => { - const receivedMessages = []; - channel.subscribe(function listener(message) { - receivedMessages.push(message); - if (receivedMessages.length === 2) { - channel.unsubscribe(listener); - resolve(); - } + const unfilteredSubscriptionReceivedFirstTwoMessagesPromise = new Promise((resolve) => { + const receivedMessages = []; + channel.subscribe(function listener(message) { + receivedMessages.push(message); + if (receivedMessages.length === 2) { + channel.unsubscribe(listener); + resolve(); + } + }); }); - }); - await channel.publish(await decodeMessage({ clientId: 'someClientId' })); - await channel.publish(await decodeMessage({ clientId: 'someOtherClientId' })); - await unfilteredSubscriptionReceivedFirstTwoMessagesPromise; + await channel.publish(await decodeMessage({ clientId: 'someClientId' })); + await channel.publish(await decodeMessage({ clientId: 'someOtherClientId' })); + await unfilteredSubscriptionReceivedFirstTwoMessagesPromise; - expect(filteredSubscriptionReceivedMessages.length).to.equal(1); - expect(filteredSubscriptionReceivedMessages[0].clientId).to.equal('someClientId'); + expect(filteredSubscriptionReceivedMessages.length).to.equal(1); + expect(filteredSubscriptionReceivedMessages[0].clientId).to.equal('someClientId'); - // Test `unsubscribe` with a filter: call `unsubscribe` with the clientId filter, publish a message matching the filter, check that only the unfiltered listener recieves it - channel.unsubscribe(messageFilter); + // Test `unsubscribe` with a filter: call `unsubscribe` with the clientId filter, publish a message matching the filter, check that only the unfiltered listener recieves it + channel.unsubscribe(messageFilter); - const unfilteredSubscriptionReceivedNextMessagePromise = new Promise((resolve) => { - channel.subscribe(function listener() { - channel.unsubscribe(listener); - resolve(); + const unfilteredSubscriptionReceivedNextMessagePromise = new Promise((resolve) => { + channel.subscribe(function listener() { + channel.unsubscribe(listener); + resolve(); + }); }); - }); - await channel.publish(await decodeMessage({ clientId: 'someClientId' })); - await unfilteredSubscriptionReceivedNextMessagePromise; + await channel.publish(await decodeMessage({ clientId: 'someClientId' })); + await unfilteredSubscriptionReceivedNextMessagePromise; - expect(filteredSubscriptionReceivedMessages.length).to./* (still) */ equal(1); + expect(filteredSubscriptionReceivedMessages.length).to./* (still) */ equal(1); + }); }); }); }); }); -}); +} + +// This function is called by browser_setup.js once `require` is available +window.registerAblyModulesTests = async () => { + return new Promise((resolve) => { + require(['shared_helper'], (helper) => { + registerAblyModulesTests(helper); + resolve(); + }); + }); +}; diff --git a/test/common/modules/shared_helper.js b/test/common/modules/shared_helper.js index 465ef5a74f..6a6cfd43c5 100644 --- a/test/common/modules/shared_helper.js +++ b/test/common/modules/shared_helper.js @@ -250,7 +250,7 @@ define([ expect(json1 === json2, 'JSON data contents mismatch.').to.be.ok; } - var exports = { + return (module.exports = { setupApp: testAppModule.setup, tearDownApp: testAppModule.tearDown, createStats: testAppModule.createStatsFixtureData, @@ -284,11 +284,5 @@ define([ whenPromiseSettles: whenPromiseSettles, randomString: randomString, testMessageEquality: testMessageEquality, - }; - - if (typeof window !== 'undefined') { - window.ablyHelpers = exports; - } - - return (module.exports = exports); + }); }); diff --git a/test/support/browser_setup.js b/test/support/browser_setup.js index af7fc6b693..b668c84a45 100644 --- a/test/support/browser_setup.js +++ b/test/support/browser_setup.js @@ -66,7 +66,15 @@ require([(baseUrl + '/test/common/globals/named_dependencies.js').replace('//', // dynamically load all test files deps: allTestFiles, - // we have to kickoff mocha - callback: () => mocha.run(), + callback: () => { + // (For some reason things don’t work if you return a Promise from this callback, hence the nested async function) + (async () => { + // Let modules.test.js register its tests before we run the test suite + await registerAblyModulesTests(); + + // we have to kickoff mocha + mocha.run(); + })(); + }, }); });