@dxos/shell 0.3.11-main.68db74e → 0.3.11-main.6901f09

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 (67) hide show
  1. package/dist/bundle/shell.mjs +15305 -13616
  2. package/dist/bundle/shell.mjs.map +1 -1
  3. package/dist/bundle/shell.umd.js +81 -75
  4. package/dist/bundle/shell.umd.js.map +1 -1
  5. package/dist/bundle/style.css +1 -1
  6. package/dist/lib/browser/meta.json +1 -1
  7. package/dist/lib/browser/react.mjs +55 -43
  8. package/dist/lib/browser/react.mjs.map +3 -3
  9. package/dist/lib/browser/testing/index.mjs +35 -14
  10. package/dist/lib/browser/testing/index.mjs.map +3 -3
  11. package/dist/types/src/components/IdentityList/SpaceMemberList.js +1 -1
  12. package/dist/types/src/components/IdentityList/SpaceMemberList.js.map +1 -1
  13. package/dist/types/src/components/Panel/Action.d.ts.map +1 -1
  14. package/dist/types/src/components/Panel/Action.js +3 -18
  15. package/dist/types/src/components/Panel/Action.js.map +1 -1
  16. package/dist/types/src/panels/IdentityPanel/IdentityPanel.js +1 -1
  17. package/dist/types/src/panels/IdentityPanel/IdentityPanel.js.map +1 -1
  18. package/dist/types/src/panels/IdentityPanel/IdentityPanel.stories.js +3 -3
  19. package/dist/types/src/panels/IdentityPanel/IdentityPanel.stories.js.map +1 -1
  20. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.d.ts.map +1 -1
  21. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.js +1 -1
  22. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.js.map +1 -1
  23. package/dist/types/src/panels/IdentityPanel/steps/SignOutChooser.js +2 -2
  24. package/dist/types/src/panels/IdentityPanel/steps/SignOutChooser.js.map +1 -1
  25. package/dist/types/src/panels/JoinPanel/joinMachine.d.ts +1 -1
  26. package/dist/types/src/panels/JoinPanel/joinMachine.d.ts.map +1 -1
  27. package/dist/types/src/panels/JoinPanel/joinMachine.js +14 -17
  28. package/dist/types/src/panels/JoinPanel/joinMachine.js.map +1 -1
  29. package/dist/types/src/panels/JoinPanel/steps/AdditionMethodChooser.js +3 -3
  30. package/dist/types/src/panels/JoinPanel/steps/AdditionMethodChooser.js.map +1 -1
  31. package/dist/types/src/panels/JoinPanel/steps/InvitationInput.d.ts.map +1 -1
  32. package/dist/types/src/panels/JoinPanel/steps/InvitationInput.js +13 -6
  33. package/dist/types/src/panels/JoinPanel/steps/InvitationInput.js.map +1 -1
  34. package/dist/types/src/panels/JoinPanel/steps/InvitationRescuer.js.map +1 -1
  35. package/dist/types/src/panels/JoinPanel/steps/ResetIdentity.js +1 -1
  36. package/dist/types/src/panels/JoinPanel/steps/ResetIdentity.js.map +1 -1
  37. package/dist/types/src/playwright/invitations-manager.d.ts +1 -0
  38. package/dist/types/src/playwright/invitations-manager.d.ts.map +1 -1
  39. package/dist/types/src/playwright/invitations-manager.js +4 -1
  40. package/dist/types/src/playwright/invitations-manager.js.map +1 -1
  41. package/dist/types/src/playwright/invitations.spec.js +15 -0
  42. package/dist/types/src/playwright/invitations.spec.js.map +1 -1
  43. package/dist/types/src/stories/Invitations.e2e-stories.js +3 -3
  44. package/dist/types/src/stories/Invitations.e2e-stories.js.map +1 -1
  45. package/dist/types/src/styles/invitationStatusStyles.d.ts.map +1 -1
  46. package/dist/types/src/styles/invitationStatusStyles.js.map +1 -1
  47. package/dist/types/src/testing/shell-manager.d.ts +5 -4
  48. package/dist/types/src/testing/shell-manager.d.ts.map +1 -1
  49. package/dist/types/src/testing/shell-manager.js +36 -12
  50. package/dist/types/src/testing/shell-manager.js.map +1 -1
  51. package/package.json +22 -22
  52. package/src/components/IdentityList/SpaceMemberList.tsx +1 -1
  53. package/src/components/Panel/Action.tsx +4 -19
  54. package/src/panels/IdentityPanel/IdentityPanel.stories.tsx +1 -1
  55. package/src/panels/IdentityPanel/IdentityPanel.tsx +1 -1
  56. package/src/panels/IdentityPanel/steps/IdentityActionChooser.tsx +5 -1
  57. package/src/panels/IdentityPanel/steps/SignOutChooser.tsx +2 -0
  58. package/src/panels/JoinPanel/joinMachine.ts +15 -18
  59. package/src/panels/JoinPanel/steps/AdditionMethodChooser.tsx +3 -3
  60. package/src/panels/JoinPanel/steps/InvitationInput.tsx +7 -1
  61. package/src/panels/JoinPanel/steps/InvitationRescuer.tsx +2 -2
  62. package/src/panels/JoinPanel/steps/ResetIdentity.tsx +1 -1
  63. package/src/playwright/invitations-manager.ts +5 -1
  64. package/src/playwright/invitations.spec.ts +19 -0
  65. package/src/stories/Invitations.e2e-stories.tsx +2 -2
  66. package/src/styles/invitationStatusStyles.ts +6 -6
  67. package/src/testing/shell-manager.ts +40 -13
