Refactor + improve test coverage for QR login (#9525)
This commit is contained in:
parent
9096bd82d6
commit
f05cc5d8f3
10 changed files with 1604 additions and 774 deletions
|
@ -22,14 +22,8 @@ import { logger } from 'matrix-js-sdk/src/logger';
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
|
||||||
import QRCode from '../elements/QRCode';
|
|
||||||
import Spinner from '../elements/Spinner';
|
|
||||||
import { Icon as BackButtonIcon } from "../../../../res/img/element-icons/back.svg";
|
|
||||||
import { Icon as DevicesIcon } from "../../../../res/img/element-icons/devices.svg";
|
|
||||||
import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning-badge.svg";
|
|
||||||
import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg";
|
|
||||||
import { wrapRequestWithDialog } from '../../../utils/UserInteractiveAuth';
|
import { wrapRequestWithDialog } from '../../../utils/UserInteractiveAuth';
|
||||||
|
import LoginWithQRFlow from './LoginWithQRFlow';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The intention of this enum is to have a mode that scans a QR code instead of generating one.
|
* The intention of this enum is to have a mode that scans a QR code instead of generating one.
|
||||||
|
@ -41,7 +35,7 @@ export enum Mode {
|
||||||
Show = "show",
|
Show = "show",
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Phase {
|
export enum Phase {
|
||||||
Loading,
|
Loading,
|
||||||
ShowingQR,
|
ShowingQR,
|
||||||
Connecting,
|
Connecting,
|
||||||
|
@ -51,6 +45,14 @@ enum Phase {
|
||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Click {
|
||||||
|
Cancel,
|
||||||
|
Decline,
|
||||||
|
Approve,
|
||||||
|
TryAgain,
|
||||||
|
Back,
|
||||||
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
mode: Mode;
|
mode: Mode;
|
||||||
|
@ -68,7 +70,7 @@ interface IState {
|
||||||
/**
|
/**
|
||||||
* A component that allows sign in and E2EE set up with a QR code.
|
* A component that allows sign in and E2EE set up with a QR code.
|
||||||
*
|
*
|
||||||
* It implements both `login.start` and `login-reciprocate` capabilities as well as both scanning and showing QR codes.
|
* It implements `login.reciprocate` capabilities and showing QR codes.
|
||||||
*
|
*
|
||||||
* This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906
|
* This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906
|
||||||
*/
|
*/
|
||||||
|
@ -138,6 +140,7 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.setState({ phase: Phase.Verifying });
|
||||||
await this.state.rendezvous.verifyNewDeviceOnExistingDevice();
|
await this.state.rendezvous.verifyNewDeviceOnExistingDevice();
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -197,200 +200,41 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private cancelClicked = async (e: React.FormEvent) => {
|
private onClick = async (type: Click) => {
|
||||||
e.preventDefault();
|
switch (type) {
|
||||||
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
|
case Click.Cancel:
|
||||||
this.reset();
|
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
|
||||||
this.props.onFinished(false);
|
this.reset();
|
||||||
};
|
this.props.onFinished(false);
|
||||||
|
break;
|
||||||
private declineClicked = async (e: React.FormEvent) => {
|
case Click.Approve:
|
||||||
e.preventDefault();
|
await this.approveLogin();
|
||||||
await this.state.rendezvous?.declineLoginOnExistingDevice();
|
break;
|
||||||
this.reset();
|
case Click.Decline:
|
||||||
this.props.onFinished(false);
|
await this.state.rendezvous?.declineLoginOnExistingDevice();
|
||||||
};
|
this.reset();
|
||||||
|
this.props.onFinished(false);
|
||||||
private tryAgainClicked = async (e: React.FormEvent) => {
|
break;
|
||||||
e.preventDefault();
|
case Click.TryAgain:
|
||||||
this.reset();
|
this.reset();
|
||||||
await this.updateMode(this.props.mode);
|
await this.updateMode(this.props.mode);
|
||||||
};
|
break;
|
||||||
|
case Click.Back:
|
||||||
private onBackClick = async () => {
|
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
|
||||||
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
|
this.props.onFinished(false);
|
||||||
|
break;
|
||||||
this.props.onFinished(false);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
private cancelButton = () => <AccessibleButton
|
|
||||||
kind="primary_outline"
|
|
||||||
onClick={this.cancelClicked}
|
|
||||||
>
|
|
||||||
{ _t("Cancel") }
|
|
||||||
</AccessibleButton>;
|
|
||||||
|
|
||||||
private simpleSpinner = (description?: string): JSX.Element => {
|
|
||||||
return <div className="mx_LoginWithQR_spinner">
|
|
||||||
<div>
|
|
||||||
<Spinner />
|
|
||||||
{ description && <p>{ description }</p> }
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
let title: string;
|
|
||||||
let titleIcon: JSX.Element | undefined;
|
|
||||||
let main: JSX.Element | undefined;
|
|
||||||
let buttons: JSX.Element | undefined;
|
|
||||||
let backButton = true;
|
|
||||||
let cancellationMessage: string | undefined;
|
|
||||||
let centreTitle = false;
|
|
||||||
|
|
||||||
switch (this.state.phase) {
|
|
||||||
case Phase.Error:
|
|
||||||
switch (this.state.failureReason) {
|
|
||||||
case RendezvousFailureReason.Expired:
|
|
||||||
cancellationMessage = _t("The linking wasn't completed in the required time.");
|
|
||||||
break;
|
|
||||||
case RendezvousFailureReason.InvalidCode:
|
|
||||||
cancellationMessage = _t("The scanned code is invalid.");
|
|
||||||
break;
|
|
||||||
case RendezvousFailureReason.UnsupportedAlgorithm:
|
|
||||||
cancellationMessage = _t("Linking with this device is not supported.");
|
|
||||||
break;
|
|
||||||
case RendezvousFailureReason.UserDeclined:
|
|
||||||
cancellationMessage = _t("The request was declined on the other device.");
|
|
||||||
break;
|
|
||||||
case RendezvousFailureReason.OtherDeviceAlreadySignedIn:
|
|
||||||
cancellationMessage = _t("The other device is already signed in.");
|
|
||||||
break;
|
|
||||||
case RendezvousFailureReason.OtherDeviceNotSignedIn:
|
|
||||||
cancellationMessage = _t("The other device isn't signed in.");
|
|
||||||
break;
|
|
||||||
case RendezvousFailureReason.UserCancelled:
|
|
||||||
cancellationMessage = _t("The request was cancelled.");
|
|
||||||
break;
|
|
||||||
case RendezvousFailureReason.Unknown:
|
|
||||||
cancellationMessage = _t("An unexpected error occurred.");
|
|
||||||
break;
|
|
||||||
case RendezvousFailureReason.HomeserverLacksSupport:
|
|
||||||
cancellationMessage = _t("The homeserver doesn't support signing in another device.");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
cancellationMessage = _t("The request was cancelled.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
title = _t("Connection failed");
|
|
||||||
centreTitle = true;
|
|
||||||
titleIcon = <WarningBadge className="error" />;
|
|
||||||
backButton = false;
|
|
||||||
main = <p data-testid="cancellation-message">{ cancellationMessage }</p>;
|
|
||||||
buttons = <>
|
|
||||||
<AccessibleButton
|
|
||||||
kind="primary"
|
|
||||||
onClick={this.tryAgainClicked}
|
|
||||||
>
|
|
||||||
{ _t("Try again") }
|
|
||||||
</AccessibleButton>
|
|
||||||
{ this.cancelButton() }
|
|
||||||
</>;
|
|
||||||
break;
|
|
||||||
case Phase.Connected:
|
|
||||||
title = _t("Devices connected");
|
|
||||||
titleIcon = <DevicesIcon className="normal" />;
|
|
||||||
backButton = false;
|
|
||||||
main = <>
|
|
||||||
<p>{ _t("Check that the code below matches with your other device:") }</p>
|
|
||||||
<div className="mx_LoginWithQR_confirmationDigits">
|
|
||||||
{ this.state.confirmationDigits }
|
|
||||||
</div>
|
|
||||||
<div className="mx_LoginWithQR_confirmationAlert">
|
|
||||||
<div>
|
|
||||||
<InfoIcon />
|
|
||||||
</div>
|
|
||||||
<div>{ _t("By approving access for this device, it will have full access to your account.") }</div>
|
|
||||||
</div>
|
|
||||||
</>;
|
|
||||||
|
|
||||||
buttons = <>
|
|
||||||
<AccessibleButton
|
|
||||||
data-testid="decline-login-button"
|
|
||||||
kind="primary_outline"
|
|
||||||
onClick={this.declineClicked}
|
|
||||||
>
|
|
||||||
{ _t("Cancel") }
|
|
||||||
</AccessibleButton>
|
|
||||||
<AccessibleButton
|
|
||||||
data-testid="approve-login-button"
|
|
||||||
kind="primary"
|
|
||||||
onClick={this.approveLogin}
|
|
||||||
>
|
|
||||||
{ _t("Approve") }
|
|
||||||
</AccessibleButton>
|
|
||||||
</>;
|
|
||||||
break;
|
|
||||||
case Phase.ShowingQR:
|
|
||||||
title =_t("Sign in with QR code");
|
|
||||||
if (this.state.rendezvous) {
|
|
||||||
const code = <div className="mx_LoginWithQR_qrWrapper">
|
|
||||||
<QRCode data={[{ data: Buffer.from(this.state.rendezvous.code), mode: 'byte' }]} className="mx_QRCode" />
|
|
||||||
</div>;
|
|
||||||
main = <>
|
|
||||||
<p>{ _t("Scan the QR code below with your device that's signed out.") }</p>
|
|
||||||
<ol>
|
|
||||||
<li>{ _t("Start at the sign in screen") }</li>
|
|
||||||
<li>{ _t("Select 'Scan QR code'") }</li>
|
|
||||||
<li>{ _t("Review and approve the sign in") }</li>
|
|
||||||
</ol>
|
|
||||||
{ code }
|
|
||||||
</>;
|
|
||||||
} else {
|
|
||||||
main = this.simpleSpinner();
|
|
||||||
buttons = this.cancelButton();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Phase.Loading:
|
|
||||||
main = this.simpleSpinner();
|
|
||||||
break;
|
|
||||||
case Phase.Connecting:
|
|
||||||
main = this.simpleSpinner(_t("Connecting..."));
|
|
||||||
buttons = this.cancelButton();
|
|
||||||
break;
|
|
||||||
case Phase.WaitingForDevice:
|
|
||||||
main = this.simpleSpinner(_t("Waiting for device to sign in"));
|
|
||||||
buttons = this.cancelButton();
|
|
||||||
break;
|
|
||||||
case Phase.Verifying:
|
|
||||||
title = _t("Success");
|
|
||||||
centreTitle = true;
|
|
||||||
main = this.simpleSpinner(_t("Completing set up of your new device"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="login-with-qr" className="mx_LoginWithQR">
|
<LoginWithQRFlow
|
||||||
<div className={centreTitle ? "mx_LoginWithQR_centreTitle" : ""}>
|
onClick={this.onClick}
|
||||||
{ backButton ?
|
phase={this.state.phase}
|
||||||
<AccessibleButton
|
code={this.state.phase === Phase.ShowingQR ? this.state.rendezvous?.code : undefined}
|
||||||
data-testid="back-button"
|
confirmationDigits={this.state.phase === Phase.Connected ? this.state.confirmationDigits : undefined}
|
||||||
className="mx_LoginWithQR_BackButton"
|
failureReason={this.state.phase === Phase.Error ? this.state.failureReason : undefined}
|
||||||
onClick={this.onBackClick}
|
/>
|
||||||
title="Back"
|
|
||||||
>
|
|
||||||
<BackButtonIcon />
|
|
||||||
</AccessibleButton>
|
|
||||||
: null }
|
|
||||||
<h1>{ titleIcon }{ title }</h1>
|
|
||||||
</div>
|
|
||||||
<div className="mx_LoginWithQR_main">
|
|
||||||
{ main }
|
|
||||||
</div>
|
|
||||||
<div className="mx_LoginWithQR_buttons">
|
|
||||||
{ buttons }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
227
src/components/views/auth/LoginWithQRFlow.tsx
Normal file
227
src/components/views/auth/LoginWithQRFlow.tsx
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous';
|
||||||
|
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import QRCode from '../elements/QRCode';
|
||||||
|
import Spinner from '../elements/Spinner';
|
||||||
|
import { Icon as BackButtonIcon } from "../../../../res/img/element-icons/back.svg";
|
||||||
|
import { Icon as DevicesIcon } from "../../../../res/img/element-icons/devices.svg";
|
||||||
|
import { Icon as WarningBadge } from "../../../../res/img/element-icons/warning-badge.svg";
|
||||||
|
import { Icon as InfoIcon } from "../../../../res/img/element-icons/i.svg";
|
||||||
|
import { Click, Phase } from './LoginWithQR';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
phase: Phase;
|
||||||
|
code?: string;
|
||||||
|
onClick(type: Click): Promise<void>;
|
||||||
|
failureReason?: RendezvousFailureReason;
|
||||||
|
confirmationDigits?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component that implements the UI for sign in and E2EE set up with a QR code.
|
||||||
|
*
|
||||||
|
* This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906
|
||||||
|
*/
|
||||||
|
export default class LoginWithQRFlow extends React.Component<IProps> {
|
||||||
|
public constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleClick = (type: Click) => {
|
||||||
|
return async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await this.props.onClick(type);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
private cancelButton = () => <AccessibleButton
|
||||||
|
data-testid="cancel-button"
|
||||||
|
kind="primary_outline"
|
||||||
|
onClick={this.handleClick(Click.Cancel)}
|
||||||
|
>
|
||||||
|
{ _t("Cancel") }
|
||||||
|
</AccessibleButton>;
|
||||||
|
|
||||||
|
private simpleSpinner = (description?: string): JSX.Element => {
|
||||||
|
return <div className="mx_LoginWithQR_spinner">
|
||||||
|
<div>
|
||||||
|
<Spinner />
|
||||||
|
{ description && <p>{ description }</p> }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
let title = '';
|
||||||
|
let titleIcon: JSX.Element | undefined;
|
||||||
|
let main: JSX.Element | undefined;
|
||||||
|
let buttons: JSX.Element | undefined;
|
||||||
|
let backButton = true;
|
||||||
|
let cancellationMessage: string | undefined;
|
||||||
|
let centreTitle = false;
|
||||||
|
|
||||||
|
switch (this.props.phase) {
|
||||||
|
case Phase.Error:
|
||||||
|
switch (this.props.failureReason) {
|
||||||
|
case RendezvousFailureReason.Expired:
|
||||||
|
cancellationMessage = _t("The linking wasn't completed in the required time.");
|
||||||
|
break;
|
||||||
|
case RendezvousFailureReason.InvalidCode:
|
||||||
|
cancellationMessage = _t("The scanned code is invalid.");
|
||||||
|
break;
|
||||||
|
case RendezvousFailureReason.UnsupportedAlgorithm:
|
||||||
|
cancellationMessage = _t("Linking with this device is not supported.");
|
||||||
|
break;
|
||||||
|
case RendezvousFailureReason.UserDeclined:
|
||||||
|
cancellationMessage = _t("The request was declined on the other device.");
|
||||||
|
break;
|
||||||
|
case RendezvousFailureReason.OtherDeviceAlreadySignedIn:
|
||||||
|
cancellationMessage = _t("The other device is already signed in.");
|
||||||
|
break;
|
||||||
|
case RendezvousFailureReason.OtherDeviceNotSignedIn:
|
||||||
|
cancellationMessage = _t("The other device isn't signed in.");
|
||||||
|
break;
|
||||||
|
case RendezvousFailureReason.UserCancelled:
|
||||||
|
cancellationMessage = _t("The request was cancelled.");
|
||||||
|
break;
|
||||||
|
case RendezvousFailureReason.Unknown:
|
||||||
|
cancellationMessage = _t("An unexpected error occurred.");
|
||||||
|
break;
|
||||||
|
case RendezvousFailureReason.HomeserverLacksSupport:
|
||||||
|
cancellationMessage = _t("The homeserver doesn't support signing in another device.");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
cancellationMessage = _t("The request was cancelled.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
title = _t("Connection failed");
|
||||||
|
centreTitle = true;
|
||||||
|
titleIcon = <WarningBadge className="error" />;
|
||||||
|
backButton = false;
|
||||||
|
main = <p data-testid="cancellation-message">{ cancellationMessage }</p>;
|
||||||
|
buttons = <>
|
||||||
|
<AccessibleButton
|
||||||
|
data-testid="try-again-button"
|
||||||
|
kind="primary"
|
||||||
|
onClick={this.handleClick(Click.TryAgain)}
|
||||||
|
>
|
||||||
|
{ _t("Try again") }
|
||||||
|
</AccessibleButton>
|
||||||
|
{ this.cancelButton() }
|
||||||
|
</>;
|
||||||
|
break;
|
||||||
|
case Phase.Connected:
|
||||||
|
title = _t("Devices connected");
|
||||||
|
titleIcon = <DevicesIcon className="normal" />;
|
||||||
|
backButton = false;
|
||||||
|
main = <>
|
||||||
|
<p>{ _t("Check that the code below matches with your other device:") }</p>
|
||||||
|
<div className="mx_LoginWithQR_confirmationDigits">
|
||||||
|
{ this.props.confirmationDigits }
|
||||||
|
</div>
|
||||||
|
<div className="mx_LoginWithQR_confirmationAlert">
|
||||||
|
<div>
|
||||||
|
<InfoIcon />
|
||||||
|
</div>
|
||||||
|
<div>{ _t("By approving access for this device, it will have full access to your account.") }</div>
|
||||||
|
</div>
|
||||||
|
</>;
|
||||||
|
|
||||||
|
buttons = <>
|
||||||
|
<AccessibleButton
|
||||||
|
data-testid="decline-login-button"
|
||||||
|
kind="primary_outline"
|
||||||
|
onClick={this.handleClick(Click.Decline)}
|
||||||
|
>
|
||||||
|
{ _t("Cancel") }
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton
|
||||||
|
data-testid="approve-login-button"
|
||||||
|
kind="primary"
|
||||||
|
onClick={this.handleClick(Click.Approve)}
|
||||||
|
>
|
||||||
|
{ _t("Approve") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</>;
|
||||||
|
break;
|
||||||
|
case Phase.ShowingQR:
|
||||||
|
title =_t("Sign in with QR code");
|
||||||
|
if (this.props.code) {
|
||||||
|
const code = <div className="mx_LoginWithQR_qrWrapper">
|
||||||
|
<QRCode data={[{ data: Buffer.from(this.props.code ?? ''), mode: 'byte' }]} className="mx_QRCode" />
|
||||||
|
</div>;
|
||||||
|
main = <>
|
||||||
|
<p>{ _t("Scan the QR code below with your device that's signed out.") }</p>
|
||||||
|
<ol>
|
||||||
|
<li>{ _t("Start at the sign in screen") }</li>
|
||||||
|
<li>{ _t("Select 'Scan QR code'") }</li>
|
||||||
|
<li>{ _t("Review and approve the sign in") }</li>
|
||||||
|
</ol>
|
||||||
|
{ code }
|
||||||
|
</>;
|
||||||
|
} else {
|
||||||
|
main = this.simpleSpinner();
|
||||||
|
buttons = this.cancelButton();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Phase.Loading:
|
||||||
|
main = this.simpleSpinner();
|
||||||
|
break;
|
||||||
|
case Phase.Connecting:
|
||||||
|
main = this.simpleSpinner(_t("Connecting..."));
|
||||||
|
buttons = this.cancelButton();
|
||||||
|
break;
|
||||||
|
case Phase.WaitingForDevice:
|
||||||
|
main = this.simpleSpinner(_t("Waiting for device to sign in"));
|
||||||
|
buttons = this.cancelButton();
|
||||||
|
break;
|
||||||
|
case Phase.Verifying:
|
||||||
|
title = _t("Success");
|
||||||
|
centreTitle = true;
|
||||||
|
main = this.simpleSpinner(_t("Completing set up of your new device"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid="login-with-qr" className="mx_LoginWithQR">
|
||||||
|
<div className={centreTitle ? "mx_LoginWithQR_centreTitle" : ""}>
|
||||||
|
{ backButton ?
|
||||||
|
<AccessibleButton
|
||||||
|
data-testid="back-button"
|
||||||
|
className="mx_LoginWithQR_BackButton"
|
||||||
|
onClick={this.handleClick(Click.Back)}
|
||||||
|
title="Back"
|
||||||
|
>
|
||||||
|
<BackButtonIcon />
|
||||||
|
</AccessibleButton>
|
||||||
|
: null }
|
||||||
|
<h1>{ titleIcon }{ title }</h1>
|
||||||
|
</div>
|
||||||
|
<div className="mx_LoginWithQR_main">
|
||||||
|
{ main }
|
||||||
|
</div>
|
||||||
|
<div className="mx_LoginWithQR_buttons">
|
||||||
|
{ buttons }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,7 +32,7 @@ export default class LoginWithQRSection extends React.Component<IProps> {
|
||||||
super(props);
|
super(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element | null {
|
||||||
const msc3882Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3882'];
|
const msc3882Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3882'];
|
||||||
const msc3886Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3886'];
|
const msc3886Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3886'];
|
||||||
|
|
||||||
|
|
36
test/components/views/elements/QRCode-test.tsx
Normal file
36
test/components/views/elements/QRCode-test.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render, waitFor, cleanup } from "@testing-library/react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import QRCode from "../../../../src/components/views/elements/QRCode";
|
||||||
|
|
||||||
|
describe("<QRCode />", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a QR with defaults", async () => {
|
||||||
|
const { container, getAllByAltText } = render(<QRCode data="asd" />);
|
||||||
|
await waitFor(() => getAllByAltText('QR Code').length === 1);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a QR with high error correction level", async () => {
|
||||||
|
const { container, getAllByAltText } = render(<QRCode data="asd" errorCorrectionLevel="high" />);
|
||||||
|
await waitFor(() => getAllByAltText('QR Code').length === 1);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,29 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<QRCode /> renders a QR with defaults 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_QRCode"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="QR Code"
|
||||||
|
class="mx_VerificationQRCode"
|
||||||
|
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHQAAAB0CAYAAABUmhYnAAAAAklEQVR4AewaftIAAALASURBVO3BQW7kQAwEwSxC//9yro88NSBIM2sTjIg/WGMUa5RijVKsUYo1SrFGKdYoxRqlWKMUa5RijVKsUYo1SrFGKdYoxRrl4qEkfJPKSRI6lS4JJypdEr5J5YlijVKsUYo1ysXLVN6UhCeScKJyh8qbkvCmYo1SrFGKNcrFhyXhDpU7knCi0iWhS0KnckcS7lD5pGKNUqxRijXKxR+n0iXhDpVJijVKsUYp1igXw6h0SehUuiR0Kn9ZsUYp1ijFGuXiw1T+J5VPUvlNijVKsUYp1igXL0vCNyWhU+mS0Kk8kYTfrFijFGuUYo1y8ZDKb5KETqVLwh0qf0mxRinWKMUa5eKhJNyh0iXhk5JwotIl4UTljiR0Kp9UrFGKNUqxRrl4SKVLQqfSJaFT6ZLQqXyTSpeEO5LQqZwkoVN5olijFGuUYo1y8VASOpUTlTuScKLSJaFTOUlCp9KpnCThjiR8UrFGKdYoxRrl4j9LQqfypiR0Kp3KSRI6lU7lDpVPKtYoxRqlWKNcfFgSnkhCp3KHykkSOpU7ktCp3JGETuWJYo1SrFGKNUr8wR+WhCdUuiR0Kl0SOpU7knCi8kSxRinWKMUa5eKhJHyTyolKl4QnkvBEEr6pWKMUa5RijXLxMpU3JeEJlS4JXRJOVLokdEk4UTlJwpuKNUqxRinWKBcfloQ7VL5JpUtCl4QTlZMkdCqfVKxRijVKsUa5+ONUuiR0Kp3KiUqXhE7liSR0Km8q1ijFGqVYo1z8cUk4SUKn0iWhU+lUTpLQqXQqJ0noVJ4o1ijFGqVYo1x8mMo3qXRJOFE5SUKn0qmcJOFE5U3FGqVYoxRrlIuXJeE3UTlJQqdyRxI6lTuS0Kk8UaxRijVKsUaJP1hjFGuUYo1SrFGKNUqxRinWKMUapVijFGuUYo1SrFGKNUqxRinWKMUa5R93wRT2jE+pTAAAAABJRU5ErkJggg=="
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<QRCode /> renders a QR with high error correction level 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_QRCode"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="QR Code"
|
||||||
|
class="mx_VerificationQRCode"
|
||||||
|
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHQAAAB0CAYAAABUmhYnAAAAAklEQVR4AewaftIAAAKwSURBVO3BQY6YUAwFwW6L+1/5ZZZefQkBk8RylfnBGqNYoxRrlGKNUqxRijVKsUYp1ijFGqVYoxRrlGKNUqxRijVKsUYp1igXD6n8piQ8oXKShE7lNyXhiWKNUqxRijXKxcuS8CaVE5UuCZ3KSRLuSMKbVN5UrFGKNUqxRrn4mModSXhC5SQJnUqXhDtU7kjCl4o1SrFGKdYoF8MkoVM5ScIkxRqlWKMUa5SLYVS6JHQqJ0n4nxVrlGKNUqxRLj6WhN+UhE6lS0Kn8kQS/iXFGqVYoxRrlIuXqUym8i8r1ijFGqVYo1w8lIS/KQmdypuS8D8p1ijFGqVYo1x8TKVLwonKE0noVDqVLgmdSpeETuVNSXhTsUYp1ijFGsX84EUqXRJOVLokvEnlS0m4Q+UkCU8Ua5RijVKsUcwPPqTypST8JpWTJNyh0iXhiWKNUqxRijXKxUMqdyShUzlJQqfSqXRJ6FS6JLwpCZ1Kl4ROpUvCm4o1SrFGKdYo5gcfUumScKJyRxI6lTuS0Kl0SehU7khCp3KShCeKNUqxRinWKBcfS8IdSbhD5Y4kdCpdEjqVLgknKidJ6FTeVKxRijVKsUa5eEjlNyXhjiR0Kr9JpUvCl4o1SrFGKdYoFy9LwptU7kjCSRI6lU6lS8KJSpeEE5UvFWuUYo1SrFEuPqZyRxLuSEKn0iWhU+mS8EQSOpW/qVijFGuUYo1yMZzKHSpdEjqVLgl3JOFLxRqlWKMUa5SL/5zKl1ROVE6S0KmcJOGJYo1SrFGKNcrFx5LwpSR0Kl0STlS6JJyodEnoVDqVLglfKtYoxRqlWKNcvEzlX6JykoQTlROVJ1S6JDxRrFGKNUqxRjE/WGMUa5RijVKsUYo1SrFGKdYoxRqlWKMUa5RijVKsUYo1SrFGKdYoxRrlD3iZAgP0EjHgAAAAAElFTkSuQmCC"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -14,22 +14,25 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
import { cleanup, render, waitFor } from '@testing-library/react';
|
||||||
import { mocked } from 'jest-mock';
|
import { mocked } from 'jest-mock';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MSC3886SimpleHttpRendezvousTransport } from 'matrix-js-sdk/src/rendezvous/transports';
|
|
||||||
import { MSC3906Rendezvous, RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous';
|
import { MSC3906Rendezvous, RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous';
|
||||||
|
|
||||||
import LoginWithQR, { Mode } from '../../../../../src/components/views/auth/LoginWithQR';
|
import LoginWithQR, { Click, Mode, Phase } from '../../../../../src/components/views/auth/LoginWithQR';
|
||||||
import type { MatrixClient } from 'matrix-js-sdk/src/matrix';
|
import type { MatrixClient } from 'matrix-js-sdk/src/matrix';
|
||||||
import { flushPromisesWithFakeTimers } from '../../../../test-utils';
|
|
||||||
|
|
||||||
jest.useFakeTimers();
|
|
||||||
|
|
||||||
jest.mock('matrix-js-sdk/src/rendezvous');
|
jest.mock('matrix-js-sdk/src/rendezvous');
|
||||||
jest.mock('matrix-js-sdk/src/rendezvous/transports');
|
jest.mock('matrix-js-sdk/src/rendezvous/transports');
|
||||||
jest.mock('matrix-js-sdk/src/rendezvous/channels');
|
jest.mock('matrix-js-sdk/src/rendezvous/channels');
|
||||||
|
|
||||||
|
const mockedFlow = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('../../../../../src/components/views/auth/LoginWithQRFlow', () => (props) => {
|
||||||
|
mockedFlow(props);
|
||||||
|
return <div />;
|
||||||
|
});
|
||||||
|
|
||||||
function makeClient() {
|
function makeClient() {
|
||||||
return mocked({
|
return mocked({
|
||||||
getUser: jest.fn(),
|
getUser: jest.fn(),
|
||||||
|
@ -50,248 +53,252 @@ function makeClient() {
|
||||||
} as unknown as MatrixClient);
|
} as unknown as MatrixClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unresolvedPromise<T>(): Promise<T> {
|
||||||
|
return new Promise(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
describe('<LoginWithQR />', () => {
|
describe('<LoginWithQR />', () => {
|
||||||
const client = makeClient();
|
let client = makeClient();
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
mode: Mode.Show,
|
mode: Mode.Show,
|
||||||
onFinished: jest.fn(),
|
onFinished: jest.fn(),
|
||||||
};
|
};
|
||||||
const mockConfirmationDigits = 'mock-confirmation-digits';
|
const mockConfirmationDigits = 'mock-confirmation-digits';
|
||||||
|
const mockRendezvousCode = 'mock-rendezvous-code';
|
||||||
const newDeviceId = 'new-device-id';
|
const newDeviceId = 'new-device-id';
|
||||||
|
|
||||||
const getComponent = (props: { client: MatrixClient, onFinished?: () => void }) =>
|
const getComponent = (props: { client: MatrixClient, onFinished?: () => void }) =>
|
||||||
(<LoginWithQR {...defaultProps} {...props} />);
|
(<React.StrictMode><LoginWithQR {...defaultProps} {...props} /></React.StrictMode>);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
mockedFlow.mockReset();
|
||||||
jest.spyOn(MSC3906Rendezvous.prototype, 'generateCode').mockRestore();
|
jest.resetAllMocks();
|
||||||
|
jest.spyOn(MSC3906Rendezvous.prototype, 'generateCode').mockResolvedValue();
|
||||||
|
// @ts-ignore
|
||||||
|
// workaround for https://github.com/facebook/jest/issues/9675
|
||||||
|
MSC3906Rendezvous.prototype.code = mockRendezvousCode;
|
||||||
jest.spyOn(MSC3906Rendezvous.prototype, 'cancel').mockResolvedValue();
|
jest.spyOn(MSC3906Rendezvous.prototype, 'cancel').mockResolvedValue();
|
||||||
jest.spyOn(MSC3906Rendezvous.prototype, 'declineLoginOnExistingDevice').mockResolvedValue();
|
|
||||||
jest.spyOn(MSC3906Rendezvous.prototype, 'startAfterShowingCode').mockResolvedValue(mockConfirmationDigits);
|
jest.spyOn(MSC3906Rendezvous.prototype, 'startAfterShowingCode').mockResolvedValue(mockConfirmationDigits);
|
||||||
|
jest.spyOn(MSC3906Rendezvous.prototype, 'declineLoginOnExistingDevice').mockResolvedValue();
|
||||||
jest.spyOn(MSC3906Rendezvous.prototype, 'approveLoginOnExistingDevice').mockResolvedValue(newDeviceId);
|
jest.spyOn(MSC3906Rendezvous.prototype, 'approveLoginOnExistingDevice').mockResolvedValue(newDeviceId);
|
||||||
|
jest.spyOn(MSC3906Rendezvous.prototype, 'verifyNewDeviceOnExistingDevice').mockResolvedValue(undefined);
|
||||||
client.requestLoginToken.mockResolvedValue({
|
client.requestLoginToken.mockResolvedValue({
|
||||||
login_token: 'token',
|
login_token: 'token',
|
||||||
expires_in: 1000,
|
expires_in: 1000,
|
||||||
});
|
});
|
||||||
// @ts-ignore
|
|
||||||
client.crypto = undefined;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('no content in case of no support', async () => {
|
afterEach(() => {
|
||||||
|
client = makeClient();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
jest.useRealTimers();
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no homeserver support', async () => {
|
||||||
// simulate no support
|
// simulate no support
|
||||||
jest.spyOn(MSC3906Rendezvous.prototype, 'generateCode').mockRejectedValue('');
|
jest.spyOn(MSC3906Rendezvous.prototype, 'generateCode').mockRejectedValue('');
|
||||||
const { container } = render(getComponent({ client }));
|
render(getComponent({ client }));
|
||||||
await waitFor(() => screen.getAllByTestId('cancellation-message').length === 1);
|
await waitFor(() =>
|
||||||
expect(container).toMatchSnapshot();
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||||
});
|
phase: Phase.Error,
|
||||||
|
failureReason: RendezvousFailureReason.HomeserverLacksSupport,
|
||||||
it('renders spinner while generating code', async () => {
|
onClick: expect.any(Function),
|
||||||
const { container } = render(getComponent({ client }));
|
}),
|
||||||
expect(container).toMatchSnapshot();
|
);
|
||||||
});
|
|
||||||
|
|
||||||
it('cancels rendezvous after user goes back', async () => {
|
|
||||||
const { getByTestId } = render(getComponent({ client }));
|
|
||||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||||
// @ts-ignore assign to private prop
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||||
rendezvous.code = 'rendezvous-code';
|
});
|
||||||
|
|
||||||
// flush generate code promise
|
test('failed to connect', async () => {
|
||||||
await flushPromisesWithFakeTimers();
|
jest.spyOn(MSC3906Rendezvous.prototype, 'startAfterShowingCode').mockRejectedValue('');
|
||||||
|
render(getComponent({ client }));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||||
|
phase: Phase.Error,
|
||||||
|
failureReason: RendezvousFailureReason.Unknown,
|
||||||
|
onClick: expect.any(Function),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||||
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||||
|
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
fireEvent.click(getByTestId('back-button'));
|
test('render QR then cancel and try again', async () => {
|
||||||
|
const onFinished = jest.fn();
|
||||||
|
jest.spyOn(MSC3906Rendezvous.prototype, 'startAfterShowingCode').mockImplementation(() => unresolvedPromise());
|
||||||
|
render(getComponent({ client, onFinished }));
|
||||||
|
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||||
|
|
||||||
// wait for cancel
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||||
await flushPromisesWithFakeTimers();
|
phase: Phase.ShowingQR,
|
||||||
|
})));
|
||||||
|
// display QR code
|
||||||
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||||
|
phase: Phase.ShowingQR,
|
||||||
|
code: mockRendezvousCode,
|
||||||
|
onClick: expect.any(Function),
|
||||||
|
});
|
||||||
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||||
|
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// cancel
|
||||||
|
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||||
|
await onClick(Click.Cancel);
|
||||||
|
expect(onFinished).toHaveBeenCalledWith(false);
|
||||||
|
expect(rendezvous.cancel).toHaveBeenCalledWith(RendezvousFailureReason.UserCancelled);
|
||||||
|
|
||||||
|
// try again
|
||||||
|
onClick(Click.TryAgain);
|
||||||
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||||
|
phase: Phase.ShowingQR,
|
||||||
|
})));
|
||||||
|
// display QR code
|
||||||
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||||
|
phase: Phase.ShowingQR,
|
||||||
|
code: mockRendezvousCode,
|
||||||
|
onClick: expect.any(Function),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('render QR then back', async () => {
|
||||||
|
const onFinished = jest.fn();
|
||||||
|
jest.spyOn(MSC3906Rendezvous.prototype, 'startAfterShowingCode').mockReturnValue(unresolvedPromise());
|
||||||
|
render(getComponent({ client, onFinished }));
|
||||||
|
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||||
|
|
||||||
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||||
|
phase: Phase.ShowingQR,
|
||||||
|
})));
|
||||||
|
// display QR code
|
||||||
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||||
|
phase: Phase.ShowingQR,
|
||||||
|
code: mockRendezvousCode,
|
||||||
|
onClick: expect.any(Function),
|
||||||
|
});
|
||||||
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||||
|
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// back
|
||||||
|
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||||
|
await onClick(Click.Back);
|
||||||
|
expect(onFinished).toHaveBeenCalledWith(false);
|
||||||
expect(rendezvous.cancel).toHaveBeenCalledWith(RendezvousFailureReason.UserCancelled);
|
expect(rendezvous.cancel).toHaveBeenCalledWith(RendezvousFailureReason.UserCancelled);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays qr code after it is created', async () => {
|
test('render QR then decline', async () => {
|
||||||
const { container, getByText } = render(getComponent({ client }));
|
const onFinished = jest.fn();
|
||||||
|
render(getComponent({ client, onFinished }));
|
||||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||||
// @ts-ignore assign to private prop
|
|
||||||
rendezvous.code = 'rendezvous-code';
|
|
||||||
|
|
||||||
await flushPromisesWithFakeTimers();
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||||
|
phase: Phase.Connected,
|
||||||
|
})));
|
||||||
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||||
|
phase: Phase.Connected,
|
||||||
|
confirmationDigits: mockConfirmationDigits,
|
||||||
|
onClick: expect.any(Function),
|
||||||
|
});
|
||||||
|
|
||||||
|
// decline
|
||||||
|
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||||
|
await onClick(Click.Decline);
|
||||||
|
expect(onFinished).toHaveBeenCalledWith(false);
|
||||||
|
|
||||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||||
expect(getByText('Sign in with QR code')).toBeTruthy();
|
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays confirmation digits after connected to rendezvous', async () => {
|
|
||||||
const { container, getByText } = render(getComponent({ client }));
|
|
||||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
|
||||||
// @ts-ignore assign to private prop
|
|
||||||
rendezvous.code = 'rendezvous-code';
|
|
||||||
|
|
||||||
// flush generate code promise
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
// flush waiting for connection promise
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
expect(getByText(mockConfirmationDigits)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays unknown error if connection to rendezvous fails', async () => {
|
|
||||||
const { container } = render(getComponent({ client }));
|
|
||||||
expect(MSC3886SimpleHttpRendezvousTransport).toHaveBeenCalledWith({
|
|
||||||
onFailure: expect.any(Function),
|
|
||||||
client,
|
|
||||||
});
|
|
||||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
|
||||||
// @ts-ignore assign to private prop
|
|
||||||
rendezvous.code = 'rendezvous-code';
|
|
||||||
mocked(rendezvous).startAfterShowingCode.mockRejectedValue('oups');
|
|
||||||
|
|
||||||
// flush generate code promise
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
// flush waiting for connection promise
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('declines login', async () => {
|
|
||||||
const { getByTestId } = render(getComponent({ client }));
|
|
||||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
|
||||||
// @ts-ignore assign to private prop
|
|
||||||
rendezvous.code = 'rendezvous-code';
|
|
||||||
|
|
||||||
// flush generate code promise
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
// flush waiting for connection promise
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
|
|
||||||
fireEvent.click(getByTestId('decline-login-button'));
|
|
||||||
|
|
||||||
expect(rendezvous.declineLoginOnExistingDevice).toHaveBeenCalled();
|
expect(rendezvous.declineLoginOnExistingDevice).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays error when approving login fails', async () => {
|
test('approve - no crypto', async () => {
|
||||||
const { container, getByTestId } = render(getComponent({ client }));
|
// @ts-ignore
|
||||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
client.crypto = undefined;
|
||||||
// @ts-ignore assign to private prop
|
|
||||||
rendezvous.code = 'rendezvous-code';
|
|
||||||
client.requestLoginToken.mockRejectedValue('oups');
|
|
||||||
|
|
||||||
// flush generate code promise
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
// flush waiting for connection promise
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
|
|
||||||
fireEvent.click(getByTestId('approve-login-button'));
|
|
||||||
|
|
||||||
expect(client.requestLoginToken).toHaveBeenCalled();
|
|
||||||
// flush token request promise
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('approves login and waits for new device', async () => {
|
|
||||||
const { container, getByTestId, getByText } = render(getComponent({ client }));
|
|
||||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
|
||||||
// @ts-ignore assign to private prop
|
|
||||||
rendezvous.code = 'rendezvous-code';
|
|
||||||
|
|
||||||
// flush generate code promise
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
// flush waiting for connection promise
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
|
|
||||||
fireEvent.click(getByTestId('approve-login-button'));
|
|
||||||
|
|
||||||
expect(client.requestLoginToken).toHaveBeenCalled();
|
|
||||||
// flush token request promise
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
|
|
||||||
expect(getByText('Waiting for device to sign in')).toBeTruthy();
|
|
||||||
expect(container).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not continue with verification when user denies login', async () => {
|
|
||||||
const onFinished = jest.fn();
|
const onFinished = jest.fn();
|
||||||
const { getByTestId } = render(getComponent({ client, onFinished }));
|
// jest.spyOn(MSC3906Rendezvous.prototype, 'approveLoginOnExistingDevice').mockReturnValue(unresolvedPromise());
|
||||||
|
render(getComponent({ client, onFinished }));
|
||||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||||
// @ts-ignore assign to private prop
|
|
||||||
rendezvous.code = 'rendezvous-code';
|
|
||||||
// no device id returned => user denied
|
|
||||||
mocked(rendezvous).approveLoginOnExistingDevice.mockReturnValue(undefined);
|
|
||||||
|
|
||||||
// flush generate code promise
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||||
await flushPromisesWithFakeTimers();
|
phase: Phase.Connected,
|
||||||
// flush waiting for connection promise
|
})));
|
||||||
await flushPromisesWithFakeTimers();
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||||
|
phase: Phase.Connected,
|
||||||
|
confirmationDigits: mockConfirmationDigits,
|
||||||
|
onClick: expect.any(Function),
|
||||||
|
});
|
||||||
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||||
|
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||||
|
|
||||||
fireEvent.click(getByTestId('approve-login-button'));
|
// approve
|
||||||
|
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||||
|
await onClick(Click.Approve);
|
||||||
|
|
||||||
// flush token request promise
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||||
await flushPromisesWithFakeTimers();
|
phase: Phase.WaitingForDevice,
|
||||||
await flushPromisesWithFakeTimers();
|
})));
|
||||||
|
|
||||||
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalled();
|
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith('token');
|
||||||
|
|
||||||
await flushPromisesWithFakeTimers();
|
expect(onFinished).toHaveBeenCalledWith(true);
|
||||||
expect(onFinished).not.toHaveBeenCalled();
|
|
||||||
expect(rendezvous.verifyNewDeviceOnExistingDevice).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('waits for device approval on existing device and finishes when crypto is not setup', async () => {
|
test('approve + verifying', async () => {
|
||||||
const { getByTestId } = render(getComponent({ client }));
|
const onFinished = jest.fn();
|
||||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
|
||||||
// @ts-ignore assign to private prop
|
|
||||||
rendezvous.code = 'rendezvous-code';
|
|
||||||
|
|
||||||
// flush generate code promise
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
// flush waiting for connection promise
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
|
|
||||||
fireEvent.click(getByTestId('approve-login-button'));
|
|
||||||
|
|
||||||
// flush token request promise
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
|
|
||||||
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalled();
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
expect(defaultProps.onFinished).toHaveBeenCalledWith(true);
|
|
||||||
// didnt attempt verification
|
|
||||||
expect(rendezvous.verifyNewDeviceOnExistingDevice).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('waits for device approval on existing device and verifies device', async () => {
|
|
||||||
const { getByTestId } = render(getComponent({ client }));
|
|
||||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
|
||||||
// @ts-ignore assign to private prop
|
|
||||||
rendezvous.code = 'rendezvous-code';
|
|
||||||
// we just check for presence of crypto
|
|
||||||
// pretend it is set up
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
client.crypto = {};
|
client.crypto = {};
|
||||||
|
jest.spyOn(MSC3906Rendezvous.prototype, 'verifyNewDeviceOnExistingDevice')
|
||||||
|
.mockImplementation(() => unresolvedPromise());
|
||||||
|
render(getComponent({ client, onFinished }));
|
||||||
|
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||||
|
|
||||||
// flush generate code promise
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||||
await flushPromisesWithFakeTimers();
|
phase: Phase.Connected,
|
||||||
// flush waiting for connection promise
|
})));
|
||||||
await flushPromisesWithFakeTimers();
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||||
|
phase: Phase.Connected,
|
||||||
|
confirmationDigits: mockConfirmationDigits,
|
||||||
|
onClick: expect.any(Function),
|
||||||
|
});
|
||||||
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||||
|
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||||
|
|
||||||
fireEvent.click(getByTestId('approve-login-button'));
|
// approve
|
||||||
|
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||||
|
onClick(Click.Approve);
|
||||||
|
|
||||||
// flush token request promise
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||||
await flushPromisesWithFakeTimers();
|
phase: Phase.Verifying,
|
||||||
await flushPromisesWithFakeTimers();
|
})));
|
||||||
|
|
||||||
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalled();
|
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith('token');
|
||||||
// flush login approval
|
|
||||||
await flushPromisesWithFakeTimers();
|
|
||||||
expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled();
|
expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled();
|
||||||
// flush verification
|
// expect(onFinished).toHaveBeenCalledWith(true);
|
||||||
await flushPromisesWithFakeTimers();
|
});
|
||||||
expect(defaultProps.onFinished).toHaveBeenCalledWith(true);
|
|
||||||
|
test('approve + verify', async () => {
|
||||||
|
const onFinished = jest.fn();
|
||||||
|
// @ts-ignore
|
||||||
|
client.crypto = {};
|
||||||
|
render(getComponent({ client, onFinished }));
|
||||||
|
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||||
|
|
||||||
|
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||||
|
phase: Phase.Connected,
|
||||||
|
})));
|
||||||
|
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||||
|
phase: Phase.Connected,
|
||||||
|
confirmationDigits: mockConfirmationDigits,
|
||||||
|
onClick: expect.any(Function),
|
||||||
|
});
|
||||||
|
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||||
|
expect(rendezvous.startAfterShowingCode).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// approve
|
||||||
|
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||||
|
await onClick(Click.Approve);
|
||||||
|
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith('token');
|
||||||
|
expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled();
|
||||||
|
expect(onFinished).toHaveBeenCalledWith(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
116
test/components/views/settings/devices/LoginWithQRFlow-test.tsx
Normal file
116
test/components/views/settings/devices/LoginWithQRFlow-test.tsx
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { RendezvousFailureReason } from 'matrix-js-sdk/src/rendezvous';
|
||||||
|
|
||||||
|
import LoginWithQRFlow from '../../../../../src/components/views/auth/LoginWithQRFlow';
|
||||||
|
import { Click, Phase } from '../../../../../src/components/views/auth/LoginWithQR';
|
||||||
|
|
||||||
|
describe('<LoginWithQRFlow />', () => {
|
||||||
|
const onClick = jest.fn();
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
onClick,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getComponent = (props: {
|
||||||
|
phase: Phase;
|
||||||
|
onClick?: () => Promise<void>;
|
||||||
|
failureReason?: RendezvousFailureReason;
|
||||||
|
code?: string;
|
||||||
|
confirmationDigits?: string;
|
||||||
|
}) =>
|
||||||
|
(<LoginWithQRFlow {...defaultProps} {...props} />);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
onClick.mockReset();
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders spinner while loading', async () => {
|
||||||
|
const { container } = render(getComponent({ phase: Phase.Loading }));
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders spinner whilst QR generating', async () => {
|
||||||
|
const { container } = render(getComponent({ phase: Phase.ShowingQR }));
|
||||||
|
expect(screen.getAllByTestId('cancel-button')).toHaveLength(1);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
fireEvent.click(screen.getByTestId('cancel-button'));
|
||||||
|
expect(onClick).toHaveBeenCalledWith(Click.Cancel);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders QR code', async () => {
|
||||||
|
const { container } = render(getComponent({ phase: Phase.ShowingQR, code: 'mock-code' }));
|
||||||
|
// QR code is rendered async so we wait for it:
|
||||||
|
await waitFor(() => screen.getAllByAltText('QR Code').length === 1);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders spinner while connecting', async () => {
|
||||||
|
const { container } = render(getComponent({ phase: Phase.Connecting }));
|
||||||
|
expect(screen.getAllByTestId('cancel-button')).toHaveLength(1);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
fireEvent.click(screen.getByTestId('cancel-button'));
|
||||||
|
expect(onClick).toHaveBeenCalledWith(Click.Cancel);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders code when connected', async () => {
|
||||||
|
const { container } = render(getComponent({ phase: Phase.Connected, confirmationDigits: 'mock-digits' }));
|
||||||
|
expect(screen.getAllByText('mock-digits')).toHaveLength(1);
|
||||||
|
expect(screen.getAllByTestId('decline-login-button')).toHaveLength(1);
|
||||||
|
expect(screen.getAllByTestId('approve-login-button')).toHaveLength(1);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
fireEvent.click(screen.getByTestId('decline-login-button'));
|
||||||
|
expect(onClick).toHaveBeenCalledWith(Click.Decline);
|
||||||
|
fireEvent.click(screen.getByTestId('approve-login-button'));
|
||||||
|
expect(onClick).toHaveBeenCalledWith(Click.Approve);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders spinner while signing in', async () => {
|
||||||
|
const { container } = render(getComponent({ phase: Phase.WaitingForDevice }));
|
||||||
|
expect(screen.getAllByTestId('cancel-button')).toHaveLength(1);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
fireEvent.click(screen.getByTestId('cancel-button'));
|
||||||
|
expect(onClick).toHaveBeenCalledWith(Click.Cancel);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders spinner while verifying', async () => {
|
||||||
|
const { container } = render(getComponent({ phase: Phase.Verifying }));
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('errors', () => {
|
||||||
|
for (const failureReason of Object.values(RendezvousFailureReason)) {
|
||||||
|
it(`renders ${failureReason}`, async () => {
|
||||||
|
const { container } = render(getComponent({
|
||||||
|
phase: Phase.Error,
|
||||||
|
failureReason,
|
||||||
|
}));
|
||||||
|
expect(screen.getAllByTestId('cancellation-message')).toHaveLength(1);
|
||||||
|
expect(screen.getAllByTestId('try-again-button')).toHaveLength(1);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
fireEvent.click(screen.getByTestId('try-again-button'));
|
||||||
|
expect(onClick).toHaveBeenCalledWith(Click.TryAgain);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -56,7 +56,7 @@ describe('<LoginWithQRSection />', () => {
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
onShowQr: () => {},
|
onShowQr: () => {},
|
||||||
versions: undefined,
|
versions: makeVersions({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const getComponent = (props = {}) =>
|
const getComponent = (props = {}) =>
|
||||||
|
|
|
@ -1,377 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`<LoginWithQR /> approves login and waits for new device 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR"
|
|
||||||
data-testid="login-with-qr"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class=""
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_LoginWithQR_BackButton"
|
|
||||||
data-testid="back-button"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
title="Back"
|
|
||||||
>
|
|
||||||
<div />
|
|
||||||
</div>
|
|
||||||
<h1 />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_main"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_spinner"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_Spinner"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-label="Loading..."
|
|
||||||
class="mx_Spinner_icon"
|
|
||||||
data-testid="spinner"
|
|
||||||
role="progressbar"
|
|
||||||
style="width: 32px; height: 32px;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Waiting for device to sign in
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_buttons"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<LoginWithQR /> displays confirmation digits after connected to rendezvous 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR"
|
|
||||||
data-testid="login-with-qr"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class=""
|
|
||||||
>
|
|
||||||
<h1>
|
|
||||||
<div
|
|
||||||
class="normal"
|
|
||||||
/>
|
|
||||||
Devices connected
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_main"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Check that the code below matches with your other device:
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_confirmationDigits"
|
|
||||||
>
|
|
||||||
mock-confirmation-digits
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_confirmationAlert"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
By approving access for this device, it will have full access to your account.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_buttons"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
|
||||||
data-testid="decline-login-button"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
|
||||||
data-testid="approve-login-button"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Approve
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<LoginWithQR /> displays error when approving login fails 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR"
|
|
||||||
data-testid="login-with-qr"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_centreTitle"
|
|
||||||
>
|
|
||||||
<h1>
|
|
||||||
<div
|
|
||||||
class="error"
|
|
||||||
/>
|
|
||||||
Connection failed
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_main"
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
data-testid="cancellation-message"
|
|
||||||
>
|
|
||||||
An unexpected error occurred.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_buttons"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Try again
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<LoginWithQR /> displays qr code after it is created 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR"
|
|
||||||
data-testid="login-with-qr"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class=""
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_LoginWithQR_BackButton"
|
|
||||||
data-testid="back-button"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
title="Back"
|
|
||||||
>
|
|
||||||
<div />
|
|
||||||
</div>
|
|
||||||
<h1>
|
|
||||||
Sign in with QR code
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_main"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Scan the QR code below with your device that's signed out.
|
|
||||||
</p>
|
|
||||||
<ol>
|
|
||||||
<li>
|
|
||||||
Start at the sign in screen
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Select 'Scan QR code'
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Review and approve the sign in
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_qrWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_QRCode mx_QRCode"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_Spinner"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-label="Loading..."
|
|
||||||
class="mx_Spinner_icon"
|
|
||||||
data-testid="spinner"
|
|
||||||
role="progressbar"
|
|
||||||
style="width: 32px; height: 32px;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_buttons"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<LoginWithQR /> displays unknown error if connection to rendezvous fails 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR"
|
|
||||||
data-testid="login-with-qr"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_centreTitle"
|
|
||||||
>
|
|
||||||
<h1>
|
|
||||||
<div
|
|
||||||
class="error"
|
|
||||||
/>
|
|
||||||
Connection failed
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_main"
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
data-testid="cancellation-message"
|
|
||||||
>
|
|
||||||
An unexpected error occurred.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_buttons"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Try again
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<LoginWithQR /> no content in case of no support 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR"
|
|
||||||
data-testid="login-with-qr"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_centreTitle"
|
|
||||||
>
|
|
||||||
<h1>
|
|
||||||
<div
|
|
||||||
class="error"
|
|
||||||
/>
|
|
||||||
Connection failed
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_main"
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
data-testid="cancellation-message"
|
|
||||||
>
|
|
||||||
The homeserver doesn't support signing in another device.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_buttons"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Try again
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<LoginWithQR /> renders spinner while generating code 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR"
|
|
||||||
data-testid="login-with-qr"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class=""
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_AccessibleButton mx_LoginWithQR_BackButton"
|
|
||||||
data-testid="back-button"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
title="Back"
|
|
||||||
>
|
|
||||||
<div />
|
|
||||||
</div>
|
|
||||||
<h1 />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_main"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_spinner"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="mx_Spinner"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-label="Loading..."
|
|
||||||
class="mx_Spinner_icon"
|
|
||||||
data-testid="spinner"
|
|
||||||
role="progressbar"
|
|
||||||
style="width: 32px; height: 32px;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_LoginWithQR_buttons"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
|
@ -0,0 +1,948 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> errors renders data_mismatch 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_centreTitle"
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
<div
|
||||||
|
class="error"
|
||||||
|
/>
|
||||||
|
Connection failed
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
data-testid="cancellation-message"
|
||||||
|
>
|
||||||
|
The request was cancelled.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
data-testid="try-again-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||||
|
data-testid="cancel-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> errors renders expired 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_centreTitle"
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
<div
|
||||||
|
class="error"
|
||||||
|
/>
|
||||||
|
Connection failed
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
data-testid="cancellation-message"
|
||||||
|
>
|
||||||
|
The linking wasn't completed in the required time.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
data-testid="try-again-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||||
|
data-testid="cancel-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> errors renders homeserver_lacks_support 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_centreTitle"
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
<div
|
||||||
|
class="error"
|
||||||
|
/>
|
||||||
|
Connection failed
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
data-testid="cancellation-message"
|
||||||
|
>
|
||||||
|
The homeserver doesn't support signing in another device.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
data-testid="try-again-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||||
|
data-testid="cancel-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> errors renders invalid_code 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_centreTitle"
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
<div
|
||||||
|
class="error"
|
||||||
|
/>
|
||||||
|
Connection failed
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
data-testid="cancellation-message"
|
||||||
|
>
|
||||||
|
The scanned code is invalid.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
data-testid="try-again-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||||
|
data-testid="cancel-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> errors renders other_device_already_signed_in 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_centreTitle"
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
<div
|
||||||
|
class="error"
|
||||||
|
/>
|
||||||
|
Connection failed
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
data-testid="cancellation-message"
|
||||||
|
>
|
||||||
|
The other device is already signed in.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
data-testid="try-again-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||||
|
data-testid="cancel-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> errors renders other_device_not_signed_in 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_centreTitle"
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
<div
|
||||||
|
class="error"
|
||||||
|
/>
|
||||||
|
Connection failed
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
data-testid="cancellation-message"
|
||||||
|
>
|
||||||
|
The other device isn't signed in.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
data-testid="try-again-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||||
|
data-testid="cancel-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> errors renders unknown 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_centreTitle"
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
<div
|
||||||
|
class="error"
|
||||||
|
/>
|
||||||
|
Connection failed
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
data-testid="cancellation-message"
|
||||||
|
>
|
||||||
|
An unexpected error occurred.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
data-testid="try-again-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||||
|
data-testid="cancel-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> errors renders unsupported_algorithm 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_centreTitle"
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
<div
|
||||||
|
class="error"
|
||||||
|
/>
|
||||||
|
Connection failed
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
data-testid="cancellation-message"
|
||||||
|
>
|
||||||
|
Linking with this device is not supported.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
data-testid="try-again-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||||
|
data-testid="cancel-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> errors renders unsupported_transport 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_centreTitle"
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
<div
|
||||||
|
class="error"
|
||||||
|
/>
|
||||||
|
Connection failed
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
data-testid="cancellation-message"
|
||||||
|
>
|
||||||
|
The request was cancelled.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
data-testid="try-again-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||||
|
data-testid="cancel-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> errors renders user_cancelled 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_centreTitle"
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
<div
|
||||||
|
class="error"
|
||||||
|
/>
|
||||||
|
Connection failed
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
data-testid="cancellation-message"
|
||||||
|
>
|
||||||
|
The request was cancelled.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
data-testid="try-again-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||||
|
data-testid="cancel-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> errors renders user_declined 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_centreTitle"
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
<div
|
||||||
|
class="error"
|
||||||
|
/>
|
||||||
|
Connection failed
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
data-testid="cancellation-message"
|
||||||
|
>
|
||||||
|
The request was declined on the other device.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
data-testid="try-again-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||||
|
data-testid="cancel-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> renders QR code 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class=""
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_LoginWithQR_BackButton"
|
||||||
|
data-testid="back-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
title="Back"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
<h1>
|
||||||
|
Sign in with QR code
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Scan the QR code below with your device that's signed out.
|
||||||
|
</p>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
Start at the sign in screen
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Select 'Scan QR code'
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Review and approve the sign in
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_qrWrapper"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_QRCode mx_QRCode"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="QR Code"
|
||||||
|
class="mx_VerificationQRCode"
|
||||||
|
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHQAAAB0CAYAAABUmhYnAAAAAklEQVR4AewaftIAAAKxSURBVO3BQW7kQAwEwSxC//9yro88NSBI4/UQjIg/WGMUa5RijVKsUYo1SrFGKdYoxRqlWKMUa5RijVKsUYo1SrFGKdYoxRrl4qEk/CaVkyR0Kl0STlS6JPwmlSeKNUqxRinWKBcvU3lTEp5IwonKHSpvSsKbijVKsUYp1igXH5aEO1TuSMKJSpeELgmdyh1JuEPlk4o1SrFGKdYoF19OpUvCHSqTFGuUYo1SrFEuhlHpktCpdEnoVL5ZsUYp1ijFGuXiw1T+J5VPUvlLijVKsUYp1igXL0vCb0pCp9IloVN5Igl/WbFGKdYoxRrl4iGVvyQJnUqXhDtUvkmxRinWKMUa5eKhJNyh0iXhk5JwotIl4UTljiR0Kp9UrFGKNUqxRok/eFESOpUuCZ3KN0nCiUqXhDtUnijWKMUapVijxB88kIQ3qXRJeELlJAmdSpeEJ1S6JJyoPFGsUYo1SrFGuXiZSpeETqVLwonKSRKeUDlROUlCp3Ki8knFGqVYoxRrlPiDB5LwhMpJEjqVO5LQqXRJ6FROktCp3JGETuVNxRqlWKMUa5T4gy+WhE7lJAmdyh1JuEOlS8KJyhPFGqVYoxRrlIuHkvCbVO5Iwh1J+GbFGqVYoxRrlIuXqbwpCXckoVPpktAl4UTlJAmdyonKJxVrlGKNUqxRLj4sCXeovCkJJyonSehU/rJijVKsUYo1ysWXUzlJQqfyRBJOknCHypuKNUqxRinWKBdfLgmdSqdykoROpVPpknCHykkSOpUnijVKsUYp1igXH6byPyWhU+lUTpLQqZwk4SQJncqbijVKsUYp1igXL0vC/5SEO5LQqZwkoVPpVO5IQqfyRLFGKdYoxRol/mCNUaxRijVKsUYp1ijFGqVYoxRrlGKNUqxRijVKsUYp1ijFGqVYoxRrlH+MbAvtLaAtKAAAAABJRU5ErkJggg=="
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> renders code when connected 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class=""
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
<div
|
||||||
|
class="normal"
|
||||||
|
/>
|
||||||
|
Devices connected
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Check that the code below matches with your other device:
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_confirmationDigits"
|
||||||
|
>
|
||||||
|
mock-digits
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_confirmationAlert"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
By approving access for this device, it will have full access to your account.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||||
|
data-testid="decline-login-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||||
|
data-testid="approve-login-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Approve
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> renders spinner while connecting 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class=""
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_LoginWithQR_BackButton"
|
||||||
|
data-testid="back-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
title="Back"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
<h1>
|
||||||
|
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_spinner"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_Spinner"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Loading..."
|
||||||
|
class="mx_Spinner_icon"
|
||||||
|
data-testid="spinner"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Connecting...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||||
|
data-testid="cancel-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> renders spinner while loading 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class=""
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_LoginWithQR_BackButton"
|
||||||
|
data-testid="back-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
title="Back"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
<h1>
|
||||||
|
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_spinner"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_Spinner"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Loading..."
|
||||||
|
class="mx_Spinner_icon"
|
||||||
|
data-testid="spinner"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> renders spinner while signing in 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class=""
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_LoginWithQR_BackButton"
|
||||||
|
data-testid="back-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
title="Back"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
<h1>
|
||||||
|
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_spinner"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_Spinner"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Loading..."
|
||||||
|
class="mx_Spinner_icon"
|
||||||
|
data-testid="spinner"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Waiting for device to sign in
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||||
|
data-testid="cancel-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> renders spinner while verifying 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_centreTitle"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_LoginWithQR_BackButton"
|
||||||
|
data-testid="back-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
title="Back"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
<h1>
|
||||||
|
Success
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_spinner"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_Spinner"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Loading..."
|
||||||
|
class="mx_Spinner_icon"
|
||||||
|
data-testid="spinner"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Completing set up of your new device
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<LoginWithQRFlow /> renders spinner whilst QR generating 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR"
|
||||||
|
data-testid="login-with-qr"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class=""
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_LoginWithQR_BackButton"
|
||||||
|
data-testid="back-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
title="Back"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
<h1>
|
||||||
|
Sign in with QR code
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_main"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_spinner"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_Spinner"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Loading..."
|
||||||
|
class="mx_Spinner_icon"
|
||||||
|
data-testid="spinner"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_LoginWithQR_buttons"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||||
|
data-testid="cancel-button"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
Loading…
Reference in a new issue