522 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			522 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			TypeScript
		
	
	
| /*
 | |
| Copyright 2022 - 2023 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 * as React from "react";
 | |
| import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
 | |
| import { mocked } from "jest-mock";
 | |
| import {
 | |
|     EventType,
 | |
|     IEventDecryptionResult,
 | |
|     MatrixClient,
 | |
|     MatrixEvent,
 | |
|     NotificationCountType,
 | |
|     PendingEventOrdering,
 | |
|     Room,
 | |
|     TweakName,
 | |
| } from "matrix-js-sdk/src/matrix";
 | |
| import { CryptoApi, EventEncryptionInfo, EventShieldColour, EventShieldReason } from "matrix-js-sdk/src/crypto-api";
 | |
| import { mkEncryptedMatrixEvent } from "matrix-js-sdk/src/testing";
 | |
| 
 | |
| import EventTile, { EventTileProps } from "../../../../src/components/views/rooms/EventTile";
 | |
| import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
 | |
| import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
 | |
| import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
 | |
| import { filterConsole, flushPromises, getRoomContext, mkEvent, mkMessage, stubClient } from "../../../test-utils";
 | |
| import { mkThread } from "../../../test-utils/threads";
 | |
| import DMRoomMap from "../../../../src/utils/DMRoomMap";
 | |
| import dis from "../../../../src/dispatcher/dispatcher";
 | |
| import { Action } from "../../../../src/dispatcher/actions";
 | |
| import { IRoomState } from "../../../../src/components/structures/RoomView";
 | |
| 
 | |
| describe("EventTile", () => {
 | |
|     const ROOM_ID = "!roomId:example.org";
 | |
|     let mxEvent: MatrixEvent;
 | |
|     let room: Room;
 | |
|     let client: MatrixClient;
 | |
| 
 | |
|     // let changeEvent: (event: MatrixEvent) => void;
 | |
| 
 | |
|     /** wrap the EventTile up in context providers, and with basic properties, as it would be by MessagePanel normally. */
 | |
