UNPKG

teletype-client-modified

Version:

I have modified the library developed by Atom to make it compatible with non-browser facility. I am using it only for experimentation purposes. I will remove this package post my experiment and raise a PR at Atom to finally merge this package so that this

1,060 lines (896 loc) 57.4 kB
require('./setup') const assert = require('assert') const deepEqual = require('deep-equal') const FakeBufferDelegate = require('./helpers/fake-buffer-delegate') const FakeEditorDelegate = require('./helpers/fake-editor-delegate') const FakePortalDelegate = require('./helpers/fake-portal-delegate') const condition = require('./helpers/condition') const timeout = require('./helpers/timeout') const {TeletypeClient, FollowState, Errors} = require('..') const RestGateway = require('../lib/rest-gateway') const PusherPubSubGateway = require('../lib/pusher-pub-sub-gateway') const {startTestServer} = require('@atom/teletype-server') let testEpoch = 0 suite('Client Integration', () => { let server, portals suiteSetup(async () => { const params = {databaseURL: process.env.TEST_DATABASE_URL} // Uncomment and provide credentials to test against Pusher. // params.pusherCredentials = { // appId: '123', // key: '123', // secret: '123' // } server = await startTestServer(params) }) suiteTeardown(() => { return server.stop() }) setup(() => { portals = [] return server.reset() }) teardown(async () => { for (const portal of portals) { portal.dispose() portal.peerPool.disconnect() } testEpoch++ }) test('sharing a portal and performing basic collaboration with a guest', async () => { const host = await buildClient() const guest = await buildClient() const hostPortal = await host.createPortal() let hostSetTextCallCount = 0 const hostBufferDelegate = new FakeBufferDelegate('hello world', {didSetText: () => hostSetTextCallCount++}) const hostBufferProxy = await hostPortal.createBufferProxy({uri: 'uri-1', text: hostBufferDelegate.text}) hostBufferProxy.setDelegate(hostBufferDelegate) hostBufferProxy.setTextInRange(...hostBufferDelegate.insert({row: 0, column: 11}, '!')) assert.equal(hostSetTextCallCount, 0) const hostEditorProxy = await hostPortal.createEditorProxy({ bufferProxy: hostBufferProxy, selections: { 1: {range: {start: {row: 0, column: 0}, end: {row: 0, column: 5}}}, 2: {range: {start: {row: 0, column: 6}, end: {row: 0, column: 11}}} } }) const hostEditorDelegate = new FakeEditorDelegate() hostEditorProxy.setDelegate(hostEditorDelegate) assert(!hostEditorDelegate.getSelectionsForSiteId(1)) hostPortal.activateEditorProxy(hostEditorProxy) const guestPortalDelegate = new FakePortalDelegate() const guestPortal = await guest.joinPortal(hostPortal.id) await guestPortal.setDelegate(guestPortalDelegate) const guestEditorProxy = guestPortalDelegate.getTetherEditorProxy() const guestEditorDelegate = new FakeEditorDelegate() guestEditorProxy.setDelegate(guestEditorDelegate) const guestBufferProxy = guestEditorProxy.bufferProxy const guestBufferDelegate = new FakeBufferDelegate() guestBufferProxy.setDelegate(guestBufferDelegate) assert.deepEqual(guestEditorDelegate.getSelectionsForSiteId(1), { 1: { range: {start: {row: 0, column: 0}, end: {row: 0, column: 5}}, exclusive: false, reversed: false, tailed: true }, 2: { range: {start: {row: 0, column: 6}, end: {row: 0, column: 11}}, exclusive: false, reversed: false, tailed: true } }) assert.equal(guestBufferProxy.uri, 'uri-1') assert.equal(guestBufferDelegate.getText(), 'hello world!') hostBufferProxy.setTextInRange(...hostBufferDelegate.insert({row: 0, column: 5}, ' cruel')) guestBufferProxy.setTextInRange(...guestBufferDelegate.delete({row: 0, column: 0}, {row: 0, column: 5})) guestBufferProxy.setTextInRange(...guestBufferDelegate.insert({row: 0, column: 0}, 'goodbye')) await condition(() => hostBufferDelegate.text === 'goodbye cruel world!') await condition(() => guestBufferDelegate.text === 'goodbye cruel world!') hostEditorProxy.updateSelections({ 1: { range: {start: {row: 0, column: 6}, end: {row: 0, column: 11}} }, 2: null }) guestEditorProxy.updateSelections({ 1: { range: {start: {row: 0, column: 2}, end: {row: 0, column: 4}} }, 2: { range: {start: {row: 0, column: 6}, end: {row: 0, column: 8}} } }) const expectedGuestSelectionsOnHost = { 1: { range: {start: {row: 0, column: 2}, end: {row: 0, column: 4}}, exclusive: false, reversed: false, tailed: true }, 2: { range: {start: {row: 0, column: 6}, end: {row: 0, column: 8}}, exclusive: false, reversed: false, tailed: true } } const expectedHostSelectionsOnGuest = { 1: { range: {start: {row: 0, column: 6}, end: {row: 0, column: 11}}, exclusive: false, reversed: false, tailed: true } } await condition(() => { return ( deepEqual(guestEditorDelegate.getSelectionsForSiteId(1), expectedHostSelectionsOnGuest) && deepEqual(hostEditorDelegate.getSelectionsForSiteId(2), expectedGuestSelectionsOnHost) ) }) }) test('adding and removing editor proxies', async () => { const host = await buildClient() const hostPortal = await host.createPortal() const guest1 = await buildClient() const guest1Portal = await guest1.joinPortal(hostPortal.id) const guest1PortalDelegate = new FakePortalDelegate() guest1Portal.setDelegate(guest1PortalDelegate) assert.equal(guest1Portal.getEditorProxiesMetadata().length, 0) const hostBufferProxy1 = await hostPortal.createBufferProxy({uri: 'buffer-a', text: ''}) const hostEditorProxy1 = await hostPortal.createEditorProxy({bufferProxy: hostBufferProxy1}) await condition(() => guest1Portal.getEditorProxiesMetadata().length === 1) assert.equal(guest1PortalDelegate.editorProxiesChangeEventCount, 1) const hostBufferProxy2 = await hostPortal.createBufferProxy({uri: 'buffer-b', text: ''}) const hostEditorProxy2 = await hostPortal.createEditorProxy({bufferProxy: hostBufferProxy2}) hostPortal.activateEditorProxy(hostEditorProxy2) await condition(() => guest1Portal.getEditorProxiesMetadata().length === 2) assert.equal(guest1PortalDelegate.editorProxiesChangeEventCount, 2) const guest2 = await buildClient() const guest2Portal = await guest2.joinPortal(hostPortal.id) const guest2PortalDelegate = new FakePortalDelegate() guest2Portal.setDelegate(guest2PortalDelegate) assert.equal(guest2Portal.getEditorProxiesMetadata().length, 2) assert.equal(guest2PortalDelegate.editorProxiesChangeEventCount, 0) hostEditorProxy1.dispose() await condition(() => ( guest1Portal.getEditorProxiesMetadata().length === 1 && guest2Portal.getEditorProxiesMetadata().length === 1 )) assert.equal(guest1PortalDelegate.editorProxiesChangeEventCount, 3) assert.equal(guest2PortalDelegate.editorProxiesChangeEventCount, 1) hostEditorProxy2.dispose() hostPortal.activateEditorProxy(null) await condition(() => ( guest1Portal.getEditorProxiesMetadata().length === 0 && guest2Portal.getEditorProxiesMetadata().length === 0 )) assert.equal(guest1PortalDelegate.editorProxiesChangeEventCount, 4) assert.equal(guest2PortalDelegate.editorProxiesChangeEventCount, 2) const hostBufferProxy3 = await hostPortal.createBufferProxy({uri: 'buffer-c', text: ''}) const hostEditorProxy3 = await hostPortal.createEditorProxy({bufferProxy: hostBufferProxy3}) await condition(() => ( guest1Portal.getEditorProxiesMetadata().length === 1 && guest2Portal.getEditorProxiesMetadata().length === 1 )) assert.equal(guest1PortalDelegate.editorProxiesChangeEventCount, 5) assert.equal(guest2PortalDelegate.editorProxiesChangeEventCount, 3) const guest3 = await buildClient() const guest3Portal = await guest3.joinPortal(hostPortal.id) assert.equal(guest3Portal.getEditorProxiesMetadata().length, 1) }) suite('tethering to other participants', () => { test('extending, retracting, and disconnecting when collaborating across multiple editors', async () => { const host = await buildClient() const guest1 = await buildClient() const hostPortal = await host.createPortal() const hostPortalDelegate = new FakePortalDelegate() hostPortal.setDelegate(hostPortalDelegate) const hostBufferProxy1 = await hostPortal.createBufferProxy({uri: 'buffer-1', text: ('x'.repeat(30) + '\n').repeat(30)}) const hostEditorProxy1 = await hostPortal.createEditorProxy({bufferProxy: hostBufferProxy1}) hostPortal.activateEditorProxy(hostEditorProxy1) const guest1PortalDelegate = new FakePortalDelegate() const guest1Portal = await guest1.joinPortal(hostPortal.id) await guest1Portal.setDelegate(guest1PortalDelegate) assert.equal(guest1Portal.resolveFollowState(), FollowState.RETRACTED) // Extend the tether when the follower performs an action const guest1EditorProxy1 = guest1PortalDelegate.getTetherEditorProxy() guest1EditorProxy1.bufferProxy.setTextInRange({row: 1, column: 1}, {row: 1, column: 1}, 'X') await condition(() => guest1Portal.resolveFollowState() === FollowState.EXTENDED) // Retract an extended tether if leader moves to a different editor and // the tether disconnect window has elapsed since the last action taken by // by the follower await timeout(guest1Portal.tetherDisconnectWindow) const hostBufferProxy2 = await hostPortal.createBufferProxy({uri: 'buffer-2', text: ('y'.repeat(30) + '\n').repeat(30)}) const hostEditorProxy2 = await hostPortal.createEditorProxy({bufferProxy: hostBufferProxy2}) hostPortal.activateEditorProxy(hostEditorProxy2) await condition(() => ( guest1Portal.resolveFollowState() === FollowState.RETRACTED && guest1PortalDelegate.getTetherBufferProxyURI() === 'buffer-2' )) // Disconnect an extended tether if leader moves to a different editor // within the disconnect window. const guest1EditorProxy2 = guest1PortalDelegate.getTetherEditorProxy() guest1EditorProxy2.updateSelections({ 1: {range: range([2, 1], [2, 1])} }, {initialUpdate: true}) guest1EditorProxy2.bufferProxy.setTextInRange({row: 1, column: 1}, {row: 1, column: 1}, 'X') await condition(() => guest1Portal.resolveFollowState() === FollowState.EXTENDED) hostPortal.activateEditorProxy(hostEditorProxy1) await condition(() => ( guest1Portal.resolveFollowState() === FollowState.DISCONNECTED && guest1PortalDelegate.getTetherBufferProxyURI() === 'buffer-2' )) // New guests can start following existing participants. const guest2 = await buildClient() const guest2PortalDelegate = new FakePortalDelegate() const guest2Portal = await guest2.joinPortal(hostPortal.id) await guest2Portal.setDelegate(guest2PortalDelegate) guest2Portal.follow(guest1Portal.siteId) assert.equal(guest2PortalDelegate.getTetherBufferProxyURI(), 'buffer-2') assert.deepEqual(guest2PortalDelegate.getTetherPosition(), {row: 2, column: 1}) // When you start to follow a participant, you go to their position in // their active editor hostPortal.follow(guest1Portal.siteId) guest1EditorProxy2.updateSelections({ 2: {range: {start: {row: 4, column: 4}, end: {row: 4, column: 4}}} }) await condition(() => ( hostPortalDelegate.getTetherBufferProxyURI() === 'buffer-2' && deepEqual(hostPortalDelegate.getTetherPosition(), {row: 4, column: 4}) )) }) test('extending, retracting, and disconnecting when collaborating in a single editor', async () => { const host = await buildClient() const guest = await buildClient() const hostPortal = await host.createPortal() const hostBufferProxy = await hostPortal.createBufferProxy({uri: 'buffer-a', text: ('x'.repeat(30) + '\n').repeat(30)}) const hostEditorProxy = await hostPortal.createEditorProxy({bufferProxy: hostBufferProxy, selections: { 1: {range: {start: {row: 5, column: 5}, end: {row: 6, column: 6}}}, 2: {range: {start: {row: 8, column: 8}, end: {row: 9, column: 9}}} } }) hostPortal.activateEditorProxy(hostEditorProxy) const guestPortalDelegate = new FakePortalDelegate() const guestPortal = await guest.joinPortal(hostPortal.id) await guestPortal.setDelegate(guestPortalDelegate) // Guests immediately jump to host's cursor position after joining. assert.equal(guestPortal.resolveFollowState(), FollowState.RETRACTED) assert.deepEqual(guestPortalDelegate.getTetherPosition(), {row: 9, column: 9}) const guestEditorProxy = guestPortalDelegate.getTetherEditorProxy() const guestEditorDelegate = new FakeEditorDelegate() guestEditorDelegate.updateViewport(5, 15) guestEditorProxy.setDelegate(guestEditorDelegate) // Guests continue to follow host's cursor as it moves. hostEditorProxy.updateSelections({ 2: {range: {start: {row: 10, column: 10}, end: {row: 11, column: 11}}, reversed: true} }) await condition(() => deepEqual(guestPortalDelegate.getTetherPosition(), {row: 10, column: 10})) // Extend the tether when the guest explicitly moves their cursor guestEditorProxy.updateSelections({ 2: {range: {start: {row: 9, column: 9}, end: {row: 9, column: 9}}} }) assert.equal(guestPortal.resolveFollowState(), FollowState.EXTENDED) // When the tether is extended, the follower's cursor does not follow // the tether's position as long as it remains visible in the viewport assert(!guestEditorDelegate.isScrollNeededToViewPosition({row: 11, column: 11})) hostEditorProxy.updateSelections({ 2: {range: {start: {row: 11, column: 11}, end: {row: 11, column: 11}}} }) await condition(() => deepEqual( guestEditorDelegate.getSelectionsForSiteId(1)[2].range, {start: {row: 11, column: 11}, end: {row: 11, column: 11}} )) await condition(() => deepEqual(guestPortalDelegate.getTetherPosition(), {row: 10, column: 10})) // Moves out of the viewport will retract the tether so long as the // tether disconnect window has elapsed since the last cursor movement // by the follower await timeout(guestPortal.tetherDisconnectWindow) assert(guestEditorDelegate.isScrollNeededToViewPosition({row: 20, column: 20})) hostEditorProxy.updateSelections({ 2: {range: {start: {row: 20, column: 20}, end: {row: 20, column: 20}}} }) await condition(() => guestPortalDelegate.getTetherState() === FollowState.RETRACTED) assert.deepEqual(guestPortalDelegate.getTetherPosition(), {row: 20, column: 20}) hostEditorProxy.updateSelections({ 2: {range: {start: {row: 21, column: 21}, end: {row: 21, column: 21}}} }) await condition(() => deepEqual(guestPortalDelegate.getTetherPosition(), {row: 21, column: 21})) // Update delegate's tether position when text changes on remote sites. hostBufferProxy.setTextInRange({row: 21, column: 0}, {row: 21, column: 0}, 'X') await condition(() => deepEqual(guestPortalDelegate.getTetherPosition(), {row: 21, column: 22})) // Extend the tether when the follower starts typing. guestEditorProxy.bufferProxy.setTextInRange({row: 21, column: 22}, {row: 21, column: 22}, 'ABCD') assert.equal(guestPortal.resolveFollowState(), FollowState.EXTENDED) // Disconnects the tether if leader moves offscreen within the disconnect window. hostEditorProxy.updateSelections({ 2: {range: {start: {row: 0, column: 0}, end: {row: 0, column: 0}}} }) await condition(() => guestPortalDelegate.getTetherState() === FollowState.DISCONNECTED) assert.deepEqual( guestEditorDelegate.getSelectionsForSiteId(1)[2].range, {start: {row: 0, column: 0}, end: {row: 0, column: 0}} ) // Can reconnect tether after disconnecting guestPortal.follow(1) assert.equal(guestPortalDelegate.getTetherState(), FollowState.RETRACTED) assert.deepEqual(guestPortalDelegate.getTetherPosition(), {row: 0, column: 0}) hostEditorProxy.updateSelections({ 2: {range: {start: {row: 1, column: 1}, end: {row: 1, column: 1}}} }) await condition(() => deepEqual(guestPortalDelegate.getTetherPosition(), {row: 1, column: 1})) // Disconnects the tether if it moves off screen within the disconnect // window of the follower moving their cursor guestEditorProxy.updateSelections({ 2: {range: {start: {row: 22, column: 22}, end: {row: 22, column: 22}}} }) hostEditorProxy.updateSelections({ 2: {range: {start: {row: 0, column: 0}, end: {row: 0, column: 0}}} }) await condition(() => guestPortalDelegate.getTetherState() === FollowState.DISCONNECTED) assert.deepEqual( guestEditorDelegate.getSelectionsForSiteId(1)[2].range, {start: {row: 0, column: 0}, end: {row: 0, column: 0}} ) // Can reconnect tether after disconnecting guestPortal.follow(1) assert.equal(guestPortalDelegate.getTetherState(), FollowState.RETRACTED) assert.deepEqual(guestPortalDelegate.getTetherPosition(), {row: 0, column: 0}) hostEditorProxy.updateSelections({ 2: {range: {start: {row: 1, column: 1}, end: {row: 1, column: 1}}} }) await condition(() => deepEqual(guestPortalDelegate.getTetherPosition(), {row: 1, column: 1})) // Disconnect tether when we scroll out of view. In real life, the // viewport would have changed when we reconnected the tether, but in // this test we're only concerned with the tether position being out of // view when we indicate a scroll. assert(guestEditorDelegate.isScrollNeededToViewPosition({row: 1, column: 1})) guestEditorProxy.didScroll() assert.equal(guestPortalDelegate.getTetherState(), FollowState.DISCONNECTED) hostEditorProxy.updateSelections({ 2: {range: {start: {row: 0, column: 0}, end: {row: 0, column: 0}}} }) await condition(() => deepEqual( guestEditorDelegate.getSelectionsForSiteId(1)[2].range, {start: {row: 0, column: 0}, end: {row: 0, column: 0}} )) assert.notDeepEqual(guestPortalDelegate.getTetherPosition(), {row: 0, column: 0}) }) test('showing and hiding selections', async () => { const host = await buildClient() const hostPortal = await host.createPortal() const hostBufferProxy1 = await hostPortal.createBufferProxy({uri: 'buffer-a', text: ('x'.repeat(30) + '\n').repeat(30)}) const hostEditorProxy1 = await hostPortal.createEditorProxy({bufferProxy: hostBufferProxy1, selections: { 1: {range: {start: {row: 5, column: 5}, end: {row: 6, column: 6}}} }}) const hostEditor1Delegate = new FakeEditorDelegate() hostEditorProxy1.setDelegate(hostEditor1Delegate) hostPortal.activateEditorProxy(hostEditorProxy1) const guest = await buildClient() const guestPortalDelegate = new FakePortalDelegate() const guestPortal = await guest.joinPortal(hostPortal.id) await guestPortal.setDelegate(guestPortalDelegate) const guestEditor1Proxy = guestPortalDelegate.getTetherEditorProxy() const guestEditor1Delegate = new FakeEditorDelegate() guestEditor1Proxy.setDelegate(guestEditor1Delegate) guestEditor1Proxy.updateSelections({ 1: {range: {start: {row: 0, column: 0}, end: {row: 0, column: 0}}} }, {initialUpdate: true}) assert.deepEqual(guestPortalDelegate.getTetherPosition(), {row: 6, column: 6}) // Cursors are not rendered locally or remotely for followers with // retracted tethers await condition(() => deepEqual(hostEditor1Delegate.getSelectionsForSiteId(2), {})) // When the tether is extended, selections appear guestEditor1Proxy.updateSelections({ 1: {range: {start: {row: 1, column: 1}, end: {row: 1, column: 1}}} }) await condition(() => { const selection = hostEditor1Delegate.getSelectionsForSiteId(2)[1] return selection && deepEqual(selection.range, {start: {row: 1, column: 1}, end: {row: 1, column: 1}}) }) // Selections disappear when the tether is retracted again await timeout(guestPortal.tetherDisconnectWindow) guestEditor1Delegate.updateViewport(0, 6) hostEditorProxy1.updateSelections({ 1: {range: {start: {row: 12, column: 12}, end: {row: 12, column: 12}}} }) await condition(() => deepEqual(guestPortal.resolveLeaderPosition(), {row: 12, column: 12})) await condition(() => deepEqual(hostEditor1Delegate.getSelectionsForSiteId(2), {})) // Disconnecting the tether shows the selections again guestEditor1Delegate.updateViewport(6, 15) guestEditor1Proxy.updateSelections({ 1: {range: {start: {row: 13, column: 13}, end: {row: 13, column: 13}}} }) hostEditorProxy1.updateSelections({ 1: {range: {start: {row: 0, column: 0}, end: {row: 0, column: 0}}} }) await condition(() => { const selection = hostEditor1Delegate.getSelectionsForSiteId(2)[1] return selection && deepEqual(selection.range, {start: {row: 13, column: 13}, end: {row: 13, column: 13}}) }) // Activating an editor proxy hides selection on the previously active // editor proxy and shows them on the new one. const hostBufferProxy2 = await hostPortal.createBufferProxy({uri: 'buffer-b', text: ('x'.repeat(30) + '\n').repeat(30)}) const hostEditorProxy2 = await hostPortal.createEditorProxy({bufferProxy: hostBufferProxy2, selections: { 1: {range: {start: {row: 1, column: 3}, end: {row: 2, column: 4}}} }}) const hostEditor2Delegate = new FakeEditorDelegate() hostEditorProxy2.setDelegate(hostEditor2Delegate) hostPortal.activateEditorProxy(hostEditorProxy2) await condition(() => guestPortal.getEditorProxiesMetadata().length === 2) const guestEditor2ProxyMetadata = guestPortal.getEditorProxiesMetadata().find((e) => e.bufferProxyURI === 'buffer-b') const guestEditor2Proxy = await guestPortal.findOrFetchEditorProxy(guestEditor2ProxyMetadata.id) const guestEditor2Delegate = new FakeEditorDelegate() guestEditor2Proxy.setDelegate(guestEditor2Delegate) guestEditor2Proxy.updateSelections({ 1: {range: {start: {row: 0, column: 0}, end: {row: 0, column: 0}}} }, {initialUpdate: true}) await condition(() => ( deepEqual(guestEditor1Delegate.getSelectionsForSiteId(1), {}) && !deepEqual(guestEditor2Delegate.getSelectionsForSiteId(1), {}) )) hostPortal.activateEditorProxy(hostEditorProxy1) await condition(() => ( !deepEqual(guestEditor1Delegate.getSelectionsForSiteId(1), {}) && deepEqual(guestEditor2Delegate.getSelectionsForSiteId(1), {}) )) // Retracting the tether when the leader is on a different editor proxy hides follower selections. assert.notDeepEqual(hostEditor1Delegate.getSelectionsForSiteId(2), {}) assert.deepEqual(hostEditor2Delegate.getSelectionsForSiteId(2), {}) guestPortal.follow(1) await condition(() => ( deepEqual(hostEditor1Delegate.getSelectionsForSiteId(2), {}) && deepEqual(hostEditor2Delegate.getSelectionsForSiteId(2), {}) )) }) test('transitive tethering (without cycles)', async () => { const host = await buildClient() const guest1 = await buildClient() const guest2 = await buildClient() const hostPortal = await host.createPortal() const hostBufferProxy1 = await hostPortal.createBufferProxy({uri: 'buffer-a', text: ('x'.repeat(30) + '\n').repeat(30)}) const hostEditorProxy1 = await hostPortal.createEditorProxy({ bufferProxy: hostBufferProxy1, selections: {1: {range: {start: {row: 5, column: 5}, end: {row: 5, column: 5}}}} }) const hostEditor1Delegate = new FakeEditorDelegate() hostEditorProxy1.setDelegate(hostEditor1Delegate) hostPortal.activateEditorProxy(hostEditorProxy1) const guest1PortalDelegate = new FakePortalDelegate() const guest1Portal = await guest1.joinPortal(hostPortal.id) await guest1Portal.setDelegate(guest1PortalDelegate) const guest1Editor1Proxy = guest1PortalDelegate.getTetherEditorProxy() const guest1Editor1Delegate = new FakeEditorDelegate() guest1Editor1Delegate.updateViewport(5, 15) guest1Editor1Proxy.setDelegate(guest1Editor1Delegate) const guest2PortalDelegate = new FakePortalDelegate() const guest2Portal = await guest2.joinPortal(hostPortal.id) await guest2Portal.setDelegate(guest2PortalDelegate) const guest2Editor1Proxy = guest2PortalDelegate.getTetherEditorProxy() const guest2Editor1Delegate = new FakeEditorDelegate() guest2Editor1Delegate.updateViewport(5, 15) guest2Editor1Proxy.setDelegate(guest2Editor1Delegate) // Guest1 follows the host, and Guest2 follows Guest1. This has the effect // of making Guest2 follow the host. guest1Portal.follow(hostPortal.siteId) guest2Portal.follow(guest1Portal.siteId) hostEditorProxy1.updateSelections({ 1: {range: {start: {row: 12, column: 12}, end: {row: 12, column: 12}}} }) await condition(() => ( deepEqual(guest1PortalDelegate.getTetherPosition(), {row: 12, column: 12}) && deepEqual(guest2PortalDelegate.getTetherPosition(), {row: 12, column: 12}) && deepEqual(hostEditor1Delegate.getSelectionsForSiteId(2), {}) && deepEqual(hostEditor1Delegate.getSelectionsForSiteId(3), {}) )) // When tether is extended on Guest1, Guest2 momentarily stops following the host. guest1Editor1Proxy.updateSelections({ 1: {range: {start: {row: 1, column: 1}, end: {row: 1, column: 1}}} }) await condition(() => { const selection = hostEditor1Delegate.getSelectionsForSiteId(2)[1] return ( selection && deepEqual(selection.range, {start: {row: 1, column: 1}, end: {row: 1, column: 1}}) && deepEqual(guest2PortalDelegate.getTetherPosition(), {row: 1, column: 1}) && deepEqual(hostEditor1Delegate.getSelectionsForSiteId(3), {}) ) }) // When tether is retracted on Guest1, Guest2 goes back to following the host. await timeout(guest1Portal.tetherDisconnectWindow) guest1Editor1Delegate.updateViewport(0, 6) hostEditorProxy1.updateSelections({ 1: {range: {start: {row: 8, column: 0}, end: {row: 8, column: 0}}} }) await condition(() => ( deepEqual(guest1PortalDelegate.getTetherPosition(), {row: 8, column: 0}) && deepEqual(guest2PortalDelegate.getTetherPosition(), {row: 8, column: 0}) && deepEqual(hostEditor1Delegate.getSelectionsForSiteId(2), {}) && deepEqual(hostEditor1Delegate.getSelectionsForSiteId(3), {}) )) // Ensure transitive following works for new sites that join after // tethering has already been established. const guest3 = await buildClient() const guest3PortalDelegate = new FakePortalDelegate() const guest3Portal = await guest3.joinPortal(hostPortal.id) await guest3Portal.setDelegate(guest3PortalDelegate) const guest3EditorProxy = guest3PortalDelegate.getTetherEditorProxy() const guest3EditorDelegate = new FakeEditorDelegate() guest3EditorDelegate.updateViewport(5, 15) guest3EditorProxy.setDelegate(guest3EditorDelegate) guest3Portal.follow(guest2Portal.siteId) await condition(() => ( deepEqual(guest1PortalDelegate.getTetherPosition(), {row: 8, column: 0}) && deepEqual(guest2PortalDelegate.getTetherPosition(), {row: 8, column: 0}) && deepEqual(guest3PortalDelegate.getTetherPosition(), {row: 8, column: 0}) && deepEqual(hostEditor1Delegate.getSelectionsForSiteId(guest1Portal.siteId), {}) && deepEqual(hostEditor1Delegate.getSelectionsForSiteId(guest2Portal.siteId), {}) && deepEqual(hostEditor1Delegate.getSelectionsForSiteId(guest3Portal.siteId), {}) )) // Ensure transitive following works when activating other editor proxies. const hostBufferProxy2 = await hostPortal.createBufferProxy({uri: 'buffer-b', text: ('x'.repeat(30) + '\n').repeat(30)}) const hostEditorProxy2 = await hostPortal.createEditorProxy({ bufferProxy: hostBufferProxy2, selections: {1: {range: {start: {row: 5, column: 5}, end: {row: 5, column: 5}}}} }) const hostEditor2Delegate = new FakeEditorDelegate() hostEditorProxy2.setDelegate(hostEditor2Delegate) hostPortal.activateEditorProxy(hostEditorProxy2) await condition(() => ( deepEqual(guest1PortalDelegate.getTetherPosition(), {row: 5, column: 5}) && deepEqual(guest2PortalDelegate.getTetherPosition(), {row: 5, column: 5}) && deepEqual(guest3PortalDelegate.getTetherPosition(), {row: 5, column: 5}) && deepEqual(hostEditor1Delegate.getSelectionsForSiteId(guest1Portal.siteId), {}) && deepEqual(hostEditor1Delegate.getSelectionsForSiteId(guest2Portal.siteId), {}) && deepEqual(hostEditor1Delegate.getSelectionsForSiteId(guest3Portal.siteId), {}) )) // Disconnecting the tether on Guest1 breaks transitivity. const guest1Editor2Proxy = guest1PortalDelegate.getTetherEditorProxy() const guest1Editor2Delegate = new FakeEditorDelegate() guest1Editor2Delegate.updateViewport(6, 15) guest1Editor2Proxy.setDelegate(guest1Editor2Delegate) guest1Editor2Proxy.updateSelections({ 1: {range: {start: {row: 13, column: 13}, end: {row: 13, column: 13}}} }) hostEditorProxy2.updateSelections({ 1: {range: {start: {row: 0, column: 0}, end: {row: 0, column: 0}}} }) await condition(() => { const selection = hostEditor2Delegate.getSelectionsForSiteId(guest1Portal.siteId)[1] return ( selection && deepEqual(selection.range, {start: {row: 13, column: 13}, end: {row: 13, column: 13}}) && deepEqual(guest2PortalDelegate.getTetherPosition(), {row: 13, column: 13}) && deepEqual(hostEditor2Delegate.getSelectionsForSiteId(3), {}) ) }) }) test('transitive tethering (with cycles)', async () => { const host = await buildClient() const guest1 = await buildClient() const guest2 = await buildClient() const hostPortal = await host.createPortal() const hostPortalDelegate = new FakePortalDelegate() hostPortal.setDelegate(hostPortalDelegate) const hostBufferProxy = await hostPortal.createBufferProxy({uri: 'buffer-a', text: ('x'.repeat(30) + '\n').repeat(30)}) const hostEditorProxy = await hostPortal.createEditorProxy({ bufferProxy: hostBufferProxy, selections: {1: {range: {start: {row: 5, column: 5}, end: {row: 5, column: 5}}}} }) const hostEditorDelegate = new FakeEditorDelegate() hostEditorProxy.setDelegate(hostEditorDelegate) hostPortal.activateEditorProxy(hostEditorProxy) const guest1PortalDelegate = new FakePortalDelegate() const guest1Portal = await guest1.joinPortal(hostPortal.id) await guest1Portal.setDelegate(guest1PortalDelegate) const guest1EditorProxy = guest1PortalDelegate.getTetherEditorProxy() const guest1EditorDelegate = new FakeEditorDelegate() guest1EditorDelegate.updateViewport(5, 15) guest1EditorProxy.setDelegate(guest1EditorDelegate) const guest2PortalDelegate = new FakePortalDelegate() const guest2Portal = await guest2.joinPortal(hostPortal.id) await guest2Portal.setDelegate(guest2PortalDelegate) const guest2EditorProxy = guest2PortalDelegate.getTetherEditorProxy() const guest2EditorDelegate = new FakeEditorDelegate() guest2EditorDelegate.updateViewport(5, 15) guest2EditorProxy.setDelegate(guest2EditorDelegate) // Disconnect guest1's tether. guest1Portal.unfollow() guest1EditorProxy.updateSelections({1: {range: range([0, 0], [0, 0])}}) await condition(() => { const guest1SelectionOnHost = hostEditorDelegate.getSelectionsForSiteId(guest1Portal.siteId)[1] const guest1SelectionOnGuest2 = guest2EditorDelegate.getSelectionsForSiteId(guest1Portal.siteId)[1] return ( guest1SelectionOnHost && deepEqual(guest1SelectionOnHost.range, range([0, 0], [0, 0])) && guest1SelectionOnGuest2 && deepEqual(guest1SelectionOnGuest2.range, range([0, 0], [0, 0])) ) }) // Form a cycle (guest1 -> guest2 -> host -> guest1) and ensure it gets // broken on the site with the lowest site id. guest1Portal.follow(guest2Portal.siteId) guest2Portal.follow(hostPortal.siteId) hostPortal.follow(guest1Portal.siteId) await condition(() => ( deepEqual(hostPortalDelegate.getTetherPosition(), {row: 5, column: 5}) && deepEqual(guest1PortalDelegate.getTetherPosition(), {row: 5, column: 5}) && deepEqual(guest2PortalDelegate.getTetherPosition(), {row: 5, column: 5}) )) assert.equal(hostPortal.getFollowedSiteId(), null) assert.equal(hostPortalDelegate.getTetherState(), FollowState.DISCONNECTED) assert.equal(guest1Portal.getFollowedSiteId(), guest2Portal.siteId) assert.equal(guest1PortalDelegate.getTetherState(), FollowState.RETRACTED) assert.equal(guest2Portal.getFollowedSiteId(), hostPortal.siteId) assert.equal(guest2PortalDelegate.getTetherState(), FollowState.RETRACTED) // The site which breaks the cycle becomes the leader. guest1EditorProxy.updateSelections({ 1: {range: {start: {row: 13, column: 13}, end: {row: 13, column: 13}}} }) guest1Portal.unfollow() await condition(() => ( deepEqual(hostPortalDelegate.getTetherPosition(), {row: 13, column: 13}) && deepEqual(guest1PortalDelegate.getTetherPosition(), {row: 13, column: 13}) && deepEqual(guest2PortalDelegate.getTetherPosition(), {row: 13, column: 13}) )) assert.equal(hostPortal.getFollowedSiteId(), guest1Portal.siteId) assert.equal(hostPortalDelegate.getTetherState(), FollowState.RETRACTED) assert.equal(guest1Portal.getFollowedSiteId(), null) assert.equal(guest1PortalDelegate.getTetherState(), FollowState.DISCONNECTED) assert.equal(guest2Portal.getFollowedSiteId(), hostPortal.siteId) assert.equal(guest2PortalDelegate.getTetherState(), FollowState.RETRACTED) }) }) test('active positions of other collaborators', async () => { const host = await buildClient() const guest1 = await buildClient() const guest2 = await buildClient() const portals = [] const hostPortal = await host.createPortal() const hostPortalDelegate = new FakePortalDelegate() hostPortal.setDelegate(hostPortalDelegate) portals.push(hostPortal) const hostBufferProxy1 = await hostPortal.createBufferProxy({uri: 'buffer-a', text: ('x'.repeat(30) + '\n').repeat(30)}) const hostEditorProxy1 = await hostPortal.createEditorProxy({ bufferProxy: hostBufferProxy1, selections: {1: {range: {start: {row: 5, column: 5}, end: {row: 5, column: 5}}}} }) hostPortal.activateEditorProxy(hostEditorProxy1) const guest1PortalDelegate = new FakePortalDelegate() const guest1Portal = await guest1.joinPortal(hostPortal.id) await guest1Portal.setDelegate(guest1PortalDelegate) portals.push(guest1Portal) const guest2PortalDelegate = new FakePortalDelegate() const guest2Portal = await guest2.joinPortal(hostPortal.id) await guest2Portal.setDelegate(guest2PortalDelegate) portals.push(guest2Portal) // Update active positions after the leader updates its selections. hostEditorProxy1.updateSelections({ 1: {range: range([5, 4], [9, 6])}, 2: {range: range([2, 7], [4, 4])} }) await assertActivePositions([ {siteId: 1, editorProxyId: hostEditorProxy1.id, position: point(4, 4), followState: FollowState.DISCONNECTED}, {siteId: 2, editorProxyId: hostEditorProxy1.id, position: point(4, 4), followState: FollowState.RETRACTED}, {siteId: 3, editorProxyId: hostEditorProxy1.id, position: point(4, 4), followState: FollowState.RETRACTED} ]) // Update active positions after a text change. hostBufferProxy1.setTextInRange(point(4, 0), point(4, 0), 'X') await assertActivePositions([ {siteId: 1, editorProxyId: hostEditorProxy1.id, position: point(4, 5), followState: FollowState.DISCONNECTED}, {siteId: 2, editorProxyId: hostEditorProxy1.id, position: point(4, 5), followState: FollowState.RETRACTED}, {siteId: 3, editorProxyId: hostEditorProxy1.id, position: point(4, 5), followState: FollowState.RETRACTED} ]) // Update active positions after the leader changes. guest1PortalDelegate.getTetherEditorProxy().updateSelections({ 1: {range: range([5, 4], [9, 6]), reversed: true} }) guest2Portal.follow(guest1Portal.siteId) await assertActivePositions([ {siteId: 1, editorProxyId: hostEditorProxy1.id, position: point(4, 5), followState: FollowState.DISCONNECTED}, {siteId: 2, editorProxyId: hostEditorProxy1.id, position: point(5, 4), followState: FollowState.EXTENDED}, {siteId: 3, editorProxyId: hostEditorProxy1.id, position: point(5, 4), followState: FollowState.RETRACTED} ]) // Update active positions after host switches to a different editor proxy. const hostBufferProxy2 = await hostPortal.createBufferProxy({uri: 'buffer-b', text: ('x'.repeat(30) + '\n').repeat(30)}) const hostEditorProxy2 = await hostPortal.createEditorProxy({ bufferProxy: hostBufferProxy2, selections: {1: {range: {start: {row: 2, column: 2}, end: {row: 2, column: 2}}}} }) hostPortal.activateEditorProxy(hostEditorProxy2) await assertActivePositions([ {siteId: 1, editorProxyId: hostEditorProxy2.id, position: point(2, 2), followState: FollowState.DISCONNECTED}, {siteId: 2, editorProxyId: hostEditorProxy1.id, position: point(5, 4), followState: FollowState.DISCONNECTED}, {siteId: 3, editorProxyId: hostEditorProxy1.id, position: point(5, 4), followState: FollowState.RETRACTED} ]) // Update active positions after a guest switches to a different editor proxy. const guest1EditorProxy2Metadata = guest1Portal.getEditorProxiesMetadata().find((e) => e.bufferProxyURI === 'buffer-b') const guest1EditorProxy2 = await guest1Portal.findOrFetchEditorProxy(guest1EditorProxy2Metadata.id) guest1Portal.activateEditorProxy(guest1EditorProxy2) guest1EditorProxy2.updateSelections({ 1: {range: range([1, 0], [1, 0])} }, {initialUpdate: true}) await assertActivePositions([ {siteId: 1, editorProxyId: hostEditorProxy2.id, position: point(2, 2), followState: FollowState.DISCONNECTED}, {siteId: 2, editorProxyId: hostEditorProxy2.id, position: point(1, 0), followState: FollowState.DISCONNECTED}, {siteId: 3, editorProxyId: hostEditorProxy2.id, position: point(1, 0), followState: FollowState.RETRACTED} ]) // Update active positions after a site disconnects. guest2Portal.dispose() await assertActivePositions([ {siteId: 1, editorProxyId: hostEditorProxy2.id, position: point(2, 2), followState: FollowState.DISCONNECTED}, {siteId: 2, editorProxyId: hostEditorProxy2.id, position: point(1, 0), followState: FollowState.DISCONNECTED} ]) // Update active positions after participant switches focus to something outside of the portal. hostPortal.activateEditorProxy(null) await assertActivePositions([ {siteId: 1, editorProxyId: null, position: null, followState: null}, {siteId: 2, editorProxyId: hostEditorProxy2.id, position: point(1, 0), followState: FollowState.DISCONNECTED} ]) function assertActivePositions (expectedPositions) { const alivePortals = portals.filter((p) => !p.disposed) return condition(() => alivePortals.every((p) => deepEqual(p.delegate.getActivePositions(), expectedPositions)) ) } }) test('disposing editor and buffer proxies', async () => { const host = await buildClient() const guest = await buildClient() const hostPortal = await host.createPortal() const hostBufferProxy = await hostPortal.createBufferProxy({uri: 'some-buffer', text: ''}) hostBufferProxy.setDelegate(new FakeBufferDelegate()) const hostEditorProxy1 = await hostPortal.createEditorProxy({bufferProxy: hostBufferProxy, selections: {}}) hostEditorProxy1.setDelegate(new FakeEditorDelegate()) const hostEditorProxy2 = await hostPortal.createEditorProxy({bufferProxy: hostBufferProxy, selections: {}}) hostEditorProxy2.setDelegate(new FakeEditorDelegate()) hostPortal.activateEditorProxy(hostEditorProxy1) const guestPortal = await guest.joinPortal(hostPortal.id) const guestPortalDelegate = new FakePortalDelegate() await guestPortal.setDelegate(guestPortalDelegate) await condition(() => guestPortalDelegate.getTetherEditorProxy() != null) const guestEditorProxy1 = guestPortalDelegate.getTetherEditorProxy() guestEditorProxy1.setDelegate(new FakeEditorDelegate()) hostPortal.activateEditorProxy(hostEditorProxy2) await condition(() => guestPortalDelegate.getTetherEditorProxy() !== guestEditorProxy1) const guestEditorProxy2 = guestPortalDelegate.getTetherEditorProxy() guestEditorProxy2.setDelegate(new FakeEditorDelegate()) assert.equal(guestEditorProxy1.bufferProxy, guestEditorProxy2.bufferProxy) const guestBufferProxy = guestEditorProxy1.bufferProxy guestBufferProxy.setDelegate(new FakeBufferDelegate()) hostEditorProxy1.dispose() assert(hostEditorProxy1.delegate.isDisposed()) await condition(() => guestEditorProxy1.delegate.isDisposed()) hostEditorProxy2.dispose() assert(hostEditorProxy2.delegate.isDisposed()) await condition(() => guestEditorProxy2.delegate.isDisposed()) assert(!hostBufferProxy.delegate.isDisposed()) hostBufferProxy.dispose() assert(hostBufferProxy.delegate.isDisposed()) await condition(() => guestBufferProxy.delegate.isDisposed()) }) test('changing buffer proxy uri', async () => { const host = await buildClient() const hostPortal = await host.createPortal() const guest = await buildClient() const guestPortal = await guest.joinPortal(hostPortal.id) const hostBuffer1Proxy = await hostPortal.createBufferProxy({uri: 'buffer1-a', text: ''}) const hostEditor1Proxy = await hostPortal.createEditorProxy({bufferProxy: hostBuffer1Proxy, selections: {}}) const hostBuffer2Proxy = await hostPortal.createBufferProxy({uri: 'buffer2-a', text: ''}) const hostEditor2Proxy = await hostPortal.createEditorProxy({bufferProxy: hostBuffer2Proxy, selections: {}}) const guestEditor1Proxy = await guestPortal.findOrFetchEditorProxy(hostEditor1Proxy.id) const guestBuffer1Proxy = guestEditor1Proxy.bufferProxy guestBuffer1Proxy.setDelegate(new FakeBufferDelegate()) assert.equal(guestBuffer1Proxy.uri, 'buffer1-a') assert.equal(guestPortal.getEditorProxyMetadata(hostEditor1Proxy.id).bufferProxyURI, 'buffer1-a') assert.equal(guestPortal.getEditorProxyMetadata(hostEditor2Proxy.id).bufferProxyURI, 'buffer2-a') hostBuffer1Proxy.setURI('buffer1-b') hostBuffer2Proxy.setURI('buffer2-b') await condition(() => guestBuffer1Proxy.delegate.uriChangeCount === 1) assert.equal(guestBuffer1Proxy.uri, 'buffer1-b') assert.equal(guestPortal.getEditorProxyMetadata(hostEditor1Proxy.id).bufferProxyURI, 'buffer1-b') assert.equal(guestPortal.getEditorProxyMetadata(hostEditor2Proxy.id).bufferProxyURI, 'buffer2-b') }) test('save requests', async () => { const host = await buildClient() const hostPortal = await host.createPortal() const hostBufferProxy = await hostPortal.createBufferProxy({uri: 'buffer-a', text: ''}) const hostBufferDelegate = new FakeBufferDelegate('') hostBufferProxy.setDelegate(hostBufferDelegate) const hostEditorProxy = await hostPortal.createEditorProxy({bufferProxy: hostBufferProxy, selections: {}}) hostPortal.activateEditorProxy(hostEditorProxy) const guest1 = await buildClient() const guest1PortalDelegate = new FakePortalDelegate() const guest1Portal = await guest1.joinPortal(hostPortal.id) guest1Portal.setDelegate(guest1PortalDelegate) const guest1EditorProxy = guest1PortalDelegate.getTetherEditorProxy() const guest1BufferProxy = guest1EditorProxy.bufferProxy const guest2 = await buildClient() const guest2PortalDelegate = new FakePortalDelegate() const guest2Portal = await guest2.joinPortal(hostPortal.id) guest2Portal.setDelegate(guest2PortalDelegate) const guest2EditorProxy = guest2PortalDelegate.getTetherEditorProxy() const guest2BufferProxy = guest2EditorProxy.bufferProxy guest1BufferProxy.requestSave() guest2BufferProxy.requestSave() await condition(() => hostBufferDelegate.saveRequestCount === 2) }) suite('leaving, closing, or losing connection to a portal', () => { let hostPortal, hostEditorDelegate let guest1Portal, guest1PortalDelegate, guest1EditorDelegate let guest2Portal, guest2PortalDelegate, guest2EditorDelegate let guest3Portal, guest3PortalDelegate, guest3EditorDelegate setup(async () => { const host = await buildClient() hostPortal = await host.createPortal() const guest1 = await buildClient() guest1PortalDelegate = new FakePortalDelegate() guest1Portal = await guest1.joinPortal(hostPortal.id) await guest1Portal.setDelegate(guest1PortalDelegate) const guest2 = await buildClient() guest2PortalDelegate = new FakePortalDelegate() guest2Portal = await guest2.joinPortal(hostPortal.id) await guest2Portal.setDelegate(guest2PortalDelegate) const guest3 = await buildClient() guest3PortalDelegate = new FakePortalDelegate() guest3Portal = await guest3.joinPortal(hostPortal.id) await guest3Portal.setDelegate(guest3PortalDelegate) const hostBufferDelegate = new FakeBufferDelegate('') const hostBufferProxy = await hostPortal.createBufferProxy({uri: 'some-buffer', text: hostBufferDelegate.text}) hostBufferProxy.setDelegate(hostBufferDelegate) const hostEditorProxy = await hostPortal.createEditorProxy({bufferProxy: hostBufferProxy, selections: {}}) hostEditorDelegate = new FakeEditorDelegate() hostEditorProxy.setDelegate(hostEditorDelegate) hostPortal.activateEditorProxy(hostEditorProxy) await condition(() => guest1PortalDelegate.getTetherEditorProxy() != null && guest2PortalDelegate.getTetherEditorProxy() != null && guest3PortalDelegate.getTetherEditorProxy() != null ) const guest1EditorProxy = guest1PortalDelegate.getTetherEditorProxy() const guest1BufferProxy = guest1EditorProxy.bufferProxy const guest1BufferDelegate = new FakeBufferDelegate() guest1BufferProxy.setDelegate(guest1BufferDelegate) guest1EditorDelegate = new FakeEditorDelegate() guest1EditorProxy.setDelegate(guest1EditorDelegate) guest1EditorProxy.updateSelections( {1: {range: {start: {row: 0, column: 0}, end: {row: 0, column: 0}}}} ) const guest2EditorProxy = guest2PortalDelegate.getTetherEditorProxy() const guest2BufferProxy = guest2EditorProxy.bufferProxy const guest2BufferDelegate = new FakeBufferDelegate() guest2BufferProxy.setDelegate(guest2BufferDelegate) guest2EditorDelegate = new FakeEditorDelegate() guest2EditorProxy.setDelegate(guest2EditorDelegate) guest2EditorProxy.updateSelections( {1: {range: {start: {row: 0, column: 0}, end: {row: 0, column: 0}}}} ) const guest3EditorProxy = guest3PortalDelegate.getTetherEditorProxy() const guest3BufferProxy = guest3EditorProxy.bufferProxy const guest3BufferDelegate = new FakeBufferDelegate() guest3BufferProxy.setDelegate(guest3BufferDelegate) guest3EditorDelegate = new FakeEditorDelegate() guest3EditorProxy.setDelegate(guest3EditorDelegate) guest3EditorProxy.updateSelections( {1: {range: {start: {row: 0, column: 0}, end: {row: 0, column: 0}}}} ) await condition(() => hostEditorDelegate.getSelectionsForSiteId(guest1Portal.siteId) != null && hostEditorDelegate.getSelectionsForSiteId(guest2Portal.siteId) != null && hostEditorDelegate.getSelectionsForSiteId(guest3Portal.siteId) != null ) }) test('guest leaving a portal', async () => { guest1Portal.dispose() await condition(() => hostEditorDelegate.getSelectionsForSiteId(guest1Portal.siteId) == null && guest2EditorDelegate.getSelectionsForSiteId(guest1Portal.siteId) == null && guest3EditorDelegate.getSelectionsForSiteId(guest1Portal.siteId) == null ) assert(!hostEditorDelegate.isDisposed()) assert(!guest2EditorDelegate.isDisposed()) assert(!guest3EditorDelegate.isDisposed()) assert(hostEditorDelegate.getSelectionsForSiteId(guest2Portal.siteId)) assert(hostEditorDelegate.getSelectionsForSiteId(guest3Portal.siteId)) }) test('host closing a portal', async () => { assert(!guest1PortalDelegate.hasHostClosedPortal() && !guest2PortalDelegate.hasHostClosedPortal() && !guest3PortalDelegate.h