Refactor + improve test coverage for QR login (#9525)

This commit is contained in:
Hugh Nimmo-Smith 2022-11-02 11:51:20 +01:00 committed by GitHub
parent 9096bd82d6
commit f05cc5d8f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 1604 additions and 774 deletions

View file

@ -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}
/>
);
}
}

View 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>
);
}
}

View file

@ -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'];

View 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();
});
});

View file

@ -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>
`;

View file

@ -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);
});
});

View 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);
});
}
});
});

View file

@ -56,7 +56,7 @@ describe('<LoginWithQRSection />', () => {
const defaultProps = {
onShowQr: () => {},
versions: undefined,
versions: makeVersions({}),
};
const getComponent = (props = {}) =>

View file

@ -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>
`;

View file

@ -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>
`;