|     function WrappedEventTile(props: {
 | |
|         roomContext: IRoomState;
 | |
|         eventTilePropertyOverrides?: Partial<EventTileProps>;
 | |
|     }) {
 | |
|         return (
 | |
|             <MatrixClientContext.Provider value={client}>
 | |
|                 <RoomContext.Provider value={props.roomContext}>
 | |
|                     <EventTile
 | |
|                         mxEvent={mxEvent}
 | |
|                         replacingEventId={mxEvent.replacingEventId()}
 | |
|                         {...(props.eventTilePropertyOverrides ?? {})}
 | |
|                     />
 | |
|                 </RoomContext.Provider>
 | |
|             </MatrixClientContext.Provider>
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     function getComponent(
 | |
|         overrides: Partial<EventTileProps> = {},
 | |
|         renderingType: TimelineRenderingType = TimelineRenderingType.Room,
 | |
|     ) {
 | |
|         const context = getRoomContext(room, {
 | |
|             timelineRenderingType: renderingType,
 | |
|         });
 | |
|         return render(<WrappedEventTile roomContext={context} eventTilePropertyOverrides={overrides} />);
 | |
|     }
 | |
| 
 | |
|     beforeEach(() => {
 | |
|         jest.clearAllMocks();
 | |
| 
 | |
|         stubClient();
 | |
|         client = MatrixClientPeg.safeGet();
 | |
| 
 | |
|         room = new Room(ROOM_ID, client, client.getSafeUserId(), {
 | |
|             pendingEventOrdering: PendingEventOrdering.Detached,
 | |
|             timelineSupport: true,
 | |
|         });
 | |
| 
 | |
|         jest.spyOn(client, "getRoom").mockReturnValue(room);
 | |
|         jest.spyOn(client, "decryptEventIfNeeded").mockResolvedValue();
 | |
| 
 | |
|         mxEvent = mkMessage({
 | |
|             room: room.roomId,
 | |
|             user: "@alice:example.org",
 | |
|             msg: "Hello world!",
 | |
|             event: true,
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("EventTile thread summary", () => {
 | |
|         beforeEach(() => {
 | |
|             jest.spyOn(client, "supportsThreads").mockReturnValue(true);
 | |
|         });
 | |
| 
 | |
|         it("removes the thread summary when thread is deleted", async () => {
 | |
|             const {
 | |
|                 rootEvent,
 | |
|                 events: [, reply],
 | |
|             } = mkThread({
 | |
|                 room,
 | |
|                 client,
 | |
|                 authorId: "@alice:example.org",
 | |
|                 participantUserIds: ["@alice:example.org"],
 | |
|                 length: 2, // root + 1 answer
 | |
|             });
 | |
|             getComponent(
 | |
|                 {
 | |
|                     mxEvent: rootEvent,
 | |
|                 },
 | |
|                 TimelineRenderingType.Room,
 | |
|             );
 | |
| 
 | |
|             await waitFor(() => expect(screen.queryByTestId("thread-summary")).not.toBeNull());
 | |
| 
 | |
|             const redaction = mkEvent({
 | |
|                 event: true,
 | |
|                 type: EventType.RoomRedaction,
 | |
|                 user: "@alice:example.org",
 | |
|                 room: room.roomId,
 | |
|                 redacts: reply.getId(),
 | |
|                 content: {},
 | |
|             });
 | |
| 
 | |
|             act(() => room.processThreadedEvents([redaction], false));
 | |
| 
 | |
|             await waitFor(() => expect(screen.queryByTestId("thread-summary")).toBeNull());
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("EventTile renderingType: ThreadsList", () => {
 | |
|         it("shows an unread notification badge", () => {
 | |
|             const { container } = getComponent({}, TimelineRenderingType.ThreadsList);
 | |
| 
 | |
|             // By default, the thread will assume it is read.
 | |
|             expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(0);
 | |
| 
 | |
|             act(() => {
 | |
|                 room.setThreadUnreadNotificationCount(mxEvent.getId()!, NotificationCountType.Total, 3);
 | |
|             });
 | |
| 
 | |
|             expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1);
 | |
|             expect(container.getElementsByClassName("mx_NotificationBadge_level_highlight")).toHaveLength(0);
 | |
| 
 | |
|             act(() => {
 | |
|                 room.setThreadUnreadNotificationCount(mxEvent.getId()!, NotificationCountType.Highlight, 1);
 | |
|             });
 | |
| 
 | |
|             expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1);
 | |
|             expect(container.getElementsByClassName("mx_NotificationBadge_level_highlight")).toHaveLength(1);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("EventTile in the right panel", () => {
 | |
|         beforeAll(() => {
 | |
|             const dmRoomMap: DMRoomMap = {
 | |
|                 getUserIdForRoomId: jest.fn(),
 | |
|             } as unknown as DMRoomMap;
 | |
|             DMRoomMap.setShared(dmRoomMap);
 | |
|         });
 | |
| 
 | |
|         it("renders the room name for notifications", () => {
 | |
|             const { container } = getComponent({}, TimelineRenderingType.Notification);
 | |
|             expect(container.getElementsByClassName("mx_EventTile_details")[0]).toHaveTextContent(
 | |
|                 "@alice:example.org in !roomId:example.org",
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it("renders the sender for the thread list", () => {
 | |
|             const { container } = getComponent({}, TimelineRenderingType.ThreadsList);
 | |
|             expect(container.getElementsByClassName("mx_EventTile_details")[0]).toHaveTextContent("@alice:example.org");
 | |
|         });
 | |
| 
 | |
|         it.each([
 | |
|             [TimelineRenderingType.Notification, Action.ViewRoom],
 | |
|             [TimelineRenderingType.ThreadsList, Action.ShowThread],
 | |
|         ])("type %s dispatches %s", (renderingType, action) => {
 | |
|             jest.spyOn(dis, "dispatch");
 | |
| 
 | |
|             const { container } = getComponent({}, renderingType);
 | |
| 
 | |
|             fireEvent.click(container.querySelector("li")!);
 | |
| 
 | |
|             expect(dis.dispatch).toHaveBeenCalledWith(
 | |
|                 expect.objectContaining({
 | |
|                     action,
 | |
|                 }),
 | |
|             );
 | |
|         });
 | |
|     });
 | |
|     describe("Event verification", () => {
 | |
|         // data for our stubbed getEncryptionInfoForEvent: a map from event id to result
 | |
|         const eventToEncryptionInfoMap = new Map<string, EventEncryptionInfo>();
 | |
| 
 | |
|         beforeEach(() => {
 | |
|             eventToEncryptionInfoMap.clear();
 | |
| 
 | |
|             const mockCrypto = {
 | |
|                 // a mocked version of getEncryptionInfoForEvent which will pick its result from `eventToEncryptionInfoMap`
 | |
|                 getEncryptionInfoForEvent: async (event: MatrixEvent) => eventToEncryptionInfoMap.get(event.getId()!)!,
 | |
|             } as unknown as CryptoApi;
 | |
|             client.getCrypto = () => mockCrypto;
 | |
|         });
 | |
| 
 | |
|         it("shows a warning for an event from an unverified device", async () => {
 | |
|             mxEvent = await mkEncryptedMatrixEvent({
 | |
|                 plainContent: { msgtype: "m.text", body: "msg1" },
 | |
|                 plainType: "m.room.message",
 | |
|                 sender: "@alice:example.org",
 | |
|                 roomId: room.roomId,
 | |
|             });
 | |
|             eventToEncryptionInfoMap.set(mxEvent.getId()!, {
 | |
|                 shieldColour: EventShieldColour.RED,
 | |
|                 shieldReason: EventShieldReason.UNSIGNED_DEVICE,
 | |
|             } as EventEncryptionInfo);
 | |
| 
 | |
|             const { container } = getComponent();
 | |
|             await act(flushPromises);
 | |
| 
 | |
|             const eventTiles = container.getElementsByClassName("mx_EventTile");
 | |
|             expect(eventTiles).toHaveLength(1);
 | |
| 
 | |
|             // there should be a warning shield
 | |
|             expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
 | |
|             expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
 | |
|                 "mx_EventTile_e2eIcon_warning",
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it("shows no shield for a verified event", async () => {
 | |
|             mxEvent = await mkEncryptedMatrixEvent({
 | |
|                 plainContent: { msgtype: "m.text", body: "msg1" },
 | |
|                 plainType: "m.room.message",
 | |
|                 sender: "@alice:example.org",
 | |
|                 roomId: room.roomId,
 | |
|             });
 | |
|             eventToEncryptionInfoMap.set(mxEvent.getId()!, {
 | |
|                 shieldColour: EventShieldColour.NONE,
 | |
|                 shieldReason: null,
 | |
|             } as EventEncryptionInfo);
 | |
| 
 | |
|             const { container } = getComponent();
 | |
|             await act(flushPromises);
 | |
| 
 | |
|             const eventTiles = container.getElementsByClassName("mx_EventTile");
 | |
|             expect(eventTiles).toHaveLength(1);
 | |
| 
 | |
|             // there should be no warning
 | |
|             expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(0);
 | |
|         });
 | |
| 
 | |
|         it.each([
 | |
|             [EventShieldReason.UNKNOWN, "Unknown error"],
 | |
|             [EventShieldReason.UNVERIFIED_IDENTITY, "unverified user"],
 | |
|             [EventShieldReason.UNSIGNED_DEVICE, "device not verified by its owner"],
 | |
|             [EventShieldReason.UNKNOWN_DEVICE, "unknown or deleted device"],
 | |
|             [EventShieldReason.AUTHENTICITY_NOT_GUARANTEED, "can't be guaranteed"],
 | |
|             [EventShieldReason.MISMATCHED_SENDER_KEY, "Encrypted by an unverified session"],
 | |
|         ])("shows the correct reason code for %i (%s)", async (reasonCode: EventShieldReason, expectedText: string) => {
 | |
|             mxEvent = await mkEncryptedMatrixEvent({
 | |
|                 plainContent: { msgtype: "m.text", body: "msg1" },
 | |
|                 plainType: "m.room.message",
 | |
|                 sender: "@alice:example.org",
 | |
|                 roomId: room.roomId,
 | |
|             });
 | |
|             eventToEncryptionInfoMap.set(mxEvent.getId()!, {
 | |
|                 shieldColour: EventShieldColour.GREY,
 | |
|                 shieldReason: reasonCode,
 | |
|             } as EventEncryptionInfo);
 | |
| 
 | |
|             const { container } = getComponent();
 | |
|             await act(flushPromises);
 | |
| 
 | |
|             const e2eIcons = container.getElementsByClassName("mx_EventTile_e2eIcon");
 | |
|             expect(e2eIcons).toHaveLength(1);
 | |
|             expect(e2eIcons[0].classList).toContain("mx_EventTile_e2eIcon_normal");
 | |
|             fireEvent.focus(e2eIcons[0]);
 | |
|             expect(e2eIcons[0].getAttribute("aria-describedby")).toBeTruthy();
 | |
|             expect(document.getElementById(e2eIcons[0].getAttribute("aria-describedby")!)).toHaveTextContent(
 | |
|                 expectedText,
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         describe("undecryptable event", () => {
 | |
|             filterConsole("Error decrypting event");
 | |
| 
 | |
|             it("shows an undecryptable warning", async () => {
 | |
|                 mxEvent = mkEvent({
 | |
|                     type: "m.room.encrypted",
 | |
|                     room: room.roomId,
 | |
|                     user: "@alice:example.org",
 | |
|                     event: true,
 | |
|                     content: {},
 | |
|                 });
 | |
| 
 | |
|                 const mockCrypto = {
 | |
|                     decryptEvent: async (_ev): Promise<IEventDecryptionResult> => {
 | |
|                         throw new Error("can't decrypt");
 | |
|                     },
 | |
|                 } as Parameters<MatrixEvent["attemptDecryption"]>[0];
 | |
|                 await mxEvent.attemptDecryption(mockCrypto);
 | |
| 
 | |
|                 const { container } = getComponent();
 | |
|                 await act(flushPromises);
 | |
| 
 | |
|                 const eventTiles = container.getElementsByClassName("mx_EventTile");
 | |
|                 expect(eventTiles).toHaveLength(1);
 | |
| 
 | |
|                 expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
 | |
|                 expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
 | |
|                     "mx_EventTile_e2eIcon_decryption_failure",
 | |
|                 );
 | |
|             });
 | |
|         });
 | |
| 
 | |
|         it("should update the warning when the event is edited", async () => {
 | |
|             // we start out with an event from the trusted device
 | |
|             mxEvent = await mkEncryptedMatrixEvent({
 | |
|                 plainContent: { msgtype: "m.text", body: "msg1" },
 | |
|                 plainType: "m.room.message",
 | |
|                 sender: "@alice:example.org",
 | |
|                 roomId: room.roomId,
 | |
|             });
 | |
|             eventToEncryptionInfoMap.set(mxEvent.getId()!, {
 | |
|                 shieldColour: EventShieldColour.NONE,
 | |
|                 shieldReason: null,
 | |
|             } as EventEncryptionInfo);
 | |
| 
 | |
|             const roomContext = getRoomContext(room, {});
 | |
|             const { container, rerender } = render(<WrappedEventTile roomContext={roomContext} />);
 | |
| 
 | |
|             await act(flushPromises);
 | |
| 
 | |
|             const eventTiles = container.getElementsByClassName("mx_EventTile");
 | |
|             expect(eventTiles).toHaveLength(1);
 | |
| 
 | |
|             // there should be no warning
 | |
|             expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(0);
 | |
| 
 | |
|             // then we replace the event with one from the unverified device
 | |
|             const replacementEvent = await mkEncryptedMatrixEvent({
 | |
|                 plainContent: { msgtype: "m.text", body: "msg1" },
 | |
|                 plainType: "m.room.message",
 | |
|                 sender: "@alice:example.org",
 | |
|                 roomId: room.roomId,
 | |
|             });
 | |
|             eventToEncryptionInfoMap.set(replacementEvent.getId()!, {
 | |
|                 shieldColour: EventShieldColour.RED,
 | |
|                 shieldReason: EventShieldReason.UNSIGNED_DEVICE,
 | |
|             } as EventEncryptionInfo);
 | |
| 
 | |
|             await act(async () => {
 | |
|                 mxEvent.makeReplaced(replacementEvent);
 | |
|                 rerender(<WrappedEventTile roomContext={roomContext} />);
 | |
|                 await flushPromises;
 | |
|             });
 | |
| 
 | |
|             // check it was updated
 | |
|             expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
 | |
|             expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
 | |
|                 "mx_EventTile_e2eIcon_warning",
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it("should update the warning when the event is replaced with an unencrypted one", async () => {
 | |
|             jest.spyOn(client, "isRoomEncrypted").mockReturnValue(true);
 | |
| 
 | |
|             // we start out with an event from the trusted device
 | |
|             mxEvent = await mkEncryptedMatrixEvent({
 | |
|                 plainContent: { msgtype: "m.text", body: "msg1" },
 | |
|                 plainType: "m.room.message",
 | |
|                 sender: "@alice:example.org",
 | |
|                 roomId: room.roomId,
 | |
|             });
 | |
| 
 | |
|             eventToEncryptionInfoMap.set(mxEvent.getId()!, {
 | |
|                 shieldColour: EventShieldColour.NONE,
 | |
|                 shieldReason: null,
 | |
|             } as EventEncryptionInfo);
 | |
| 
 | |
|             const roomContext = getRoomContext(room, {});
 | |
|             const { container, rerender } = render(<WrappedEventTile roomContext={roomContext} />);
 | |
|             await act(flushPromises);
 | |
| 
 | |
|             const eventTiles = container.getElementsByClassName("mx_EventTile");
 | |
|             expect(eventTiles).toHaveLength(1);
 | |
| 
 | |
|             // there should be no warning
 | |
|             expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(0);
 | |
| 
 | |
|             // then we replace the event with an unencrypted one
 | |
|             const replacementEvent = await mkMessage({
 | |
|                 msg: "msg2",
 | |
|                 user: "@alice:example.org",
 | |
|                 room: room.roomId,
 | |
|                 event: true,
 | |
|             });
 | |
| 
 | |
|             await act(async () => {
 | |
|                 mxEvent.makeReplaced(replacementEvent);
 | |
|                 rerender(<WrappedEventTile roomContext={roomContext} />);
 | |
|                 await flushPromises;
 | |
|             });
 | |
| 
 | |
|             // check it was updated
 | |
|             expect(container.getElementsByClassName("mx_EventTile_e2eIcon")).toHaveLength(1);
 | |
|             expect(container.getElementsByClassName("mx_EventTile_e2eIcon")[0].classList).toContain(
 | |
|                 "mx_EventTile_e2eIcon_warning",
 | |
|             );
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe("event highlighting", () => {
 | |
|         const isHighlighted = (container: HTMLElement): boolean =>
 | |
|             !!container.getElementsByClassName("mx_EventTile_highlight").length;
 | |
| 
 | |
|         beforeEach(() => {
 | |
|             mocked(client.getPushActionsForEvent).mockReturnValue(null);
 | |
|         });
 | |
| 
 | |
|         it("does not highlight message where message matches no push actions", () => {
 | |
|             const { container } = getComponent();
 | |
| 
 | |
|             expect(client.getPushActionsForEvent).toHaveBeenCalledWith(mxEvent);
 | |
|             expect(isHighlighted(container)).toBeFalsy();
 | |
|         });
 | |
| 
 | |
|         it(`does not highlight when message's push actions does not have a highlight tweak`, () => {
 | |
|             mocked(client.getPushActionsForEvent).mockReturnValue({ notify: true, tweaks: {} });
 | |
|             const { container } = getComponent();
 | |
| 
 | |
|             expect(isHighlighted(container)).toBeFalsy();
 | |
|         });
 | |
| 
 | |
|         it(`highlights when message's push actions have a highlight tweak`, () => {
 | |
|             mocked(client.getPushActionsForEvent).mockReturnValue({
 | |
|                 notify: true,
 | |
|                 tweaks: { [TweakName.Highlight]: true },
 | |
|             });
 | |
|             const { container } = getComponent();
 | |
| 
 | |
|             expect(isHighlighted(container)).toBeTruthy();
 | |
|         });
 | |
| 
 | |
|         describe("when a message has been edited", () => {
 | |
|             let editingEvent: MatrixEvent;
 | |
| 
 | |
|             beforeEach(() => {
 | |
|                 editingEvent = new MatrixEvent({
 | |
|                     type: "m.room.message",
 | |
|                     room_id: ROOM_ID,
 | |
|                     sender: "@alice:example.org",
 | |
|                     content: {
 | |
|                         "msgtype": "m.text",
 | |
|                         "body": "* edited body",
 | |
|                         "m.new_content": {
 | |
|                             msgtype: "m.text",
 | |
|                             body: "edited body",
 | |
|                         },
 | |
|                         "m.relates_to": {
 | |
|                             rel_type: "m.replace",
 | |
|                             event_id: mxEvent.getId(),
 | |
|                         },
 | |
|                     },
 | |
|                 });
 | |
|                 mxEvent.makeReplaced(editingEvent);
 | |
|             });
 | |
| 
 | |
|             it("does not highlight message where no version of message matches any push actions", () => {
 | |
|                 const { container } = getComponent();
 | |
| 
 | |
|                 // get push actions for both events
 | |
|                 expect(client.getPushActionsForEvent).toHaveBeenCalledWith(mxEvent);
 | |
|                 expect(client.getPushActionsForEvent).toHaveBeenCalledWith(editingEvent);
 | |
|                 expect(isHighlighted(container)).toBeFalsy();
 | |
|             });
 | |
| 
 | |
|             it(`does not highlight when no version of message's push actions have a highlight tweak`, () => {
 | |
|                 mocked(client.getPushActionsForEvent).mockReturnValue({ notify: true, tweaks: {} });
 | |
|                 const { container } = getComponent();
 | |
| 
 | |
|                 expect(isHighlighted(container)).toBeFalsy();
 | |
|             });
 | |
| 
 | |
|             it(`highlights when previous version of message's push actions have a highlight tweak`, () => {
 | |
|                 mocked(client.getPushActionsForEvent).mockImplementation((event: MatrixEvent) => {
 | |
|                     if (event === mxEvent) {
 | |
|                         return { notify: true, tweaks: { [TweakName.Highlight]: true } };
 | |
|                     }
 | |
|                     return { notify: false, tweaks: {} };
 | |
|                 });
 | |
|                 const { container } = getComponent();
 | |
| 
 | |
|                 expect(isHighlighted(container)).toBeTruthy();
 | |
|             });
 | |
| 
 | |
|             it(`highlights when new version of message's push actions have a highlight tweak`, () => {
 | |
|                 mocked(client.getPushActionsForEvent).mockImplementation((event: MatrixEvent) => {
 | |
|                     if (event === editingEvent) {
 | |
|                         return { notify: true, tweaks: { [TweakName.Highlight]: true } };
 | |
|                     }
 | |
|                     return { notify: false, tweaks: {} };
 | |
|                 });
 | |
|                 const { container } = getComponent();
 | |
| 
 | |
|                 expect(isHighlighted(container)).toBeTruthy();
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| });
 |