Refactor MKeyVerificationRequest to shared view MVVM (#33461)
* Refactor key verification request to shared view * Fix prettier * add tests to pass coverage
This commit is contained in:
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 { type RenderResult, render } from "jest-matrix-react";
|
||||
import { type MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import MKeyVerificationRequest from "../../../../../src/components/views/messages/MKeyVerificationRequest";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { filterConsole } from "../../../../test-utils";
|
||||
|
||||
describe("MKeyVerificationRequest", () => {
|
||||
filterConsole(
|
||||
"The above error occurred in the <MKeyVerificationRequest> component",
|
||||
"Error: Attempting to render verification request without a client context!",
|
||||
"Error: Verification request did not include a sender!",
|
||||
"Error: Verification request did not include a room ID!",
|
||||
);
|
||||
|
||||
it("shows an error if not wrapped in a client context", () => {
|
||||
const event = new MatrixEvent({ type: "m.key.verification.request" });
|
||||
const { container } = renderEventNoClient(event);
|
||||
expect(container).toHaveTextContent("Can't load this message");
|
||||
});
|
||||
|
||||
it("shows an error if the event has no sender", () => {
|
||||
const { client } = setup();
|
||||
const event = new MatrixEvent({ type: "m.key.verification.request" });
|
||||
const { container } = renderEvent(client, event);
|
||||
expect(container).toHaveTextContent("Can't load this message");
|
||||
});
|
||||
|
||||
it("shows an error if the event has no room", () => {
|
||||
const { client } = setup();
|
||||
const event = new MatrixEvent({ type: "m.key.verification.request", sender: "@a:b.co" });
|
||||
const { container } = renderEvent(client, event);
|
||||
expect(container).toHaveTextContent("Can't load this message");
|
||||
});
|
||||
|
||||
it("displays a request from me", () => {
|
||||
const { client, myUserId } = setup();
|
||||
const event = new MatrixEvent({ type: "m.key.verification.request", sender: myUserId, room_id: "!x:y.co" });
|
||||
const { container } = renderEvent(client, event);
|
||||
expect(container).toHaveTextContent("You sent a verification request");
|
||||
});
|
||||
|
||||
it("displays a request from someone else to me", () => {
|
||||
const otherUserId = "@other:s.uk";
|
||||
const { client } = setup();
|
||||
const event = new MatrixEvent({ type: "m.key.verification.request", sender: otherUserId, room_id: "!x:y.co" });
|
||||
const { container } = renderEvent(client, event);
|
||||
expect(container).toHaveTextContent("other:s.uk wants to verify");
|
||||
});
|
||||
});
|
||||
|
||||
interface TestTileErrorBoundaryProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface TestTileErrorBoundaryState {
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
class TestTileErrorBoundary extends React.Component<TestTileErrorBoundaryProps, TestTileErrorBoundaryState> {
|
||||
public constructor(props: TestTileErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
public static getDerivedStateFromError(error: Error): Partial<TestTileErrorBoundaryState> {
|
||||
return { error };
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
if (this.state.error) {
|
||||
return "Can't load this message";
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function renderEventNoClient(event: MatrixEvent): RenderResult {
|
||||
return render(
|
||||
<TestTileErrorBoundary>
|
||||
<MKeyVerificationRequest mxEvent={event} />
|
||||
</TestTileErrorBoundary>,
|
||||
);
|
||||
}
|
||||
|
||||
function renderEvent(client: MatrixClient, event: MatrixEvent): RenderResult {
|
||||
return render(
|
||||
<TestTileErrorBoundary>
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<MKeyVerificationRequest mxEvent={event} />
|
||||
</MatrixClientContext.Provider>
|
||||
,
|
||||
</TestTileErrorBoundary>,
|
||||
);
|
||||
}
|
||||
|
||||
function setup(): { client: MatrixClient; myUserId: string } {
|
||||
const myUserId = "@me:s.co";
|
||||
|
||||
const client = {
|
||||
getSafeUserId: jest.fn().mockReturnValue(myUserId),
|
||||
getRoom: jest.fn(),
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
return { client, myUserId };
|
||||
}
|
||||
@@ -6,8 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { EventType, type MatrixClient, MatrixEvent, MsgType, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { EventType, type MatrixClient, MatrixEvent, MsgType, Room, type RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
JSONEventFactory,
|
||||
@@ -20,9 +22,25 @@ import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import { createTestClient, mkEvent } from "../../test-utils";
|
||||
import { TimelineRenderingType } from "../../../src/contexts/RoomContext";
|
||||
import { ModuleApi } from "../../../src/modules/Api";
|
||||
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
|
||||
|
||||
const roomId = "!room:example.com";
|
||||
|
||||
function makeVerificationRequestEvent({ sender, to }: { sender: string; to: string }): MatrixEvent {
|
||||
return mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: sender,
|
||||
room: roomId,
|
||||
content: {
|
||||
msgtype: MsgType.KeyVerificationRequest,
|
||||
from_device: "DEVICE",
|
||||
methods: ["m.sas.v1"],
|
||||
to,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("pickFactory", () => {
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
@@ -206,14 +224,30 @@ describe("pickFactory", () => {
|
||||
it("should return a MessageEventFactory for a UTD event", () => {
|
||||
expect(pickFactory(utdEvent, client, false)).toBe(MessageEventFactory);
|
||||
});
|
||||
|
||||
it("should not render key verification requests which do not involve the current user", () => {
|
||||
const event = makeVerificationRequestEvent({
|
||||
sender: "@alice:example.com",
|
||||
to: "@bob:example.com",
|
||||
});
|
||||
|
||||
expect(pickFactory(event, client, false)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderTile", () => {
|
||||
let client: MatrixClient;
|
||||
let originalRenderMessage: typeof ModuleApi.instance.customComponents.renderMessage;
|
||||
|
||||
beforeEach(() => {
|
||||
client = createTestClient();
|
||||
originalRenderMessage = ModuleApi.instance.customComponents.renderMessage;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ModuleApi.instance.customComponents.renderMessage = originalRenderMessage;
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("rendering a tile defers to the module API", () => {
|
||||
@@ -258,4 +292,75 @@ describe("renderTile", () => {
|
||||
mxEvent: messageEvent,
|
||||
});
|
||||
});
|
||||
|
||||
it("renders an incoming key verification request with the wrapped shared-components view", () => {
|
||||
const sender = "@alice:example.com";
|
||||
const room = new Room(roomId, client, client.getSafeUserId());
|
||||
jest.spyOn(room, "getMember").mockImplementation((userId: string) => {
|
||||
if (userId === sender) return { name: "Alice" } as RoomMember;
|
||||
return null;
|
||||
});
|
||||
mocked(client.getRoom).mockReturnValue(room);
|
||||
|
||||
const verificationRequestEvent = makeVerificationRequestEvent({
|
||||
sender,
|
||||
to: client.getUserId()!,
|
||||
});
|
||||
|
||||
const tile = renderTile(
|
||||
TimelineRenderingType.Room,
|
||||
{ mxEvent: verificationRequestEvent, showHiddenEvents: false },
|
||||
client,
|
||||
);
|
||||
if (!tile) throw new Error("Expected a key verification request tile");
|
||||
|
||||
render(React.createElement(MatrixClientContext.Provider, { value: client }, tile));
|
||||
|
||||
expect(screen.getByText("Alice wants to verify")).toBeInTheDocument();
|
||||
expect(screen.getByText("Alice (@alice:example.com)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders an outgoing key verification request with the wrapped shared-components view", () => {
|
||||
const recipient = "@alice:example.com";
|
||||
const room = new Room(roomId, client, client.getSafeUserId());
|
||||
jest.spyOn(room, "getMember").mockImplementation((userId: string) => {
|
||||
if (userId === recipient) return { name: "Alice" } as RoomMember;
|
||||
return null;
|
||||
});
|
||||
mocked(client.getRoom).mockReturnValue(room);
|
||||
|
||||
const verificationRequestEvent = makeVerificationRequestEvent({
|
||||
sender: client.getUserId()!,
|
||||
to: recipient,
|
||||
});
|
||||
|
||||
const tile = renderTile(
|
||||
TimelineRenderingType.Room,
|
||||
{ mxEvent: verificationRequestEvent, showHiddenEvents: false },
|
||||
client,
|
||||
);
|
||||
if (!tile) throw new Error("Expected a key verification request tile");
|
||||
|
||||
render(React.createElement(MatrixClientContext.Provider, { value: client }, tile));
|
||||
|
||||
expect(screen.getByText("You sent a verification request")).toBeInTheDocument();
|
||||
expect(screen.getByText("Alice (@alice:example.com)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("throws when a key verification request tile is rendered without a client context", () => {
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
const verificationRequestEvent = makeVerificationRequestEvent({
|
||||
sender: client.getUserId()!,
|
||||
to: "@alice:example.com",
|
||||
});
|
||||
|
||||
const tile = renderTile(
|
||||
TimelineRenderingType.Room,
|
||||
{ mxEvent: verificationRequestEvent, showHiddenEvents: false },
|
||||
client,
|
||||
);
|
||||
if (!tile) throw new Error("Expected a key verification request tile");
|
||||
|
||||
expect(() => render(tile)).toThrow("Attempting to render verification request without a client context!");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations 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 { type MatrixClient, MatrixEvent, type Room, type RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MKeyVerificationRequestViewModel } from "../../../src/viewmodels/room/timeline/event-tile/MKeyVerificationRequestViewModel";
|
||||
|
||||
describe("MKeyVerificationRequestViewModel", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const myUserId = "@me:example.org";
|
||||
let cli: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
room = {
|
||||
getMember: jest.fn(),
|
||||
} as unknown as Room;
|
||||
cli = {
|
||||
getSafeUserId: jest.fn().mockReturnValue(myUserId),
|
||||
getRoom: jest.fn().mockReturnValue(room),
|
||||
} as unknown as MatrixClient;
|
||||
});
|
||||
|
||||
const createEvent = (sender?: string, to = myUserId, eventRoomId: string | undefined = roomId): MatrixEvent =>
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
room_id: eventRoomId,
|
||||
sender,
|
||||
content: {
|
||||
msgtype: "m.key.verification.request",
|
||||
from_device: "DEVICE",
|
||||
methods: ["m.sas.v1"],
|
||||
to,
|
||||
},
|
||||
});
|
||||
|
||||
it("throws if the event has no sender", () => {
|
||||
expect(() => new MKeyVerificationRequestViewModel({ cli, mxEvent: createEvent() })).toThrow(
|
||||
"Verification request did not include a sender!",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws if the event has no room", () => {
|
||||
expect(
|
||||
() =>
|
||||
new MKeyVerificationRequestViewModel({
|
||||
cli,
|
||||
mxEvent: createEvent("@alice:example.org", myUserId, ""),
|
||||
}),
|
||||
).toThrow("Verification request did not include a room ID!");
|
||||
});
|
||||
|
||||
it("renders a request sent by me", () => {
|
||||
jest.mocked(room.getMember).mockReturnValue({
|
||||
name: "Alice",
|
||||
} as RoomMember);
|
||||
|
||||
const vm = new MKeyVerificationRequestViewModel({
|
||||
cli,
|
||||
mxEvent: createEvent(myUserId, "@alice:example.org"),
|
||||
});
|
||||
|
||||
expect(vm.getSnapshot()).toMatchObject({
|
||||
title: "You sent a verification request",
|
||||
subtitle: "Alice (@alice:example.org)",
|
||||
});
|
||||
});
|
||||
|
||||
it("renders a request sent by someone else", () => {
|
||||
jest.mocked(room.getMember).mockReturnValue({
|
||||
name: "Alice",
|
||||
} as RoomMember);
|
||||
|
||||
const vm = new MKeyVerificationRequestViewModel({
|
||||
cli,
|
||||
mxEvent: createEvent("@alice:example.org"),
|
||||
});
|
||||
|
||||
expect(vm.getSnapshot()).toMatchObject({
|
||||
title: "Alice wants to verify",
|
||||
subtitle: "Alice (@alice:example.org)",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to user ID when no room member is available", () => {
|
||||
const vm = new MKeyVerificationRequestViewModel({
|
||||
cli,
|
||||
mxEvent: createEvent("@alice:example.org"),
|
||||
});
|
||||
|
||||
expect(vm.getSnapshot()).toMatchObject({
|
||||
title: "@alice:example.org wants to verify",
|
||||
subtitle: "@alice:example.org",
|
||||
});
|
||||
});
|
||||
|
||||
it("updates the snapshot when the event changes", () => {
|
||||
const vm = new MKeyVerificationRequestViewModel({
|
||||
cli,
|
||||
mxEvent: createEvent("@alice:example.org"),
|
||||
});
|
||||
const listener = jest.fn();
|
||||
vm.subscribe(listener);
|
||||
|
||||
vm.setEvent(createEvent(myUserId, "@bob:example.org"));
|
||||
|
||||
expect(vm.getSnapshot()).toMatchObject({
|
||||
title: "You sent a verification request",
|
||||
subtitle: "@bob:example.org",
|
||||
});
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("updates only timestamp when the timestamp changes", () => {
|
||||
const timestamp = <span>14:56</span>;
|
||||
const vm = new MKeyVerificationRequestViewModel({
|
||||
cli,
|
||||
mxEvent: createEvent("@alice:example.org"),
|
||||
});
|
||||
const listener = jest.fn();
|
||||
vm.subscribe(listener);
|
||||
|
||||
vm.setTimestamp(timestamp);
|
||||
|
||||
expect(vm.getSnapshot().timestamp).toBe(timestamp);
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not emit when the event-derived snapshot is unchanged", () => {
|
||||
const vm = new MKeyVerificationRequestViewModel({
|
||||
cli,
|
||||
mxEvent: createEvent("@alice:example.org"),
|
||||
});
|
||||
const listener = jest.fn();
|
||||
vm.subscribe(listener);
|
||||
|
||||
vm.setEvent(createEvent("@alice:example.org"));
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not emit when the timestamp is unchanged", () => {
|
||||
const timestamp = <span>14:56</span>;
|
||||
const vm = new MKeyVerificationRequestViewModel({
|
||||
cli,
|
||||
mxEvent: createEvent("@alice:example.org"),
|
||||
timestamp,
|
||||
});
|
||||
const listener = jest.fn();
|
||||
vm.subscribe(listener);
|
||||
|
||||
vm.setTimestamp(timestamp);
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user