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 { _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 LoginWithQRFlow from './LoginWithQRFlow';
|
||||
|
||||
/**
|
||||
* 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",
|
||||
}
|
||||
|
||||
enum Phase {
|
||||
export enum Phase {
|
||||
Loading,
|
||||
ShowingQR,
|
||||
Connecting,
|
||||
|
@ -51,6 +45,14 @@ enum Phase {
|
|||
Error,
|
||||
}
|
||||
|
||||
export enum Click {
|
||||
Cancel,
|
||||
Decline,
|
||||
Approve,
|
||||
TryAgain,
|
||||
Back,
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
client: MatrixClient;
|
||||
mode: Mode;
|
||||
|
@ -68,7 +70,7 @@ interface IState {
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
@ -138,6 +140,7 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
|||
this.props.onFinished(true);
|
||||
return;
|
||||
}
|
||||
this.setState({ phase: Phase.Verifying });
|
||||
await this.state.rendezvous.verifyNewDeviceOnExistingDevice();
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
|
@ -197,200 +200,41 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
private cancelClicked = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
|
||||
this.reset();
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private declineClicked = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
await this.state.rendezvous?.declineLoginOnExistingDevice();
|
||||
this.reset();
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private tryAgainClicked = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
this.reset();
|
||||
await this.updateMode(this.props.mode);
|
||||
};
|
||||
|
||||
private onBackClick = async () => {
|
||||
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
|
||||
|
||||
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>;
|
||||
private onClick = async (type: Click) => {
|
||||
switch (type) {
|
||||
case Click.Cancel:
|
||||
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
|
||||
this.reset();
|
||||
this.props.onFinished(false);
|
||||
break;
|
||||
case Click.Approve:
|
||||
await this.approveLogin();
|
||||
break;
|
||||
case Click.Decline:
|
||||
await this.state.rendezvous?.declineLoginOnExistingDevice();
|
||||
this.reset();
|
||||
this.props.onFinished(false);
|
||||
break;
|
||||
case Click.TryAgain:
|
||||
this.reset();
|
||||
await this.updateMode(this.props.mode);
|
||||
break;
|
||||
case Click.Back:
|
||||
await this.state.rendezvous?.cancel(RendezvousFailureReason.UserCancelled);
|
||||
this.props.onFinished(false);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<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.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>
|
||||
<LoginWithQRFlow
|
||||
onClick={this.onClick}
|
||||
phase={this.state.phase}
|
||||
code={this.state.phase === Phase.ShowingQR ? this.state.rendezvous?.code : undefined}
|
||||
confirmationDigits={this.state.phase === Phase.Connected ? this.state.confirmationDigits : undefined}
|
||||
failureReason={this.state.phase === Phase.Error ? this.state.failureReason : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
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);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
public render(): JSX.Element | null {
|
||||
const msc3882Supported = !!this.props.versions?.unstable_features?.['org.matrix.msc3882'];
|
||||
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.
|
||||
*/
|
||||
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { cleanup, render, waitFor } from '@testing-library/react';
|
||||
import { mocked } from 'jest-mock';
|
||||
import React from 'react';
|
||||
import { MSC3886SimpleHttpRendezvousTransport } from 'matrix-js-sdk/src/rendezvous/transports';
|
||||
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 { flushPromisesWithFakeTimers } from '../../../../test-utils';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
jest.mock('matrix-js-sdk/src/rendezvous');
|
||||
jest.mock('matrix-js-sdk/src/rendezvous/transports');
|
||||
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() {
|
||||
return mocked({
|
||||
getUser: jest.fn(),
|
||||
|
@ -50,248 +53,252 @@ function makeClient() {
|
|||
} as unknown as MatrixClient);
|
||||
}
|
||||
|
||||
function unresolvedPromise<T>(): Promise<T> {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
|
||||
describe('<LoginWithQR />', () => {
|
||||
const client = makeClient();
|
||||
let client = makeClient();
|
||||
const defaultProps = {
|
||||
mode: Mode.Show,
|
||||
onFinished: jest.fn(),
|
||||
};
|
||||
const mockConfirmationDigits = 'mock-confirmation-digits';
|
||||
const mockRendezvousCode = 'mock-rendezvous-code';
|
||||
const newDeviceId = 'new-device-id';
|
||||
|
||||
const getComponent = (props: { client: MatrixClient, onFinished?: () => void }) =>
|
||||
(<LoginWithQR {...defaultProps} {...props} />);
|
||||
(<React.StrictMode><LoginWithQR {...defaultProps} {...props} /></React.StrictMode>);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, 'generateCode').mockRestore();
|
||||
mockedFlow.mockReset();
|
||||
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, 'declineLoginOnExistingDevice').mockResolvedValue();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, 'startAfterShowingCode').mockResolvedValue(mockConfirmationDigits);
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, 'declineLoginOnExistingDevice').mockResolvedValue();
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, 'approveLoginOnExistingDevice').mockResolvedValue(newDeviceId);
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, 'verifyNewDeviceOnExistingDevice').mockResolvedValue(undefined);
|
||||
client.requestLoginToken.mockResolvedValue({
|
||||
login_token: 'token',
|
||||
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
|
||||
jest.spyOn(MSC3906Rendezvous.prototype, 'generateCode').mockRejectedValue('');
|
||||
const { container } = render(getComponent({ client }));
|
||||
await waitFor(() => screen.getAllByTestId('cancellation-message').length === 1);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders spinner while generating code', async () => {
|
||||
const { container } = render(getComponent({ client }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('cancels rendezvous after user goes back', async () => {
|
||||
const { getByTestId } = render(getComponent({ client }));
|
||||
render(getComponent({ client }));
|
||||
await waitFor(() =>
|
||||
expect(mockedFlow).toHaveBeenLastCalledWith({
|
||||
phase: Phase.Error,
|
||||
failureReason: RendezvousFailureReason.HomeserverLacksSupport,
|
||||
onClick: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
// @ts-ignore assign to private prop
|
||||
rendezvous.code = 'rendezvous-code';
|
||||
expect(rendezvous.generateCode).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// flush generate code promise
|
||||
await flushPromisesWithFakeTimers();
|
||||
test('failed to connect', async () => {
|
||||
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 flushPromisesWithFakeTimers();
|
||||
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();
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
it('displays qr code after it is created', async () => {
|
||||
const { container, getByText } = render(getComponent({ client }));
|
||||
test('render QR then decline', async () => {
|
||||
const onFinished = jest.fn();
|
||||
render(getComponent({ client, onFinished }));
|
||||
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(getByText('Sign in with QR code')).toBeTruthy();
|
||||
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.startAfterShowingCode).toHaveBeenCalled();
|
||||
expect(rendezvous.declineLoginOnExistingDevice).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('displays error when approving login fails', async () => {
|
||||
const { container, getByTestId } = render(getComponent({ client }));
|
||||
const rendezvous = mocked(MSC3906Rendezvous).mock.instances[0];
|
||||
// @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 () => {
|
||||
test('approve - no crypto', async () => {
|
||||
// @ts-ignore
|
||||
client.crypto = undefined;
|
||||
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];
|
||||
// @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 flushPromisesWithFakeTimers();
|
||||
// flush waiting for connection promise
|
||||
await flushPromisesWithFakeTimers();
|
||||
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();
|
||||
|
||||
fireEvent.click(getByTestId('approve-login-button'));
|
||||
// approve
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
await onClick(Click.Approve);
|
||||
|
||||
// flush token request promise
|
||||
await flushPromisesWithFakeTimers();
|
||||
await flushPromisesWithFakeTimers();
|
||||
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
phase: Phase.WaitingForDevice,
|
||||
})));
|
||||
|
||||
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalled();
|
||||
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith('token');
|
||||
|
||||
await flushPromisesWithFakeTimers();
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
expect(rendezvous.verifyNewDeviceOnExistingDevice).not.toHaveBeenCalled();
|
||||
expect(onFinished).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('waits for device approval on existing device and finishes when crypto is not setup', 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('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
|
||||
test('approve + verifying', async () => {
|
||||
const onFinished = jest.fn();
|
||||
// @ts-ignore
|
||||
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 flushPromisesWithFakeTimers();
|
||||
// flush waiting for connection promise
|
||||
await flushPromisesWithFakeTimers();
|
||||
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();
|
||||
|
||||
fireEvent.click(getByTestId('approve-login-button'));
|
||||
// approve
|
||||
const onClick = mockedFlow.mock.calls[0][0].onClick;
|
||||
onClick(Click.Approve);
|
||||
|
||||
// flush token request promise
|
||||
await flushPromisesWithFakeTimers();
|
||||
await flushPromisesWithFakeTimers();
|
||||
await waitFor(() => expect(mockedFlow).toHaveBeenLastCalledWith(expect.objectContaining({
|
||||
phase: Phase.Verifying,
|
||||
})));
|
||||
|
||||
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalled();
|
||||
// flush login approval
|
||||
await flushPromisesWithFakeTimers();
|
||||
expect(rendezvous.approveLoginOnExistingDevice).toHaveBeenCalledWith('token');
|
||||
expect(rendezvous.verifyNewDeviceOnExistingDevice).toHaveBeenCalled();
|
||||
// flush verification
|
||||
await flushPromisesWithFakeTimers();
|
||||
expect(defaultProps.onFinished).toHaveBeenCalledWith(true);
|
||||
// expect(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 = {
|
||||
onShowQr: () => {},
|
||||
versions: undefined,
|
||||
versions: makeVersions({}),
|
||||
};
|
||||
|
||||
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