@dxos/react-ui 0.1.28 → 0.1.29-next.b1a9bc6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/lib/browser/index.mjs +162 -18
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/composites/Shell/Shell.d.ts +1 -1
  5. package/dist/types/src/composites/Shell/Shell.d.ts.map +1 -1
  6. package/dist/types/src/composites/Shell/Shell.stories.d.ts +10 -0
  7. package/dist/types/src/composites/Shell/Shell.stories.d.ts.map +1 -0
  8. package/dist/types/src/composites/Shell/ShellContext.d.ts +22 -0
  9. package/dist/types/src/composites/Shell/ShellContext.d.ts.map +1 -0
  10. package/dist/types/src/composites/Shell/index.d.ts +1 -0
  11. package/dist/types/src/composites/Shell/index.d.ts.map +1 -1
  12. package/dist/types/src/panels/JoinPanel/JoinHeading.d.ts.map +1 -1
  13. package/dist/types/src/panels/SpacePanel/SpacePanel.d.ts +1 -1
  14. package/dist/types/src/panels/SpacePanel/SpacePanel.d.ts.map +1 -1
  15. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.d.ts +1 -2
  16. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.d.ts.map +1 -1
  17. package/dist/types/src/playwright/invitations-manager.d.ts +33 -0
  18. package/dist/types/src/playwright/invitations-manager.d.ts.map +1 -0
  19. package/dist/types/src/playwright/invitations.spec.d.ts +2 -0
  20. package/dist/types/src/playwright/invitations.spec.d.ts.map +1 -0
  21. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  22. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  23. package/dist/types/src/stories/Invitations.stories.d.ts +14 -0
  24. package/dist/types/src/stories/Invitations.stories.d.ts.map +1 -0
  25. package/dist/types/src/translations/locales/en-US.d.ts +5 -0
  26. package/dist/types/src/translations/locales/en-US.d.ts.map +1 -1
  27. package/package.json +14 -12
  28. package/src/components/InvitationList/InvitationList.stories.tsx +2 -2
  29. package/src/components/SpaceList/SpaceListItem.tsx +1 -1
  30. package/src/composites/JoinDialog/JoinDialog.stories.tsx +2 -2
  31. package/src/composites/Shell/Shell.stories.tsx +76 -0
  32. package/src/composites/Shell/Shell.tsx +17 -3
  33. package/src/composites/Shell/ShellContext.tsx +175 -0
  34. package/src/composites/Shell/index.ts +1 -0
  35. package/src/panels/DevicesPanel/DevicesPanel.tsx +2 -2
  36. package/src/panels/JoinPanel/JoinHeading.tsx +4 -3
  37. package/src/panels/JoinPanel/JoinPanel.tsx +1 -1
  38. package/src/panels/JoinPanel/view-states/InvitationAuthenticator.tsx +2 -2
  39. package/src/panels/JoinPanel/view-states/InvitationInput.tsx +1 -1
  40. package/src/panels/SpacePanel/SpacePanel.stories.tsx +2 -40
  41. package/src/panels/SpacePanel/SpacePanel.tsx +5 -5
  42. package/src/playwright/invitations-manager.ts +210 -0
  43. package/src/playwright/invitations.spec.ts +363 -0
  44. package/src/playwright/playwright.config.ts +7 -0
  45. package/src/stories/Invitations.stories.tsx +165 -0
  46. package/src/translations/locales/en-US.ts +6 -1
  47. package/dist/types/src/testing/ClientProvider.d.ts +0 -3
  48. package/dist/types/src/testing/ClientProvider.d.ts.map +0 -1
  49. package/dist/types/src/testing/index.d.ts +0 -2
  50. package/dist/types/src/testing/index.d.ts.map +0 -1
  51. package/src/testing/ClientProvider.tsx +0 -18
  52. package/src/testing/index.ts +0 -5