@@ -6,7 +6,7 @@ import { CaretDown, Check, type IconProps, Placeholder } from '@phosphor-icons/r
6
6
  import { useControllableState } from '@radix-ui/react-use-controllable-state';
7
7
  import React, { type Dispatch, type FC, forwardRef, type SetStateAction } from 'react';
8
8
 
9
- import { type ButtonProps, Button, type ComponentFragment, DropdownMenu, useTranslation } from '@dxos/react-ui';
9
+ import { type ButtonProps, Button, DropdownMenu, useTranslation } from '@dxos/react-ui';
10
10
  import { descriptionText, getSize, mx } from '@dxos/react-ui-theme';
11
11
 
12
12
  export type LargeButtonProps = ButtonProps & {
@@ -26,21 +26,6 @@ export type BifurcatedActionProps = {
26
26
  defaultActiveAction?: string;
27
27
  } & Omit<LargeButtonProps, 'children' | 'onClick'>;
28
28
 
29
- const buttonColorFragment: ComponentFragment<LargeButtonProps> = (props: LargeButtonProps) => {
30
- const resolvedVariant = props.variant ?? 'default';
31
- return [
32
- !props.disabled
33
- ? resolvedVariant === 'default'
34
- ? 'bg-neutral-25 hover:bg-white hover:text-primary-500 active:bg-neutral-25 dark:active:bg-neutral-800'
35
- : resolvedVariant === 'ghost'
36
- ? 'active:bg-neutral-125 dark:active:bg-neutral-800'
37
- : resolvedVariant === 'primary'
38
- ? 'active:bg-primary-700 dark:active:bg-primary-500'
39
- : ''
40
- : '',
41
- ];
42
- };
43
-
44
29
  const defaultActions = {
45
30
  noopAction: {
46
31
  label: 'No-op',
@@ -76,7 +61,7 @@ export const BifurcatedAction = forwardRef<HTMLButtonElement, BifurcatedActionPr
76
61
  <div role='none' className={mx('mbs-2 flex gap-px items-center', isFull && 'is-full')}>
77
62
  <Button
78
63
  {...rest}
79
- classNames={['bs-11 flex-1 min-is-0 flex gap-2 rounded-ie-none', classNames, ...buttonColorFragment(props)]}
64
+ classNames={['bs-11 flex-1 min-is-0 flex gap-2 rounded-ie-none', classNames]}
80
65
  ref={forwardedRef}
81
66
  variant={variant}
82
67
  onClick={activeAction.onClick}
@@ -86,7 +71,7 @@ export const BifurcatedAction = forwardRef<HTMLButtonElement, BifurcatedActionPr
86
71
  </Button>
87
72
  <DropdownMenu.Root>
88
73
  <DropdownMenu.Trigger asChild>
89
- <Button classNames={['bs-11 flex-none rounded-is-none', classNames, ...buttonColorFragment(props)]}>
74
+ <Button classNames={['bs-11 flex-none rounded-is-none', classNames]}>
90
75
  <span className='sr-only'>{t('invite options label')}</span>
91
76
  <CaretDown />
92
77
  </Button>
@@ -132,7 +117,7 @@ export const Action = forwardRef<HTMLButtonElement, LargeButtonProps>((props, fo
132
117
  return (
133
118
  <Button
134
119
  {...rest}
135
- classNames={[isFull && 'is-full', 'bs-11 flex gap-2 mbs-2', ...buttonColorFragment(props), classNames]}
120
+ classNames={[isFull && 'is-full', 'bs-11 flex gap-2 mbs-2', classNames]}
136
121
  ref={forwardedRef}
137
122
  variant={variant}
138
123
  >
@@ -4,9 +4,9 @@
4
4
 
5
5
  import '@dxosTheme';
6
6
 
7
- import { faker } from '@faker-js/faker';
8
7
  import React from 'react';
9
8
 
9
+ import { faker } from '@dxos/random';
10
10
  import { PublicKey } from '@dxos/react-client';
11
11
  import { DeviceKind } from '@dxos/react-client/halo';
12
12
  import { Invitation } from '@dxos/react-client/invitations';
@@ -32,7 +32,7 @@ const IdentityHeading = ({ titleId, title, identity, onDone }: IdentityPanelHead
32
32
  <Avatar.Frame classNames='block mbs-4 mbe-2 mli-auto chromatic-ignore'>
33
33
  <Avatar.Fallback href={fallbackHref} />
34
34
  </Avatar.Frame>
35
- <Avatar.Label classNames='block text-center font-light text-xl'>
35
+ <Avatar.Label classNames='block text-center font-light text-xl' data-testid='identityHeading.displayName'>
36
36
  {identity.profile?.displayName ?? generateName(identity.identityKey.toHex())}
37
37
  </Avatar.Label>
38
38
  </Avatar.Root>
@@ -88,7 +88,11 @@ export const IdentityActionChooserImpl = ({
88
88
  <span className='grow mli-3'>{t('choose profile label')}</span>
89
89
  <CaretRight weight='bold' className={getSize(4)} />
90
90
  </Action>
91
- <Action data-testid='sign-out' onClick={() => send?.({ type: 'chooseSignOut' })} classNames='plb-4'>
91
+ <Action
92
+ data-testid='devices-panel.sign-out'
93
+ onClick={() => send?.({ type: 'chooseSignOut' })}
94
+ classNames='plb-4'
95
+ >
92
96
  <Power className={getSize(6)} />
93
97
  <span className='grow mli-3'>{t('choose sign out label')}</span>
94
98
  <CaretRight weight='bold' className={getSize(4)} />
@@ -62,6 +62,7 @@ const SignOutChooserImpl = ({ disabled, onResetDevice, onJoinNewIdentity, onBack
62
62
  </Action>
63
63
  <Action
64
64
  variant='primary'
65
+ data-testid='sign-out.join-new-identity'
65
66
  classNames='!bg-error-500 hover:!bg-error-550'
66
67
  disabled={disabled}
67
68
  onClick={onJoinNewIdentity}
@@ -70,6 +71,7 @@ const SignOutChooserImpl = ({ disabled, onResetDevice, onJoinNewIdentity, onBack
70
71
  </Action>
71
72
  <Action
72
73
  variant='primary'
74
+ data-testid='sign-out.reset-device'
73
75
  classNames='!bg-error-500 hover:!bg-error-550'
74
76
  disabled={disabled}
75
77
  onClick={onResetDevice}
@@ -15,7 +15,7 @@ import {
15
15
  } from 'xstate';
16
16
 
17
17
  import { log } from '@dxos/log';
18
- import type { Client } from '@dxos/react-client';
18
+ import { type Client, AlreadyJoinedError } from '@dxos/react-client';
19
19
  import { type Identity } from '@dxos/react-client/halo';
20
20
  import { type AuthenticatingInvitationObservable, Invitation, InvitationEncoder } from '@dxos/react-client/invitations';
21
21
 
@@ -133,6 +133,12 @@ const getInvitationSubscribable = (
133
133
  }
134
134
  },
135
135
  (error: any) => {
136
+ if (error instanceof AlreadyJoinedError) {
137
+ log('[already joined]', { Kind, error });
138
+ next({ type: `success${Kind}Invitation`, invitation: invitation.get() });
139
+ return complete?.();
140
+ }
141
+
136
142
  log.error('[invitation errored]', { Kind, error });
137
143
  next({ type: `fail${Kind}Invitation`, reason: 'error' });
138
144
  return onError?.(error);
@@ -437,9 +443,8 @@ const useJoinMachine = (
437
443
  ({ halo }: JoinMachineContext) => {
438
444
  if (halo.unredeemedCode) {
439
445
  try {
440
- const invitationObservable = client.halo.join(
441
- InvitationEncoder.decode(defaultCodeFromUrl('halo', halo.unredeemedCode)),
442
- );
446
+ const invitation = InvitationEncoder.decode(defaultCodeFromUrl('halo', halo.unredeemedCode));
447
+ const invitationObservable = client.halo.join(invitation);
443
448
  return {
444
449
  ...halo,
445
450
  invitationObservable,
@@ -461,20 +466,12 @@ const useJoinMachine = (
461
466
  if (space.unredeemedCode) {
462
467
  try {
463
468
  const invitation = InvitationEncoder.decode(defaultCodeFromUrl('space', space.unredeemedCode));
464
- // Don’t join with the invitation if it has already succeeded.
465
- if (space.succeededKeys?.has(invitation.spaceKey?.toHex() ?? 'never')) {
466
- return {
467
- ...space,
468
- invitation,
469
- };
470
- } else {
471
- const invitationObservable = client.spaces.join(invitation);
472
- return {
473
- ...space,
474
- invitationObservable,
475
- invitationSubscribable: getInvitationSubscribable('Space', invitationObservable),
476
- };
477
- }
469
+ const invitationObservable = client.spaces.join(invitation);
470
+ return {
471
+ ...space,
472
+ invitationObservable,
473
+ invitationSubscribable: getInvitationSubscribable('Space', invitationObservable),
474
+ };
478
475
  } catch (err) {
479
476
  log.error('Could not redeem space invitation code', err);
480
477
  return space;
@@ -35,7 +35,7 @@ export const AdditionMethodChooser = (viewStateProps: AdditionMethodChooserProps
35
35
  before={<Plus className={getSize(6)} />}
36
36
  onClick={() => send({ type: 'createIdentity' })}
37
37
  data-autofocus='choosingAuthMethod'
38
- data-testid='create-identity'
38
+ data-testid='identity-chooser.create-identity'
39
39
  >
40
40
  {t('create identity label')}
41
41
  </CompoundButton>
@@ -44,7 +44,7 @@ export const AdditionMethodChooser = (viewStateProps: AdditionMethodChooserProps
44
44
  description={t('join identity description')}
45
45
  before={<QrCode className={getSize(6)} />}
46
46
  onClick={() => send({ type: 'acceptHaloInvitation' })}
47
- data-testid='join-identity'
47
+ data-testid='identity-chooser.join-identity'
48
48
  >
49
49
  {t('join identity label')}
50
50
  </CompoundButton>
@@ -55,7 +55,7 @@ export const AdditionMethodChooser = (viewStateProps: AdditionMethodChooserProps
55
55
  description={t('recover identity description')}
56
56
  before={<Textbox className={getSize(6)} />}
57
57
  onClick={() => send({ type: 'recoverIdentity' })}
58
- data-testid='recover-identity'
58
+ data-testid='identity-chooser.recover-identity'
59
59
  >
60
60
  {t('recover identity label')}
61
61
  </CompoundButton>
@@ -39,12 +39,17 @@ export const InvitationInput = (props: InvitationInputProps) => {
39
39
  unredeemedCode && setInputValue(unredeemedCode ?? '');
40
40
  }, [unredeemedCode]);
41
41
 
42
- const handleNext = () =>
42
+ const handleNext = () => {
43
+ if (inputValue.trim().length === 0) {
44
+ return;
45
+ }
46
+
43
47
  send({
44
48
  type: `set${Kind}InvitationCode`,
45
49
  code: invitationCodeFromUrl(inputValue),
46
50
  ...(Kind === 'Space' && { succeededKeys }),
47
51
  });
52
+ };
48
53
 
49
54
  const exitAction = (
50
55
  <Action
@@ -62,6 +67,7 @@ export const InvitationInput = (props: InvitationInputProps) => {
62
67
  <div role='none' className='grow flex flex-col justify-center'>
63
68
  <Input
64
69
  label={<StepHeading>{t('invitation input label')}</StepHeading>}
70
+ // TODO(burdon): Translation.
65
71
  placeholder='Type an invitation code'
66
72
  disabled={disabled}
67
73
  value={inputValue}
@@ -39,8 +39,8 @@ const InvitationActions = ({
39
39
  failReason === 'timeout'
40
40
  ? 'timeout status label'
41
41
  : failReason === 'cancelled'
42
- ? 'cancelled status label'
43
- : 'error status label',
42
+ ? 'cancelled status label'
43
+ : 'error status label',
44
44
  )}
45
45
  </StepHeading>
46
46
  <div role='none' className='grow' />
@@ -50,7 +50,7 @@ export const ResetIdentityImpl = ({ disabled, validationMessage, onConfirm }: Re
50
50
  {...{ validationMessage }}
51
51
  label={<StepHeading>{t('reset identity input label', { confirmationValue })}</StepHeading>}
52
52
  disabled={disabled}
53
- data-testid='identity-input'
53
+ data-testid='reset-identity-input'
54
54
  placeholder={confirmationValue}
55
55
  onChange={({ target: { value } }) => setInputValue(value)}
56
56
  />
@@ -70,6 +70,10 @@ export class InvitationsManager extends ScopedShellManager {
70
70
  return this.peer(id).getByTestId('space-list-item').nth(nth).textContent();
71
71
  }
72
72
 
73
+ async getSpaceMembersCount(id: number) {
74
+ return this.peer(id).getByTestId('space-members-list').locator('li').count();
75
+ }
76
+
73
77
  async getAuthCode(): Promise<string> {
74
78
  return await this._authCode.wait();
75
79
  }
@@ -147,7 +151,7 @@ export class InvitationsManager extends ScopedShellManager {
147
151
  const peer = this.peer(id);
148
152
  // TODO(wittjosiah): Update ids.
149
153
  if (type === 'device') {
150
- await peer.getByTestId('join-identity').click();
154
+ await peer.getByTestId('identity-chooser.join-identity').click();
151
155
  }
152
156
  await peer.getByTestId(`${type === 'device' ? 'halo' : 'space'}-invitation-input`).waitFor({ timeout: 500 });
153
157
  await peer.getByTestId(`${type === 'device' ? 'halo' : 'space'}-invitation-input`).fill(invitation);
@@ -185,6 +185,25 @@ test.describe('Invitations', () => {
185
185
  expect(await manager.getSpaceName(0, 1)).to.equal(await manager.getSpaceName(1, 1));
186
186
  });
187
187
 
188
+ test('already joined', async () => {
189
+ await manager.createIdentity(0);
190
+ await manager.createSpace(0);
191
+ await manager.openPanel(0, 1);
192
+ const invitation1 = await manager.createInvitation(0, 'space', { authMethod: Invitation.AuthMethod.NONE });
193
+ const invitation2 = await manager.createInvitation(0, 'space');
194
+
195
+ await manager.createIdentity(1);
196
+ await manager.openPanel(1, 'join');
197
+ await manager.acceptInvitation(1, 'space', invitation1);
198
+ await manager.doneInvitation('space', manager.peer(1));
199
+
200
+ await manager.openPanel(1, 'join');
201
+ await manager.acceptInvitation(1, 'space', invitation2);
202
+ await manager.doneInvitation('space', manager.peer(1));
203
+
204
+ expect(await manager.getSpaceMembersCount(0)).to.equal(2);
205
+ });
206
+
188
207
  test('no auth method', async () => {
189
208
  await manager.createIdentity(0);
190
209
  await manager.createSpace(0);
@@ -4,10 +4,10 @@
4
4
 
5
5
  import '@dxosTheme';
6
6
 
7
- import { faker } from '@faker-js/faker';
8
7
  import { Laptop, Planet, Plus, PlusCircle, SignIn, QrCode, WifiHigh, WifiSlash } from '@phosphor-icons/react';
9
8
  import React, { useMemo, useState } from 'react';
10
9
 
10
+ import { faker } from '@dxos/random';
11
11
  import { useClient } from '@dxos/react-client';
12
12
  import { type Space, type SpaceMember, SpaceProxy, useSpaces } from '@dxos/react-client/echo';
13
13
  import { useIdentity } from '@dxos/react-client/halo';
@@ -72,7 +72,7 @@ const Panel = ({ id, panel, setPanel }: { id: number; panel?: PanelType; setPane
72
72
  <ButtonGroup classNames='mbe-4'>
73
73
  {/* <Tooltip content='Create Space'> */}
74
74
  <Button
75
- onClick={() => client.spaces.create({ name: faker.animal.bird() })}
75
+ onClick={() => client.spaces.create({ name: faker.commerce.productName() })}
76
76
  data-testid='invitations.create-space'
77
77
  >
78
78
  <PlusCircle className={getSize(6)} />
@@ -26,17 +26,17 @@ export const resolvedStrokeColor = (status: Invitation.State) =>
26
26
  status === Invitation.State.ERROR
27
27
  ? errorStrokeColor
28
28
  : status === Invitation.State.CANCELLED || status === Invitation.State.TIMEOUT
29
- ? cancelledStrokeColor
30
- : successStrokeColor;
29
+ ? cancelledStrokeColor
30
+ : successStrokeColor;
31
31
  export const resolvedBgColor = (status: Invitation.State) =>
32
32
  status === Invitation.State.ERROR
33
33
  ? errorBgColor
34
34
  : status === Invitation.State.CANCELLED || status === Invitation.State.TIMEOUT
35
- ? cancelledBgColor
36
- : successBgColor;
35
+ ? cancelledBgColor
36
+ : successBgColor;
37
37
  export const resolvedTextColor = (status: Invitation.State) =>
38
38
  status === Invitation.State.ERROR
39
39
  ? errorTextColor
40
40
  : status === Invitation.State.CANCELLED || status === Invitation.State.TIMEOUT
41
- ? cancelledTextColor
42
- : successTextColor;
41
+ ? cancelledTextColor
42
+ : successTextColor;
@@ -2,12 +2,14 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import type { ConsoleMessage, Page } from '@playwright/test';
5
+ import type { ConsoleMessage, Dialog, Page } from '@playwright/test';
6
6
 
7
7
  import { sleep, Trigger } from '@dxos/async';
8
8
 
9
9
  import { ScopedShellManager } from './scoped-shell-manager';
10
10
 
11
+ // TODO(wittjosiah): Normalize data-testids between snake and camel case.
12
+
11
13
  export class ShellManager extends ScopedShellManager {
12
14
  private _invitationCode = new Trigger<string>();
13
15
  private _authCode = new Trigger<string>();
@@ -27,6 +29,10 @@ export class ShellManager extends ScopedShellManager {
27
29
 
28
30
  // Getters
29
31
 
32
+ async getDisplayName(): Promise<string | null> {
33
+ return this.shell.getByTestId('identityHeading.displayName').textContent();
34
+ }
35
+
30
36
  async isCurrentSpaceViewVisible(): Promise<boolean> {
31
37
  return await this.shell.getByTestId('current-space-view').isVisible();
32
38
  }
@@ -44,29 +50,50 @@ export class ShellManager extends ScopedShellManager {
44
50
  await this.shell.getByTestId('halo-invitation-accepted-done').click();
45
51
  }
46
52
 
47
- async closeShell() {
48
- await this.page.keyboard.press('Escape');
53
+ async createDeviceInvitation(): Promise<string> {
54
+ this._invitationCode = new Trigger<string>();
55
+ this._authCode = new Trigger<string>();
56
+ await this.shell.getByTestId('devices-panel.create-invitation').click();
57
+ return await this._invitationCode.wait();
49
58
  }
50
59
 
51
- // TODO(wittjosiah): These shortcuts are no longer within the SDK so these commands should live elsewhere.
52
- async openCurrentSpace() {
53
- await this.page.keyboard.press('Meta+.');
60
+ async resetDevice() {
61
+ const handleDialog = async (dialog: Dialog) => {
62
+ if (dialog.type() === 'confirm') {
63
+ await dialog.accept();
64
+ }
65
+ };
66
+
67
+ this.page.on('dialog', handleDialog);
68
+ await this.shell.getByTestId('devices-panel.sign-out').click();
69
+ await this.shell.getByTestId('sign-out.reset-device').click();
70
+ this.page.off('dialog', handleDialog);
54
71
  }
55
72
 
56
- async openSpaceList() {
57
- await this.page.keyboard.press('Meta+Shift+>');
73
+ async joinNewIdentity(invitationCode: string) {
74
+ await this.shell.getByTestId('devices-panel.sign-out').click();
75
+ await this.shell.getByTestId('sign-out.join-new-identity').click();
76
+ await this.shell.getByTestId('reset-identity-input').fill('CONFIRM');
77
+ await this.shell.getByTestId('reset-identity-input-confirm').click();
78
+ await this.shell.getByTestId('identity-chooser.join-identity').click();
79
+ await this.inputInvitation('device', invitationCode, this.shell);
58
80
  }
59
81
 
60
- async showAllSpaces() {
61
- await this.shell.getByTestId('show-all-spaces').click();
82
+ async authenticateDevice(authCode: string, skipDone = true) {
83
+ // Wait for focus to shift before typing.
84
+ await sleep(1500);
85
+ await this.authenticateInvitation('device', authCode, this.shell);
86
+ // TODO(wittjosiah): When "signing out" and joinging a new identity, the done step seems to be skipped.
87
+ if (!skipDone) {
88
+ await this.doneInvitation('device', this.shell);
89
+ }
62
90
  }
63
91
 
64
- async showCurrentSpace() {
65
- await this.shell.getByTestId('show-current-space').click();
92
+ async closeShell() {
93
+ await this.page.keyboard.press('Escape');
66
94
  }
67
95
 
68
96
  async createSpaceInvitation(): Promise<string> {
69
- await this.openCurrentSpace();
70
97
  this._invitationCode = new Trigger<string>();
71
98
  this._authCode = new Trigger<string>();
72
99
  await this.shell.getByTestId('spaces-panel.create-invitation').click();