mirror of
https://github.com/vector-im/element-web.git
synced 2025-11-09 04:31:15 +01:00
* Test that VerificationRequestDialog updates when phase changes * Change the title of VerificationRequestDialog when a request is cancelled Part of implementing https://github.com/element-hq/element-meta/issues/2898 but split out as a separate change because it involves making VerificationRequestDialog listen for changes to the verificationRequest so it can update based on changes to phase.
341 lines
12 KiB
TypeScript
341 lines
12 KiB
TypeScript
/*
|
|
Copyright 2025 New Vector Ltd.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import React from "react";
|
|
import { act, render, screen } from "jest-matrix-react";
|
|
import { TypedEventEmitter, User } from "matrix-js-sdk/src/matrix";
|
|
import {
|
|
type ShowSasCallbacks,
|
|
VerificationPhase,
|
|
type Verifier,
|
|
type VerificationRequest,
|
|
type ShowQrCodeCallbacks,
|
|
VerificationRequestEvent,
|
|
type VerificationRequestEventHandlerMap,
|
|
} from "matrix-js-sdk/src/crypto-api";
|
|
import { VerificationMethod } from "matrix-js-sdk/src/types";
|
|
|
|
import { stubClient } from "../../../../test-utils";
|
|
import VerificationRequestDialog from "../../../../../src/components/views/dialogs/VerificationRequestDialog";
|
|
|
|
describe("VerificationRequestDialog", () => {
|
|
function renderComponent(phase: VerificationPhase, method?: "emoji" | "qr"): ReturnType<typeof render> {
|
|
const member = User.createUser("@alice:example.org", stubClient());
|
|
const request = createRequest(phase, method);
|
|
|
|
return render(
|
|
<VerificationRequestDialog onFinished={jest.fn()} member={member} verificationRequest={request} />,
|
|
);
|
|
}
|
|
|
|
it("Initially, asks how you would like to verify this device", async () => {
|
|
const dialog = renderComponent(VerificationPhase.Ready);
|
|
|
|
expect(screen.getByRole("heading", { name: "Verify other device" })).toBeInTheDocument();
|
|
expect(screen.getByText("Verify this device by completing one of the following:")).toBeInTheDocument();
|
|
|
|
expect(dialog.asFragment()).toMatchSnapshot();
|
|
});
|
|
|
|
it("After we started verification here, says we are waiting for the other device", async () => {
|
|
const dialog = renderComponent(VerificationPhase.Requested);
|
|
|
|
expect(screen.getByRole("heading", { name: "Verify other device" })).toBeInTheDocument();
|
|
|
|
expect(
|
|
screen.getByText("To proceed, please accept the verification request on your other device."),
|
|
).toBeInTheDocument();
|
|
|
|
expect(dialog.asFragment()).toMatchSnapshot();
|
|
});
|
|
|
|
it("When other device accepted emoji, displays emojis and asks for confirmation", async () => {
|
|
const dialog = renderComponent(VerificationPhase.Started, "emoji");
|
|
|
|
expect(screen.getByRole("heading", { name: "Verify other device" })).toBeInTheDocument();
|
|
|
|
expect(
|
|
screen.getByText("Confirm the emoji below are displayed on both devices, in the same order:"),
|
|
).toBeInTheDocument();
|
|
|
|
expect(dialog.asFragment()).toMatchSnapshot();
|
|
});
|
|
|
|
it("After scanning QR, shows confirmation dialog", async () => {
|
|
const dialog = renderComponent(VerificationPhase.Started, "qr");
|
|
|
|
expect(screen.getByRole("heading", { name: "Verify other device" })).toBeInTheDocument();
|
|
expect(screen.getByRole("heading", { name: "Verify by scanning" })).toBeInTheDocument();
|
|
|
|
expect(screen.getByText("Almost there! Is your other device showing the same shield?")).toBeInTheDocument();
|
|
|
|
expect(dialog.asFragment()).toMatchSnapshot();
|
|
});
|
|
|
|
it("Shows a successful message if verification finished normally", async () => {
|
|
const dialog = renderComponent(VerificationPhase.Done);
|
|
|
|
expect(screen.getByRole("heading", { name: "Verify other device" })).toBeInTheDocument();
|
|
expect(screen.getByText("You've successfully verified your device!")).toBeInTheDocument();
|
|
|
|
expect(dialog.asFragment()).toMatchSnapshot();
|
|
});
|
|
|
|
it("Shows a failure message if verification was cancelled", async () => {
|
|
const dialog = renderComponent(VerificationPhase.Cancelled);
|
|
|
|
expect(screen.getByRole("heading", { name: "Verification failed" })).toBeInTheDocument();
|
|
expect(screen.getByRole("heading", { name: "Verification cancelled" })).toBeInTheDocument();
|
|
|
|
expect(
|
|
screen.getByText(
|
|
"You cancelled verification on your other device. Start verification again from the notification.",
|
|
),
|
|
).toBeInTheDocument();
|
|
|
|
expect(dialog.asFragment()).toMatchSnapshot();
|
|
});
|
|
|
|
it("Renders correctly if the request is supplied later via a promise", async () => {
|
|
// Given we supply a promise of a request instead of a request
|
|
const member = User.createUser("@alice:example.org", stubClient());
|
|
const requestPromise = Promise.resolve(createRequest(VerificationPhase.Cancelled));
|
|
|
|
// When we render the dialog
|
|
render(
|
|
<VerificationRequestDialog
|
|
onFinished={jest.fn()}
|
|
member={member}
|
|
verificationRequestPromise={requestPromise}
|
|
/>,
|
|
);
|
|
|
|
// And wait for the component to mount, the promise to resolve and the component state to update
|
|
await act(async () => await new Promise(process.nextTick));
|
|
|
|
// Then it renders the resolved information
|
|
expect(screen.getByRole("heading", { name: "Verification failed" })).toBeInTheDocument();
|
|
expect(screen.getByRole("heading", { name: "Verification cancelled" })).toBeInTheDocument();
|
|
|
|
expect(
|
|
screen.getByText(
|
|
"You cancelled verification on your other device. Start verification again from the notification.",
|
|
),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("Renders the later promise request if both immediate and promise are supplied", async () => {
|
|
// Given we supply a promise of a request as well as a request
|
|
const member = User.createUser("@alice:example.org", stubClient());
|
|
const request = createRequest(VerificationPhase.Ready);
|
|
const requestPromise = Promise.resolve(createRequest(VerificationPhase.Cancelled));
|
|
|
|
// When we render the dialog
|
|
render(
|
|
<VerificationRequestDialog
|
|
onFinished={jest.fn()}
|
|
member={member}
|
|
verificationRequest={request}
|
|
verificationRequestPromise={requestPromise}
|
|
/>,
|
|
);
|
|
|
|
// And wait for the component to mount, the promise to resolve and the component state to update
|
|
await act(async () => await new Promise(process.nextTick));
|
|
|
|
// Then it renders the information from the request in the promise
|
|
expect(screen.getByRole("heading", { name: "Verification failed" })).toBeInTheDocument();
|
|
expect(screen.getByRole("heading", { name: "Verification cancelled" })).toBeInTheDocument();
|
|
|
|
expect(
|
|
screen.getByText(
|
|
"You cancelled verification on your other device. Start verification again from the notification.",
|
|
),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("Changes the dialog contents when the request changes phase", async () => {
|
|
// Given we rendered the component with a phase of Unsent
|
|
const member = User.createUser("@alice:example.org", stubClient());
|
|
const request = createRequest(VerificationPhase.Unsent);
|
|
|
|
render(<VerificationRequestDialog onFinished={jest.fn()} member={member} verificationRequest={request} />);
|
|
|
|
// When I cancel the request (which changes phase and emits a Changed event)
|
|
await act(async () => await request.cancel());
|
|
|
|
// Then the dialog is updated to reflect that
|
|
expect(screen.getByRole("heading", { name: "Verification failed" })).toBeInTheDocument();
|
|
expect(screen.getByRole("heading", { name: "Verification cancelled" })).toBeInTheDocument();
|
|
|
|
expect(
|
|
screen.getByText(
|
|
"You cancelled verification on your other device. Start verification again from the notification.",
|
|
),
|
|
).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
function createRequest(phase: VerificationPhase, method?: "emoji" | "qr"): MockVerificationRequest {
|
|
let verifier = undefined;
|
|
let chosenMethod = null;
|
|
|
|
switch (method) {
|
|
case "emoji":
|
|
chosenMethod = VerificationMethod.Sas;
|
|
verifier = createEmojiVerifier();
|
|
break;
|
|
case "qr":
|
|
chosenMethod = VerificationMethod.Reciprocate;
|
|
verifier = createQrVerifier();
|
|
break;
|
|
}
|
|
|
|
return new MockVerificationRequest(phase, verifier, chosenMethod);
|
|
}
|
|
|
|
function createEmojiVerifier(): Verifier {
|
|
const showSasCallbacks = {
|
|
sas: {
|
|
emoji: [
|
|
// Example set of emoji to display.
|
|
["🐶", "Dog"],
|
|
["🐱", "Cat"],
|
|
],
|
|
},
|
|
} as ShowSasCallbacks;
|
|
|
|
return {
|
|
getShowSasCallbacks: jest.fn().mockReturnValue(showSasCallbacks),
|
|
getReciprocateQrCodeCallbacks: jest.fn(),
|
|
on: jest.fn(),
|
|
off: jest.fn(),
|
|
verify: jest.fn(),
|
|
} as unknown as Verifier;
|
|
}
|
|
|
|
function createQrVerifier(): Verifier {
|
|
const reciprocateQrCodeCallbacks = {
|
|
confirm: jest.fn(),
|
|
cancel: jest.fn(),
|
|
} as ShowQrCodeCallbacks;
|
|
|
|
return {
|
|
getShowSasCallbacks: jest.fn(),
|
|
getReciprocateQrCodeCallbacks: jest.fn().mockReturnValue(reciprocateQrCodeCallbacks),
|
|
on: jest.fn(),
|
|
off: jest.fn(),
|
|
verify: jest.fn(),
|
|
} as unknown as Verifier;
|
|
}
|
|
|
|
class MockVerificationRequest
|
|
extends TypedEventEmitter<VerificationRequestEvent, VerificationRequestEventHandlerMap>
|
|
implements VerificationRequest
|
|
{
|
|
phase_: VerificationPhase;
|
|
verifier_: Verifier | undefined;
|
|
chosenMethod_: string | null;
|
|
|
|
constructor(phase: VerificationPhase, verifier: Verifier | undefined, chosenMethod: string | null) {
|
|
super();
|
|
this.phase_ = phase;
|
|
this.verifier_ = verifier;
|
|
this.chosenMethod_ = chosenMethod;
|
|
}
|
|
|
|
get phase(): VerificationPhase {
|
|
return this.phase_;
|
|
}
|
|
|
|
get isSelfVerification(): boolean {
|
|
// So far we are only testing verification of our own devices
|
|
return true;
|
|
}
|
|
|
|
get initiatedByMe(): boolean {
|
|
// So far we are only testing verification started by this device
|
|
return true;
|
|
}
|
|
|
|
otherPartySupportsMethod(): boolean {
|
|
// This makes both emoji and QR verification options appear
|
|
return true;
|
|
}
|
|
|
|
get verifier(): Verifier | undefined {
|
|
return this.verifier_;
|
|
}
|
|
|
|
get chosenMethod(): string | null {
|
|
return this.chosenMethod_;
|
|
}
|
|
|
|
async cancel(): Promise<void> {
|
|
this.phase_ = VerificationPhase.Cancelled;
|
|
this.emit(VerificationRequestEvent.Change);
|
|
}
|
|
|
|
get transactionId(): string | undefined {
|
|
return undefined;
|
|
}
|
|
|
|
get roomId(): string | undefined {
|
|
return undefined;
|
|
}
|
|
|
|
get otherUserId(): string {
|
|
return "otheruser";
|
|
}
|
|
|
|
get otherDeviceId(): string | undefined {
|
|
return undefined;
|
|
}
|
|
|
|
get pending(): boolean {
|
|
return false;
|
|
}
|
|
|
|
get accepting(): boolean {
|
|
return false;
|
|
}
|
|
|
|
get declining(): boolean {
|
|
return false;
|
|
}
|
|
|
|
get timeout(): number | null {
|
|
return null;
|
|
}
|
|
|
|
get methods(): string[] {
|
|
return [];
|
|
}
|
|
|
|
async accept(): Promise<void> {}
|
|
|
|
startVerification(_method: string): Promise<Verifier> {
|
|
throw new Error("Method not implemented.");
|
|
}
|
|
|
|
scanQRCode(_qrCodeData: Uint8ClampedArray): Promise<Verifier> {
|
|
throw new Error("Method not implemented.");
|
|
}
|
|
|
|
async generateQRCode(): Promise<Uint8ClampedArray | undefined> {
|
|
return undefined;
|
|
}
|
|
|
|
get cancellationCode(): string | null {
|
|
return null;
|
|
}
|
|
|
|
get cancellingUserId(): string | undefined {
|
|
return "otheruser";
|
|
}
|
|
}
|