@@ -0,0 +1,210 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+ import type { Browser, ConsoleMessage, Page } from 'playwright';
7
+ import waitForExpect from 'wait-for-expect';
8
+
9
+ import { Trigger } from '@dxos/async';
10
+ import type { InvitationsOptions } from '@dxos/client';
11
+ import { ConnectionState } from '@dxos/protocols/proto/dxos/client/services';
12
+ import { setupPage } from '@dxos/test/playwright';
13
+
14
+ // TODO(wittjosiah): Factor out.
15
+ // TODO(burdon): No hard-coding of ports; reconcile all DXOS tools ports.
16
+ const storybookUrl = (storyId: string) => `http://localhost:9009/iframe.html?id=${storyId}&viewMode=story`;
17
+
18
+ export type PanelType = number | 'identity' | 'devices' | 'spaces' | 'join';
19
+
20
+ export class InvitationsManager {
21
+ page!: Page;
22
+
23
+ private _initialized = false;
24
+ private _invitationCode = new Trigger<string>();
25
+ private _authenticationCode = new Trigger<string>();
26
+
27
+ constructor(private readonly _browser: Browser) {}
28
+
29
+ async init() {
30
+ if (this._initialized) {
31
+ return;
32
+ }
33
+
34
+ const { page } = await setupPage(this._browser, {
35
+ url: storybookUrl('invitations--default'),
36
+ waitFor: (page) => page.getByTestId('invitations.identity-header').nth(0).isVisible()
37
+ });
38
+
39
+ this.page = page;
40
+ this.page.on('console', (message) => this._onConsoleMessage(message));
41
+ this._initialized = true;
42
+ }
43
+
44
+ // Getters
45
+
46
+ async getDisplayName(id: number) {
47
+ // TODO(wittjosiah): Update id.
48
+ return this._peer(id).getByTestId('identity-list-item').nth(0).textContent();
49
+ }
50
+
51
+ async getSpaceName(id: number, nth: number) {
52
+ // TODO(wittjosiah): Update id.
53
+ return this._peer(id).getByTestId('space-list-item').nth(nth).textContent();
54
+ }
55
+
56
+ authenticatorIsVisible(id: number, type: 'device' | 'space') {
57
+ // TODO(wittjosiah): Update id.
58
+ return this._peer(id)
59
+ .getByTestId(`${type === 'device' ? 'halo' : 'space'}-auth-code-input`)
60
+ .isVisible();
61
+ }
62
+
63
+ invitationFailed(id: number) {
64
+ // TODO(wittjosiah): Update id.
65
+ return this._peer(id).getByTestId('invitation-rescuer-reset').isVisible();
66
+ }
67
+
68
+ // Actions
69
+
70
+ async setConnectionState(id: number, state: ConnectionState) {
71
+ await this.page.evaluate(
72
+ ({ id, state }) => {
73
+ (window as any)[`peer${id}client`].mesh.setConnectionState(state);
74
+ },
75
+ { id, state }
76
+ );
77
+ }
78
+
79
+ async openPanel(id: number, panel: PanelType) {
80
+ const peer = this._peer(id);
81
+
82
+ if (typeof panel === 'number') {
83
+ // TODO(wittjosiah): Update id.
84
+ await peer.getByTestId('space-list-item').nth(panel).click();
85
+ }
86
+
87
+ switch (panel) {
88
+ case 'identity':
89
+ await peer.getByTestId('invitations.open-join-identity').click();
90
+ break;
91
+
92
+ case 'devices':
93
+ await peer.getByTestId('invitations.open-devices').click();
94
+ break;
95
+
96
+ case 'spaces':
97
+ await peer.getByTestId('invitations.list-spaces').click();
98
+ break;
99
+
100
+ case 'join':
101
+ await peer.getByTestId('invitations.open-join-space').click();
102
+ break;
103
+ }
104
+ }
105
+
106
+ async createIdentity(id: number) {
107
+ const createIdentity = this._peer(id).getByTestId('invitations.create-identity');
108
+ // TODO(wittjosiah): Clicking on buttons wrapped in tooltips is flaky in webkit playwright.
109
+ await createIdentity.click();
110
+ await waitForExpect(async () => {
111
+ expect(await createIdentity.isDisabled()).to.be.true;
112
+ });
113
+ }
114
+
115
+ async createSpace(id: number) {
116
+ await this._peer(id).getByTestId('invitations.create-space').click();
117
+ }
118
+
119
+ async createInvitation(id: number, type: 'device' | 'space', options?: InvitationsOptions): Promise<string> {
120
+ if (!options) {
121
+ const peer = this._peer(id);
122
+ this._invitationCode = new Trigger<string>();
123
+ await peer.getByTestId(`${type}s-panel.create-invitation`).click();
124
+ return this._invitationCode.wait();
125
+ }
126
+
127
+ this._invitationCode = new Trigger<string>();
128
+ await this.page.evaluate(
129
+ ({ id, type, options }) => {
130
+ if (type === 'device') {
131
+ (window as any)[`peer${id}client`].halo.createInvitation(options);
132
+ } else {
133
+ (window as any)[`peer${id}space`].createInvitation(options);
134
+ }
135
+ },
136
+ { id, type, options }
137
+ );
138
+ return this._invitationCode.wait();
139
+ }
140
+
141
+ async acceptInvitation(id: number, type: 'device' | 'space', invitation: string) {
142
+ const peer = this._peer(id);
143
+ // TODO(wittjosiah): Update ids.
144
+ await peer.getByTestId(type === 'device' ? 'join-identity' : 'select-identity').click();
145
+ await peer.getByTestId(`${type === 'device' ? 'halo' : 'space'}-invitation-input`).type(invitation);
146
+ await this.page.keyboard.press('Enter');
147
+ }
148
+
149
+ async cancelInvitation(id: number, type: 'device' | 'space', kind: 'host' | 'guest') {
150
+ const peer = this._peer(id);
151
+ if (kind === 'guest') {
152
+ await peer.getByTestId(`${type === 'device' ? 'halo' : 'space'}-invitation-authenticator-cancel`).click();
153
+ }
154
+ }
155
+
156
+ async invitationInputContinue(id: number, type: 'device' | 'space') {
157
+ // TODO(wittjosiah): Update ids.
158
+ await this._peer(id)
159
+ .getByTestId(`${type === 'device' ? 'halo' : 'space'}-invitation-input-continue`)
160
+ .click();
161
+ }
162
+
163
+ async getAuthenticationCode(): Promise<string> {
164
+ this._authenticationCode = new Trigger<string>();
165
+ return await this._authenticationCode.wait();
166
+ }
167
+
168
+ async authenticateInvitation(id: number, type: 'device' | 'space', authenticationCode: string) {
169
+ const peer = this._peer(id);
170
+ // TODO(wittjosiah): Update ids.
171
+ await peer.getByTestId(`${type === 'device' ? 'halo' : 'space'}-auth-code-input`).type(authenticationCode);
172
+ await peer.getByTestId(`${type === 'device' ? 'halo' : 'space'}-invitation-authenticator-next`).click();
173
+ }
174
+
175
+ async clearAuthenticationCode(id: number, type: 'device' | 'space') {
176
+ const peer = this._peer(id);
177
+ // TODO(wittjosiah): Update ids.
178
+ await peer.getByTestId(`${type === 'device' ? 'halo' : 'space'}-auth-code-input`).fill('');
179
+ await peer.getByTestId(`${type === 'device' ? 'halo' : 'space'}-auth-code-input`).focus();
180
+ }
181
+
182
+ async resetInvitation(id: number) {
183
+ const peer = this._peer(id);
184
+ // TODO(wittjosiah): Update ids.
185
+ await peer.getByTestId('invitation-rescuer-reset').click();
186
+ }
187
+
188
+ // TODO(wittjosiah): Remove.
189
+ async doneInvitation(id: number, type: 'device' | 'space') {
190
+ const peer = this._peer(id);
191
+ // TODO(wittjosiah): Update ids.
192
+ await peer.getByTestId(type === 'device' ? 'identity-added-done' : 'space-invitation-accepted-done').click();
193
+ }
194
+
195
+ // TODO(wittjosiah): Peer manager?
196
+ private _peer(id: number) {
197
+ return this.page.getByTestId(`peer-${id}`);
198
+ }
199
+
200
+ private async _onConsoleMessage(message: ConsoleMessage) {
201
+ try {
202
+ const json = JSON.parse(message.text());
203
+ if (json.invitationCode) {
204
+ this._invitationCode.wake(json.invitationCode);
205
+ } else if (json.authenticationCode) {
206
+ this._authenticationCode.wake(json.authenticationCode);
207
+ }
208
+ } catch {}
209
+ }
210
+ }
@@ -0,0 +1,363 @@
1
+ //
2
+ // Copyright 2021 DXOS.org
3
+ //
4
+
5
+ import { test } from '@playwright/test';
6
+ import { expect } from 'chai';
7
+
8
+ import { sleep } from '@dxos/async';
9
+ import { ConnectionState } from '@dxos/protocols/proto/dxos/client/services';
10
+ import { AuthMethod } from '@dxos/protocols/proto/dxos/halo/invitations';
11
+
12
+ import { InvitationsManager } from './invitations-manager';
13
+
14
+ test.describe('Invitations', () => {
15
+ let manager: InvitationsManager;
16
+
17
+ test.beforeEach(async ({ browser }) => {
18
+ manager = new InvitationsManager(browser);
19
+ await manager.init();
20
+ });
21
+
22
+ test.describe('device', () => {
23
+ test('happy path', async () => {
24
+ await manager.createIdentity(0);
25
+ await manager.openPanel(0, 'devices');
26
+ const invitation = await manager.createInvitation(0, 'device');
27
+
28
+ await manager.openPanel(1, 'identity');
29
+ const [authenticationCode] = await Promise.all([
30
+ manager.getAuthenticationCode(),
31
+ manager.acceptInvitation(1, 'device', invitation)
32
+ ]);
33
+ await manager.authenticateInvitation(1, 'device', authenticationCode);
34
+ await manager.doneInvitation(1, 'device');
35
+
36
+ expect(await manager.getDisplayName(0)).to.equal(await manager.getDisplayName(1));
37
+ });
38
+
39
+ // TODO(wittjosiah): Auth method not hooked up?
40
+ test.skip('no auth method', async () => {
41
+ await manager.createIdentity(0);
42
+ await manager.openPanel(0, 'devices');
43
+ const invitation = await manager.createInvitation(0, 'device', { authMethod: AuthMethod.NONE });
44
+
45
+ await manager.openPanel(1, 'identity');
46
+ await manager.acceptInvitation(1, 'device', invitation);
47
+ await manager.doneInvitation(1, 'device');
48
+
49
+ expect(await manager.getDisplayName(0)).to.equal(await manager.getDisplayName(1));
50
+ });
51
+
52
+ // TODO(wittjosiah): Invalid auth code cannot retry.
53
+ test.skip('invalid & retry auth code', async () => {
54
+ await manager.createIdentity(0);
55
+ await manager.openPanel(0, 'devices');
56
+ const invitation = await manager.createInvitation(0, 'device');
57
+
58
+ await manager.openPanel(1, 'identity');
59
+ const [authenticationCode] = await Promise.all([
60
+ manager.getAuthenticationCode(),
61
+ manager.acceptInvitation(1, 'device', invitation)
62
+ ]);
63
+ await manager.authenticateInvitation(1, 'device', '000000');
64
+ await manager.clearAuthenticationCode(1, 'device');
65
+ await manager.authenticateInvitation(1, 'device', authenticationCode);
66
+ await manager.doneInvitation(1, 'device');
67
+
68
+ expect(await manager.getDisplayName(0)).to.equal(await manager.getDisplayName(1));
69
+ });
70
+
71
+ // TODO(wittjosiah): Invalid auth code cannot retry.
72
+ test.skip('invalid & max auth code retries reached, retry invitation', async () => {});
73
+
74
+ // TODO(wittjosiah): Trigger timeout.
75
+ test.skip('invitation timeout & retry', async () => {
76
+ await manager.createIdentity(0);
77
+ await manager.openPanel(0, 'devices');
78
+ const invitation = await manager.createInvitation(0, 'device', { timeout: 100 });
79
+
80
+ await manager.openPanel(1, 'identity');
81
+ await sleep(100);
82
+
83
+ // TODO(wittjosiah): Retry here.
84
+
85
+ const [authenticationCode] = await Promise.all([
86
+ manager.getAuthenticationCode(),
87
+ manager.acceptInvitation(1, 'device', invitation)
88
+ ]);
89
+ await manager.authenticateInvitation(1, 'device', authenticationCode);
90
+ await manager.doneInvitation(1, 'device');
91
+
92
+ expect(await manager.getDisplayName(0)).to.equal(await manager.getDisplayName(1));
93
+ });
94
+
95
+ // TODO(wittjosiah): Propagate cancel to guest.
96
+ test.skip('invitation cancelled by host', async () => {});
97
+
98
+ test('invitation cancelled by guest & retry', async () => {
99
+ await manager.createIdentity(0);
100
+ await manager.openPanel(0, 'devices');
101
+ const invitation = await manager.createInvitation(0, 'device');
102
+
103
+ await manager.openPanel(1, 'identity');
104
+ await manager.acceptInvitation(1, 'device', invitation);
105
+ await manager.cancelInvitation(1, 'device', 'guest');
106
+ await manager.resetInvitation(1);
107
+ const [authenticationCode] = await Promise.all([
108
+ manager.getAuthenticationCode(),
109
+ manager.invitationInputContinue(1, 'device')
110
+ ]);
111
+ await manager.authenticateInvitation(1, 'device', authenticationCode);
112
+ await manager.doneInvitation(1, 'device');
113
+
114
+ expect(await manager.getDisplayName(0)).to.equal(await manager.getDisplayName(1));
115
+ });
116
+
117
+ test('recover from network failure during invitation', async () => {
118
+ await manager.createIdentity(0);
119
+ await manager.openPanel(0, 'devices');
120
+ const invitation = await manager.createInvitation(0, 'device');
121
+
122
+ await manager.openPanel(1, 'identity');
123
+ await manager.acceptInvitation(1, 'device', invitation);
124
+ await sleep(100);
125
+ await manager.setConnectionState(0, ConnectionState.OFFLINE);
126
+ await sleep(100);
127
+ await manager.setConnectionState(0, ConnectionState.ONLINE);
128
+ await manager.resetInvitation(1);
129
+ const [authenticationCode] = await Promise.all([
130
+ manager.getAuthenticationCode(),
131
+ manager.invitationInputContinue(1, 'device')
132
+ ]);
133
+ await manager.authenticateInvitation(1, 'device', authenticationCode);
134
+ await manager.doneInvitation(1, 'device');
135
+
136
+ expect(await manager.getDisplayName(0)).to.equal(await manager.getDisplayName(1));
137
+ });
138
+
139
+ test('multiple concurrent invitations', async () => {
140
+ await manager.createIdentity(0);
141
+ await manager.openPanel(0, 'devices');
142
+ const invitation1 = await manager.createInvitation(0, 'device');
143
+ const invitation2 = await manager.createInvitation(0, 'device');
144
+
145
+ await manager.openPanel(1, 'identity');
146
+ await manager.openPanel(2, 'identity');
147
+ const [authenticationCode1] = await Promise.all([
148
+ manager.getAuthenticationCode(),
149
+ manager.acceptInvitation(1, 'device', invitation1)
150
+ ]);
151
+ // Prevent auth code from being reused.
152
+ await sleep(100);
153
+ const [authenticationCode2] = await Promise.all([
154
+ manager.getAuthenticationCode(),
155
+ manager.acceptInvitation(2, 'device', invitation2)
156
+ ]);
157
+ await manager.authenticateInvitation(1, 'device', authenticationCode1);
158
+ await manager.authenticateInvitation(2, 'device', authenticationCode2);
159
+ await manager.doneInvitation(1, 'device');
160
+ await manager.doneInvitation(2, 'device');
161
+
162
+ expect(await manager.getDisplayName(0)).to.equal(await manager.getDisplayName(1));
163
+ expect(await manager.getDisplayName(0)).to.equal(await manager.getDisplayName(2));
164
+ expect(await manager.getDisplayName(1)).to.equal(await manager.getDisplayName(2));
165
+ });
166
+ });
167
+
168
+ test.describe('space', () => {
169
+ test('happy path', async () => {
170
+ await manager.createIdentity(0);
171
+ await manager.createSpace(0);
172
+ await manager.openPanel(0, 0);
173
+ const invitation = await manager.createInvitation(0, 'space');
174
+
175
+ await manager.createIdentity(1);
176
+ await manager.openPanel(1, 'join');
177
+ const [authenticationCode] = await Promise.all([
178
+ manager.getAuthenticationCode(),
179
+ manager.acceptInvitation(1, 'space', invitation)
180
+ ]);
181
+ await manager.authenticateInvitation(1, 'space', authenticationCode);
182
+ await manager.doneInvitation(1, 'space');
183
+
184
+ await manager.openPanel(0, 'spaces');
185
+ expect(await manager.getSpaceName(0, 0)).to.equal(await manager.getSpaceName(1, 0));
186
+ });
187
+
188
+ test('no auth method', async () => {
189
+ await manager.createIdentity(0);
190
+ await manager.createSpace(0);
191
+ await manager.openPanel(0, 0);
192
+ const invitation = await manager.createInvitation(0, 'space', { authMethod: AuthMethod.NONE });
193
+
194
+ await manager.createIdentity(1);
195
+ await manager.openPanel(1, 'join');
196
+ await manager.acceptInvitation(1, 'space', invitation);
197
+ await manager.doneInvitation(1, 'space');
198
+
199
+ await manager.openPanel(0, 'spaces');
200
+ expect(await manager.getSpaceName(0, 0)).to.equal(await manager.getSpaceName(1, 0));
201
+ });
202
+
203
+ test('invalid & retry auth code', async () => {
204
+ await manager.createIdentity(0);
205
+ await manager.createSpace(0);
206
+ await manager.openPanel(0, 0);
207
+ const invitation = await manager.createInvitation(0, 'space');
208
+
209
+ await manager.createIdentity(1);
210
+ await manager.openPanel(1, 'join');
211
+ const [authenticationCode] = await Promise.all([
212
+ manager.getAuthenticationCode(),
213
+ manager.acceptInvitation(1, 'space', invitation)
214
+ ]);
215
+ await manager.authenticateInvitation(1, 'space', '000000');
216
+
217
+ expect(await manager.authenticatorIsVisible(1, 'space')).to.be.true;
218
+
219
+ await manager.clearAuthenticationCode(1, 'space');
220
+ await manager.authenticateInvitation(1, 'space', authenticationCode);
221
+ await manager.doneInvitation(1, 'space');
222
+
223
+ await manager.openPanel(0, 'spaces');
224
+ expect(await manager.getSpaceName(0, 0)).to.equal(await manager.getSpaceName(1, 0));
225
+ });
226
+
227
+ test('invalid & max auth code retries reached, retry invitation', async () => {
228
+ await manager.createIdentity(0);
229
+ await manager.createSpace(0);
230
+ await manager.openPanel(0, 0);
231
+ const invitation = await manager.createInvitation(0, 'space');
232
+
233
+ await manager.createIdentity(1);
234
+ await manager.openPanel(1, 'join');
235
+ await manager.acceptInvitation(1, 'space', invitation);
236
+
237
+ await manager.authenticateInvitation(1, 'space', '000001');
238
+ await manager.clearAuthenticationCode(1, 'space');
239
+ await manager.authenticateInvitation(1, 'space', '000002');
240
+ await manager.clearAuthenticationCode(1, 'space');
241
+ await manager.authenticateInvitation(1, 'space', '000003');
242
+
243
+ expect(await manager.invitationFailed(1)).to.be.true;
244
+
245
+ await manager.resetInvitation(1);
246
+ const [authenticationCode] = await Promise.all([
247
+ manager.getAuthenticationCode(),
248
+ manager.invitationInputContinue(1, 'space')
249
+ ]);
250
+ await manager.authenticateInvitation(1, 'space', authenticationCode);
251
+ await manager.doneInvitation(1, 'space');
252
+
253
+ await manager.openPanel(0, 'spaces');
254
+ expect(await manager.getSpaceName(0, 0)).to.equal(await manager.getSpaceName(1, 0));
255
+ });
256
+
257
+ // TODO(wittjosiah): Trigger timeout.
258
+ test.skip('invitation timeout & retry', async () => {
259
+ await manager.createIdentity(0);
260
+ await manager.createSpace(0);
261
+ await manager.openPanel(0, 0);
262
+ const invitation = await manager.createInvitation(0, 'space', { timeout: 100 });
263
+
264
+ await manager.createIdentity(1);
265
+ await manager.openPanel(1, 'join');
266
+ await sleep(100);
267
+
268
+ // TODO(wittjosiah): Retry here.
269
+
270
+ const [authenticationCode] = await Promise.all([
271
+ manager.getAuthenticationCode(),
272
+ manager.acceptInvitation(1, 'space', invitation)
273
+ ]);
274
+ await manager.authenticateInvitation(1, 'space', authenticationCode);
275
+ await manager.doneInvitation(1, 'space');
276
+
277
+ await manager.openPanel(0, 'spaces');
278
+ expect(await manager.getSpaceName(0, 0)).to.equal(await manager.getSpaceName(1, 0));
279
+ });
280
+
281
+ // TODO(wittjosiah): Propagate cancel to guest.
282
+ test.skip('invitation cancelled by host', async () => {});
283
+
284
+ test('invitation cancelled by guest & retry', async () => {
285
+ await manager.createIdentity(0);
286
+ await manager.createSpace(0);
287
+ await manager.openPanel(0, 0);
288
+ const invitation = await manager.createInvitation(0, 'space');
289
+
290
+ await manager.createIdentity(1);
291
+ await manager.openPanel(1, 'join');
292
+ await manager.acceptInvitation(1, 'space', invitation);
293
+ await manager.cancelInvitation(1, 'space', 'guest');
294
+ await manager.resetInvitation(1);
295
+ const [authenticationCode] = await Promise.all([
296
+ manager.getAuthenticationCode(),
297
+ manager.invitationInputContinue(1, 'space')
298
+ ]);
299
+ await manager.authenticateInvitation(1, 'space', authenticationCode);
300
+ await manager.doneInvitation(1, 'space');
301
+
302
+ await manager.openPanel(0, 'spaces');
303
+ expect(await manager.getSpaceName(0, 0)).to.equal(await manager.getSpaceName(1, 0));
304
+ });
305
+
306
+ test('recover from network failure during invitation', async () => {
307
+ await manager.createIdentity(0);
308
+ await manager.createSpace(0);
309
+ await manager.openPanel(0, 0);
310
+ const invitation = await manager.createInvitation(0, 'space');
311
+
312
+ await manager.createIdentity(1);
313
+ await manager.openPanel(1, 'join');
314
+ await manager.acceptInvitation(1, 'space', invitation);
315
+ await sleep(100);
316
+ await manager.setConnectionState(0, ConnectionState.OFFLINE);
317
+ await sleep(100);
318
+ await manager.setConnectionState(0, ConnectionState.ONLINE);
319
+ await manager.resetInvitation(1);
320
+ const [authenticationCode] = await Promise.all([
321
+ manager.getAuthenticationCode(),
322
+ manager.invitationInputContinue(1, 'space')
323
+ ]);
324
+ await manager.authenticateInvitation(1, 'space', authenticationCode);
325
+ await manager.doneInvitation(1, 'space');
326
+
327
+ await manager.openPanel(0, 'spaces');
328
+ expect(await manager.getSpaceName(0, 0)).to.equal(await manager.getSpaceName(1, 0));
329
+ });
330
+
331
+ test('multiple concurrent invitations', async () => {
332
+ await manager.createIdentity(0);
333
+ await manager.createSpace(0);
334
+ await manager.openPanel(0, 0);
335
+ const invitation1 = await manager.createInvitation(0, 'space');
336
+ const invitation2 = await manager.createInvitation(0, 'space');
337
+
338
+ await manager.createIdentity(1);
339
+ await manager.createIdentity(2);
340
+ await manager.openPanel(1, 'join');
341
+ await manager.openPanel(2, 'join');
342
+ const [authenticationCode1] = await Promise.all([
343
+ manager.getAuthenticationCode(),
344
+ manager.acceptInvitation(1, 'space', invitation1)
345
+ ]);
346
+ // Prevent auth code from being reused.
347
+ await sleep(100);
348
+ const [authenticationCode2] = await Promise.all([
349
+ manager.getAuthenticationCode(),
350
+ manager.acceptInvitation(2, 'space', invitation2)
351
+ ]);
352
+ await manager.authenticateInvitation(1, 'space', authenticationCode1);
353
+ await manager.authenticateInvitation(2, 'space', authenticationCode2);
354
+ await manager.doneInvitation(1, 'space');
355
+ await manager.doneInvitation(2, 'space');
356
+
357
+ await manager.openPanel(0, 'spaces');
358
+ expect(await manager.getSpaceName(0, 0)).to.equal(await manager.getSpaceName(1, 0));
359
+ expect(await manager.getSpaceName(0, 0)).to.equal(await manager.getSpaceName(2, 0));
360
+ expect(await manager.getSpaceName(1, 0)).to.equal(await manager.getSpaceName(2, 0));
361
+ });
362
+ });
363
+ });
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { defaultPlaywrightConfig } from '@dxos/test/playwright';
6
+
7
+ export default defaultPlaywrightConfig;