diff --git a/Gruntfile.js b/Gruntfile.js index 3bd1b0c23..fdace117d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -73,7 +73,14 @@ module.exports = function (grunt) { }); }); - grunt.registerTask('build', ['checkGitSubmodules', 'webpack:all', 'build:browser', 'build:node', 'build:push']); + grunt.registerTask('build', [ + 'checkGitSubmodules', + 'webpack:all', + 'build:browser', + 'build:node', + 'build:push', + 'build:liveobjects', + ]); grunt.registerTask('all', ['build', 'requirejs']); @@ -138,9 +145,26 @@ module.exports = function (grunt) { }); }); + grunt.registerTask('build:liveobjects', function () { + var done = this.async(); + + Promise.all([ + esbuild.build(esbuildConfig.liveObjectsPluginConfig), + esbuild.build(esbuildConfig.liveObjectsPluginCdnConfig), + esbuild.build(esbuildConfig.minifiedLiveObjectsPluginCdnConfig), + ]) + .then(() => { + done(true); + }) + .catch((err) => { + done(err); + }); + }); + grunt.registerTask('test:webserver', 'Launch the Mocha test web server on http://localhost:3000/', [ 'build:browser', 'build:push', + 'build:liveobjects', 'checkGitSubmodules', 'mocha:webserver', ]); diff --git a/README.md b/README.md index 0f7c27796..78a2b81a2 100644 --- a/README.md +++ b/README.md @@ -586,6 +586,41 @@ The Push plugin is developed as part of the Ably client library, so it is availa For more information on publishing push notifcations over Ably, see the [Ably push documentation](https://ably.com/docs/push). +### Live Objects functionality + +Live Objects functionality is supported for Realtime clients via the LiveObjects plugin. In order to use Live Objects, you must pass in the plugin via client options. + +```javascript +import * as Ably from 'ably'; +import LiveObjects from 'ably/liveobjects'; + +const client = new Ably.Realtime({ + ...options, + plugins: { LiveObjects }, +}); +``` + +LiveObjects plugin also works with the [Modular variant](#modular-tree-shakable-variant) of the library. + +Alternatively, you can load the LiveObjects plugin directly in your HTML using `script` tag (in case you can't use a package manager): + +```html + +``` + +When loaded this way, the LiveObjects plugin will be available on the global object via the `AblyLiveObjectsPlugin` property, so you will need to pass it to the Ably instance as follows: + +```javascript +const client = new Ably.Realtime({ + ...options, + plugins: { LiveObjects: AblyLiveObjectsPlugin }, +}); +``` + +The LiveObjects plugin is developed as part of the Ably client library, so it is available for the same versions as the Ably client library itself. It also means that it follows the same semantic versioning rules as they were defined for [the Ably client library](#for-browsers). For example, to lock into a major or minor version of the LiveObjects plugin, you can specify a specific version number such as https://cdn.ably.com/lib/liveobjects.umd.min-2.js for all v2._ versions, or https://cdn.ably.com/lib/liveobjects.umd.min-2.4.js for all v2.4._ versions, or you can lock into a single release with https://cdn.ably.com/lib/liveobjects.umd.min-2.4.0.js. Note you can load the non-minified version by omitting `.min` from the URL such as https://cdn.ably.com/lib/liveobjects.umd-2.js. + +For more information about Live Objects product, see the [Ably Live Objects documentation](https://ably.com/docs/products/liveobjects). + ## Delta Plugin From version 1.2 this client library supports subscription to a stream of Vcdiff formatted delta messages from the Ably service. For certain applications this can bring significant data efficiency savings. diff --git a/ably.d.ts b/ably.d.ts index b8e85c6a4..18b7ee720 100644 --- a/ably.d.ts +++ b/ably.d.ts @@ -623,6 +623,11 @@ export interface CorePlugins { * A plugin which allows the client to be the target of push notifications. */ Push?: unknown; + + /** + * A plugin which allows the client to use LiveObjects functionality at {@link RealtimeChannel.liveObjects}. + */ + LiveObjects?: unknown; } /** @@ -2010,6 +2015,11 @@ export declare interface PushChannel { listSubscriptions(params?: Record): Promise>; } +/** + * Enables the LiveObjects state to be subscribed to for a channel. + */ +export declare interface LiveObjects {} + /** * Enables messages to be published and historic messages to be retrieved for a channel. */ @@ -2139,6 +2149,10 @@ export declare interface RealtimeChannel extends EventEmitter { return output; } -async function calculatePushPluginSize(): Promise { +async function calculatePluginSize(options: { path: string; description: string }): Promise { const output: Output = { tableRows: [], errors: [] }; - const pushPluginBundleInfo = getBundleInfo('./build/push.js'); + const pluginBundleInfo = getBundleInfo(options.path); const sizes = { - rawByteSize: pushPluginBundleInfo.byteSize, - gzipEncodedByteSize: (await promisify(gzip)(pushPluginBundleInfo.code)).byteLength, + rawByteSize: pluginBundleInfo.byteSize, + gzipEncodedByteSize: (await promisify(gzip)(pluginBundleInfo.code)).byteLength, }; output.tableRows.push({ - description: 'Push', + description: options.description, sizes: sizes, }); return output; } +async function calculatePushPluginSize(): Promise { + return calculatePluginSize({ path: './build/push.js', description: 'Push' }); +} + +async function calculateLiveObjectsPluginSize(): Promise { + return calculatePluginSize({ path: './build/liveobjects.js', description: 'LiveObjects' }); +} + async function calculateAndCheckMinimalUsefulRealtimeBundleSize(): Promise { const output: Output = { tableRows: [], errors: [] }; @@ -296,6 +304,15 @@ async function checkPushPluginFiles() { return checkBundleFiles(pushPluginBundleInfo, allowedFiles, 100); } +async function checkLiveObjectsPluginFiles() { + const pluginBundleInfo = getBundleInfo('./build/liveobjects.js'); + + // These are the files that are allowed to contribute >= `threshold` bytes to the LiveObjects bundle. + const allowedFiles = new Set(['src/plugins/liveobjects/index.ts']); + + return checkBundleFiles(pluginBundleInfo, allowedFiles, 100); +} + async function checkBundleFiles(bundleInfo: BundleInfo, allowedFiles: Set, thresholdBytes: number) { const exploreResult = await runSourceMapExplorer(bundleInfo); @@ -347,6 +364,7 @@ async function checkBundleFiles(bundleInfo: BundleInfo, allowedFiles: Set ({ tableRows: [...accum.tableRows, ...current.tableRows], @@ -355,6 +373,7 @@ async function checkBundleFiles(bundleInfo: BundleInfo, allowedFiles: Set { - const channel = client.channels.get('channel'); - await channel.attach(); + await helper.monitorConnectionThenCloseAndFinish(async () => { + const channel = client.channels.get('channel'); + await channel.attach(); - const recievedMessagePromise = new Promise((resolve) => { - channel.subscribe((message) => { - resolve(message); - }); + const recievedMessagePromise = new Promise((resolve) => { + channel.subscribe((message) => { + resolve(message); }); + }); - await channel.publish({ data: { foo: 'bar' } }); + await channel.publish({ data: { foo: 'bar' } }); - const receivedMessage = await recievedMessagePromise; - expect(receivedMessage.data).to.eql({ foo: 'bar' }); - }, - client, - ); + const receivedMessage = await recievedMessagePromise; + expect(receivedMessage.data).to.eql({ foo: 'bar' }); + }, client); }); /** @nospec */ @@ -463,48 +451,44 @@ function registerAblyModularTests(Helper) { const rxClient = new BaseRealtime({ ...clientOptions, plugins: { WebSocketTransport, FetchRequest } }); - await monitorConnectionThenCloseAndFinish( - helper, - async () => { - const rxChannel = rxClient.channels.get('channel'); - await rxChannel.attach(); + await helper.monitorConnectionThenCloseAndFinish(async () => { + const rxChannel = rxClient.channels.get('channel'); + await rxChannel.attach(); - const rxMessagePromise = new Promise((resolve, _) => rxChannel.subscribe((message) => resolve(message))); + const rxMessagePromise = new Promise((resolve, _) => rxChannel.subscribe((message) => resolve(message))); - const encryptionChannelOptions = { cipher: { key } }; + const encryptionChannelOptions = { cipher: { key } }; - const txMessage = { name: 'message', data: 'data' }; - const txClient = new clientClassConfig.clientClass({ - ...clientOptions, - plugins: { - ...clientClassConfig.additionalPlugins, - FetchRequest, - Crypto, - }, - }); + const txMessage = { name: 'message', data: 'data' }; + const txClient = new clientClassConfig.clientClass({ + ...clientOptions, + plugins: { + ...clientClassConfig.additionalPlugins, + FetchRequest, + Crypto, + }, + }); - await ( - clientClassConfig.isRealtime ? monitorConnectionThenCloseAndFinish : async (helper, op) => await op() - )( - helper, - async () => { - const txChannel = txClient.channels.get('channel', encryptionChannelOptions); - await txChannel.publish(txMessage); + const action = async () => { + const txChannel = txClient.channels.get('channel', encryptionChannelOptions); + await txChannel.publish(txMessage); - const rxMessage = await rxMessagePromise; + const rxMessage = await rxMessagePromise; - // Verify that the message was published with encryption - expect(rxMessage.encoding).to.equal('utf-8/cipher+aes-256-cbc'); + // 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); - helper.testMessageEquality(rxMessageDecrypted, txMessage); - }, - txClient, - ); - }, - rxClient, - ); + // Verify that the message was correctly encrypted + const rxMessageDecrypted = await decodeEncryptedMessage(rxMessage, encryptionChannelOptions); + helper.testMessageEquality(rxMessageDecrypted, txMessage); + }; + + if (clientClassConfig.isRealtime) { + await helper.monitorConnectionThenCloseAndFinish(action, txClient); + } else { + await action(); + } + }, rxClient); } for (const clientClassConfig of [ @@ -585,13 +569,9 @@ function registerAblyModularTests(Helper) { }), ); - await monitorConnectionThenCloseAndFinish( - helper, - async () => { - await testRealtimeUsesFormat(client, 'json'); - }, - client, - ); + await helper.monitorConnectionThenCloseAndFinish(async () => { + await testRealtimeUsesFormat(client, 'json'); + }, client); }); }); }); @@ -629,13 +609,9 @@ function registerAblyModularTests(Helper) { }), ); - await monitorConnectionThenCloseAndFinish( - helper, - async () => { - await testRealtimeUsesFormat(client, 'msgpack'); - }, - client, - ); + await helper.monitorConnectionThenCloseAndFinish(async () => { + await testRealtimeUsesFormat(client, 'msgpack'); + }, client); }); }); }); @@ -649,15 +625,11 @@ function registerAblyModularTests(Helper) { const helper = this.test.helper; const client = new BaseRealtime(helper.ablyClientOptions({ plugins: { WebSocketTransport, FetchRequest } })); - await monitorConnectionThenCloseAndFinish( - helper, - async () => { - const channel = client.channels.get('channel'); + await helper.monitorConnectionThenCloseAndFinish(async () => { + const channel = client.channels.get('channel'); - expect(() => channel.presence).to.throw('RealtimePresence plugin not provided'); - }, - client, - ); + expect(() => channel.presence).to.throw('RealtimePresence plugin not provided'); + }, client); }); /** @nospec */ @@ -667,43 +639,35 @@ function registerAblyModularTests(Helper) { helper.ablyClientOptions({ plugins: { WebSocketTransport, FetchRequest } }), ); - await monitorConnectionThenCloseAndFinish( - helper, - async () => { - const rxChannel = rxClient.channels.get('channel'); + await helper.monitorConnectionThenCloseAndFinish(async () => { + const rxChannel = rxClient.channels.get('channel'); - await rxChannel.attach(); + await rxChannel.attach(); - const receivedMessagePromise = new Promise((resolve) => rxChannel.subscribe(resolve)); + const receivedMessagePromise = new Promise((resolve) => rxChannel.subscribe(resolve)); - const txClient = new BaseRealtime( - this.test.helper.ablyClientOptions({ - clientId: Helper.randomString(), - plugins: { - WebSocketTransport, - FetchRequest, - RealtimePresence, - }, - }), - ); + const txClient = new BaseRealtime( + this.test.helper.ablyClientOptions({ + clientId: Helper.randomString(), + plugins: { + WebSocketTransport, + FetchRequest, + RealtimePresence, + }, + }), + ); - await monitorConnectionThenCloseAndFinish( - helper, - async () => { - const txChannel = txClient.channels.get('channel'); + await helper.monitorConnectionThenCloseAndFinish(async () => { + const txChannel = txClient.channels.get('channel'); - await txChannel.publish('message', 'body'); - await txChannel.presence.enter(); + await txChannel.publish('message', 'body'); + await txChannel.presence.enter(); - // 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() + // 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 receivedMessagePromise; - }, - txClient, - ); - }, - rxClient, - ); + await receivedMessagePromise; + }, txClient); + }, rxClient); }); }); @@ -727,41 +691,33 @@ function registerAblyModularTests(Helper) { ); const rxChannel = rxClient.channels.get('channel'); - await monitorConnectionThenCloseAndFinish( - helper, - async () => { - const txClientId = Helper.randomString(); - const txClient = new BaseRealtime( - this.test.helper.ablyClientOptions({ - clientId: txClientId, - plugins: { - WebSocketTransport, - FetchRequest, - RealtimePresence, - }, - }), - ); + await helper.monitorConnectionThenCloseAndFinish(async () => { + const txClientId = Helper.randomString(); + const txClient = new BaseRealtime( + this.test.helper.ablyClientOptions({ + clientId: txClientId, + plugins: { + WebSocketTransport, + FetchRequest, + RealtimePresence, + }, + }), + ); - await monitorConnectionThenCloseAndFinish( - helper, - async () => { - const txChannel = txClient.channels.get('channel'); + await helper.monitorConnectionThenCloseAndFinish(async () => { + const txChannel = txClient.channels.get('channel'); - let resolveRxPresenceMessagePromise; - const rxPresenceMessagePromise = new Promise((resolve, reject) => { - resolveRxPresenceMessagePromise = resolve; - }); - await rxChannel.presence.subscribe('enter', resolveRxPresenceMessagePromise); - await txChannel.presence.enter(); + let resolveRxPresenceMessagePromise; + const rxPresenceMessagePromise = new Promise((resolve, reject) => { + resolveRxPresenceMessagePromise = resolve; + }); + await rxChannel.presence.subscribe('enter', resolveRxPresenceMessagePromise); + await txChannel.presence.enter(); - const rxPresenceMessage = await rxPresenceMessagePromise; - expect(rxPresenceMessage.clientId).to.equal(txClientId); - }, - txClient, - ); - }, - rxClient, - ); + const rxPresenceMessage = await rxPresenceMessagePromise; + expect(rxPresenceMessage.clientId).to.equal(txClientId); + }, txClient); + }, rxClient); }); }); }); @@ -855,26 +811,22 @@ function registerAblyModularTests(Helper) { }), ); - await monitorConnectionThenCloseAndFinish( - helper, - async () => { - 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); - }, - realtime, - ); + await helper.monitorConnectionThenCloseAndFinish(async () => { + 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); + }, realtime); }); }); } @@ -914,21 +866,17 @@ function registerAblyModularTests(Helper) { helper.ablyClientOptions({ plugins: { WebSocketTransport, FetchRequest } }), ); - await monitorConnectionThenCloseAndFinish( - helper, - async () => { - const channel = realtime.channels.get('channel'); - await channel.attach(); + await helper.monitorConnectionThenCloseAndFinish(async () => { + 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'); - }, - realtime, - ); + const subscribeReceivedMessage = await subscribeReceivedMessagePromise; + expect(subscribeReceivedMessage.data).to.equal('body'); + }, realtime); }); /** @nospec */ @@ -938,23 +886,19 @@ function registerAblyModularTests(Helper) { helper.ablyClientOptions({ plugins: { WebSocketTransport, FetchRequest } }), ); - await monitorConnectionThenCloseAndFinish( - helper, - async () => { - const channel = realtime.channels.get('channel'); + await helper.monitorConnectionThenCloseAndFinish(async () => { + 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 plugin not provided'); - }, - realtime, - ); + expect(thrownError).not.to.be.null; + expect(thrownError.message).to.equal('MessageInteractions plugin not provided'); + }, realtime); }); }); @@ -975,56 +919,52 @@ function registerAblyModularTests(Helper) { }), ); - await monitorConnectionThenCloseAndFinish( - helper, - async () => { - const channel = realtime.channels.get('channel'); + await helper.monitorConnectionThenCloseAndFinish(async () => { + 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); - }, - realtime, - ); + expect(filteredSubscriptionReceivedMessages.length).to./* (still) */ equal(1); + }, realtime); }); }); }); diff --git a/test/common/globals/named_dependencies.js b/test/common/globals/named_dependencies.js index e06d92598..ead3217e7 100644 --- a/test/common/globals/named_dependencies.js +++ b/test/common/globals/named_dependencies.js @@ -11,6 +11,10 @@ define(function () { browser: 'build/push', node: 'build/push', }, + live_objects: { + browser: 'build/liveobjects', + node: 'build/liveobjects', + }, // test modules globals: { browser: 'test/common/globals/environment', node: 'test/common/globals/environment' }, diff --git a/test/common/modules/shared_helper.js b/test/common/modules/shared_helper.js index 4ce973bb4..38c791247 100644 --- a/test/common/modules/shared_helper.js +++ b/test/common/modules/shared_helper.js @@ -171,6 +171,14 @@ define([ return result; } + async monitorConnectionThenCloseAndFinish(action, realtime, states) { + try { + await this.monitorConnectionAsync(action, realtime, states); + } finally { + await this.closeAndFinishAsync(realtime); + } + } + monitorConnection(done, realtime, states) { (states || ['failed', 'suspended']).forEach(function (state) { realtime.connection.on(state, function () { diff --git a/test/realtime/live_objects.test.js b/test/realtime/live_objects.test.js new file mode 100644 index 000000000..e24d6a002 --- /dev/null +++ b/test/realtime/live_objects.test.js @@ -0,0 +1,52 @@ +'use strict'; + +define(['ably', 'shared_helper', 'async', 'chai', 'live_objects'], function ( + Ably, + Helper, + async, + chai, + LiveObjectsPlugin, +) { + var expect = chai.expect; + var createPM = Ably.protocolMessageFromDeserialized; + + function LiveObjectsRealtime(helper, options) { + return helper.AblyRealtime({ ...options, plugins: { LiveObjects: LiveObjectsPlugin } }); + } + + describe('realtime/live_objects', function () { + this.timeout(60 * 1000); + + before(function (done) { + const helper = Helper.forHook(this); + + helper.setupApp(function (err) { + if (err) { + done(err); + return; + } + done(); + }); + }); + + describe('Realtime without LiveObjects plugin', () => { + /** @nospec */ + it("throws an error when attempting to access the channel's `liveObjects` property", async function () { + const helper = this.test.helper; + const client = helper.AblyRealtime({ autoConnect: false }); + const channel = client.channels.get('channel'); + expect(() => channel.liveObjects).to.throw('LiveObjects plugin not provided'); + }); + }); + + describe('Realtime with LiveObjects plugin', () => { + /** @nospec */ + it("returns LiveObjects instance when accessing channel's `liveObjects` property", async function () { + const helper = this.test.helper; + const client = LiveObjectsRealtime(helper, { autoConnect: false }); + const channel = client.channels.get('channel'); + expect(channel.liveObjects.constructor.name).to.equal('LiveObjects'); + }); + }); + }); +}); diff --git a/test/support/browser_file_list.js b/test/support/browser_file_list.js index 80d5d8d8b..d49cbee9e 100644 --- a/test/support/browser_file_list.js +++ b/test/support/browser_file_list.js @@ -39,6 +39,7 @@ window.__testFiles__.files = { 'test/realtime/failure.test.js': true, 'test/realtime/history.test.js': true, 'test/realtime/init.test.js': true, + 'test/realtime/live_objects.test.js': true, 'test/realtime/message.test.js': true, 'test/realtime/presence.test.js': true, 'test/realtime/reauth.test.js': true,