Merge matrix-react-sdk into element-web
Merge remote-tracking branch 'repomerge/t3chguy/repomerge' into t3chguy/repo-merge Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
commit
f0ee7f7905
3265 changed files with 484599 additions and 699 deletions
156
test/unit-tests/components/views/messages/CallEvent-test.tsx
Normal file
156
test/unit-tests/components/views/messages/CallEvent-test.tsx
Normal file
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen, act, cleanup, fireEvent, waitFor } from "jest-matrix-react";
|
||||
import { mocked, Mocked } from "jest-mock";
|
||||
import { Room, RoomStateEvent, MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/matrix";
|
||||
import { ClientWidgetApi, Widget } from "matrix-widget-api";
|
||||
|
||||
import type { RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
useMockedCalls,
|
||||
MockedCall,
|
||||
stubClient,
|
||||
mkRoomMember,
|
||||
setupAsyncStoreWithClient,
|
||||
resetAsyncStoreWithClient,
|
||||
wrapInMatrixClientContext,
|
||||
useMockMediaDevices,
|
||||
} from "../../../../test-utils";
|
||||
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import { CallEvent as UnwrappedCallEvent } from "../../../../../src/components/views/messages/CallEvent";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import { CallStore } from "../../../../../src/stores/CallStore";
|
||||
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
|
||||
import { ConnectionState } from "../../../../../src/models/Call";
|
||||
|
||||
const CallEvent = wrapInMatrixClientContext(UnwrappedCallEvent);
|
||||
|
||||
describe("CallEvent", () => {
|
||||
let client: Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
let alice: RoomMember;
|
||||
let bob: RoomMember;
|
||||
let call: MockedCall;
|
||||
let widget: Widget;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
|
||||
useMockMediaDevices();
|
||||
useMockedCalls();
|
||||
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});
|
||||
|
||||
stubClient();
|
||||
client = mocked(MatrixClientPeg.safeGet());
|
||||
client.getUserId.mockReturnValue("@alice:example.org");
|
||||
|
||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
alice = mkRoomMember(room.roomId, "@alice:example.org");
|
||||
bob = mkRoomMember(room.roomId, "@bob:example.org");
|
||||
jest.spyOn(room, "getMember").mockImplementation(
|
||||
(userId) => [alice, bob].find((member) => member.userId === userId) ?? null,
|
||||
);
|
||||
|
||||
client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null));
|
||||
client.getRooms.mockReturnValue([room]);
|
||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||
|
||||
await Promise.all(
|
||||
[CallStore.instance, WidgetMessagingStore.instance].map((store) =>
|
||||
setupAsyncStoreWithClient(store, client),
|
||||
),
|
||||
);
|
||||
|
||||
MockedCall.create(room, "1");
|
||||
const maybeCall = CallStore.instance.getCall(room.roomId);
|
||||
if (!(maybeCall instanceof MockedCall)) throw new Error("Failed to create call");
|
||||
call = maybeCall;
|
||||
|
||||
widget = new Widget(call.widget);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
cleanup(); // Unmount before we do any cleanup that might update the component
|
||||
call.destroy();
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
await Promise.all([CallStore.instance, WidgetMessagingStore.instance].map(resetAsyncStoreWithClient));
|
||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const renderEvent = () => {
|
||||
render(<CallEvent mxEvent={call.event} />);
|
||||
};
|
||||
|
||||
it("shows a message and duration if the call was ended", () => {
|
||||
jest.advanceTimersByTime(90000);
|
||||
call.destroy();
|
||||
renderEvent();
|
||||
|
||||
screen.getByText("Video call ended");
|
||||
screen.getByText("1m 30s");
|
||||
});
|
||||
|
||||
it("shows a message if the call was redacted", () => {
|
||||
const event = room.currentState.getStateEvents(MockedCall.EVENT_TYPE, "1")!;
|
||||
jest.spyOn(event, "isRedacted").mockReturnValue(true);
|
||||
renderEvent();
|
||||
|
||||
screen.getByText("Video call ended");
|
||||
});
|
||||
|
||||
it("shows placeholder info if the call isn't loaded yet", () => {
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(null);
|
||||
jest.advanceTimersByTime(90000);
|
||||
renderEvent();
|
||||
|
||||
screen.getByText("@alice:example.org started a video call");
|
||||
expect(screen.getByRole("button", { name: "Join" })).toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
it("shows call details and connection controls if the call is loaded", async () => {
|
||||
jest.advanceTimersByTime(90000);
|
||||
call.participants = new Map([
|
||||
[alice, new Set(["a"])],
|
||||
[bob, new Set(["b"])],
|
||||
]);
|
||||
renderEvent();
|
||||
|
||||
screen.getByText("@alice:example.org started a video call");
|
||||
screen.getByLabelText("2 people joined");
|
||||
|
||||
// Test that the join button works
|
||||
const dispatcherSpy = jest.fn();
|
||||
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Join" }));
|
||||
await waitFor(() =>
|
||||
expect(dispatcherSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
view_call: true,
|
||||
}),
|
||||
);
|
||||
defaultDispatcher.unregister(dispatcherRef);
|
||||
await act(() => call.start());
|
||||
|
||||
// Test that the leave button works
|
||||
fireEvent.click(screen.getByRole("button", { name: "Leave" }));
|
||||
await waitFor(() => screen.getByRole("button", { name: "Join" }));
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
});
|
||||
});
|
298
test/unit-tests/components/views/messages/DateSeparator-test.tsx
Normal file
298
test/unit-tests/components/views/messages/DateSeparator-test.tsx
Normal file
|
@ -0,0 +1,298 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { fireEvent, render, screen } from "jest-matrix-react";
|
||||
import { TimestampToEventResponse, ConnectionError, HTTPError, MatrixError } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import { ViewRoomPayload } from "../../../../../src/dispatcher/payloads/ViewRoomPayload";
|
||||
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
|
||||
import { formatFullDateNoTime } from "../../../../../src/DateUtils";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import { UIFeature } from "../../../../../src/settings/UIFeature";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import {
|
||||
clearAllModals,
|
||||
flushPromisesWithFakeTimers,
|
||||
getMockClientWithEventEmitter,
|
||||
waitEnoughCyclesForModal,
|
||||
} from "../../../../test-utils";
|
||||
import DateSeparator from "../../../../../src/components/views/messages/DateSeparator";
|
||||
|
||||
jest.mock("../../../../../src/settings/SettingsStore");
|
||||
|
||||
describe("DateSeparator", () => {
|
||||
const HOUR_MS = 3600000;
|
||||
const DAY_MS = HOUR_MS * 24;
|
||||
// Friday Dec 17 2021, 9:09am
|
||||
const nowDate = new Date("2021-12-17T08:09:00.000Z");
|
||||
const roomId = "!unused:example.org";
|
||||
const defaultProps = {
|
||||
ts: nowDate.getTime(),
|
||||
roomId,
|
||||
};
|
||||
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
timestampToEvent: jest.fn(),
|
||||
});
|
||||
const getComponent = (props = {}) =>
|
||||
render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<DateSeparator {...defaultProps} {...props} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
type TestCase = [string, number, string];
|
||||
const testCases: TestCase[] = [
|
||||
["the exact same moment", nowDate.getTime(), "today"],
|
||||
["same day as current day", nowDate.getTime() - HOUR_MS, "today"],
|
||||
["day before the current day", nowDate.getTime() - HOUR_MS * 12, "yesterday"],
|
||||
["2 days ago", nowDate.getTime() - DAY_MS * 2, "Wednesday"],
|
||||
["144 hours ago", nowDate.getTime() - HOUR_MS * 144, "Sat, Dec 11, 2021"],
|
||||
[
|
||||
"6 days ago, but less than 144h",
|
||||
new Date("Saturday Dec 11 2021 23:59:00 GMT+0100 (Central European Standard Time)").getTime(),
|
||||
"Saturday",
|
||||
],
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
// Set a consistent fake time here so the test is always consistent
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(nowDate.getTime());
|
||||
|
||||
(SettingsStore.getValue as jest.Mock) = jest.fn((arg) => {
|
||||
if (arg === UIFeature.TimelineEnableRelativeDates) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(roomId);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("renders the date separator correctly", () => {
|
||||
const { asFragment } = getComponent();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
expect(SettingsStore.getValue).toHaveBeenCalledWith(UIFeature.TimelineEnableRelativeDates);
|
||||
});
|
||||
|
||||
it.each(testCases)("formats date correctly when current time is %s", (_d, ts, result) => {
|
||||
expect(getComponent({ ts, forExport: false }).container.textContent).toEqual(result);
|
||||
});
|
||||
|
||||
describe("when forExport is true", () => {
|
||||
it.each(testCases)("formats date in full when current time is %s", (_d, ts) => {
|
||||
expect(getComponent({ ts, forExport: true }).container.textContent).toEqual(
|
||||
formatFullDateNoTime(new Date(ts)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when Settings.TimelineEnableRelativeDates is falsy", () => {
|
||||
beforeEach(() => {
|
||||
(SettingsStore.getValue as jest.Mock) = jest.fn((arg) => {
|
||||
if (arg === UIFeature.TimelineEnableRelativeDates) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
it.each(testCases)("formats date in full when current time is %s", (_d, ts) => {
|
||||
expect(getComponent({ ts, forExport: false }).container.textContent).toEqual(
|
||||
formatFullDateNoTime(new Date(ts)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when feature_jump_to_date is enabled", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mocked(SettingsStore).getValue.mockImplementation((arg): any => {
|
||||
if (arg === "feature_jump_to_date") {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
jest.spyOn(dispatcher, "dispatch").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clearAllModals();
|
||||
});
|
||||
|
||||
it("renders the date separator correctly", () => {
|
||||
const { asFragment } = getComponent();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
[
|
||||
{
|
||||
timeDescriptor: "last week",
|
||||
jumpButtonTestId: "jump-to-date-last-week",
|
||||
},
|
||||
{
|
||||
timeDescriptor: "last month",
|
||||
jumpButtonTestId: "jump-to-date-last-month",
|
||||
},
|
||||
{
|
||||
timeDescriptor: "the beginning",
|
||||
jumpButtonTestId: "jump-to-date-beginning",
|
||||
},
|
||||
].forEach((testCase) => {
|
||||
it(`can jump to ${testCase.timeDescriptor}`, async () => {
|
||||
// Render the component
|
||||
getComponent();
|
||||
|
||||
// Open the jump to date context menu
|
||||
fireEvent.click(screen.getByTestId("jump-to-date-separator-button"));
|
||||
|
||||
// Jump to "x"
|
||||
const returnedDate = new Date();
|
||||
// Just an arbitrary date before "now"
|
||||
returnedDate.setDate(nowDate.getDate() - 100);
|
||||
const returnedEventId = "$abc";
|
||||
mockClient.timestampToEvent.mockResolvedValue({
|
||||
event_id: returnedEventId,
|
||||
origin_server_ts: returnedDate.getTime(),
|
||||
} satisfies TimestampToEventResponse);
|
||||
const jumpToXButton = await screen.findByTestId(testCase.jumpButtonTestId);
|
||||
fireEvent.click(jumpToXButton);
|
||||
|
||||
// Flush out the dispatcher which uses `window.setTimeout(...)` since we're
|
||||
// using `jest.useFakeTimers()` in these tests.
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
// Ensure that we're jumping to the event at the given date
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
event_id: returnedEventId,
|
||||
highlighted: true,
|
||||
room_id: roomId,
|
||||
metricsTrigger: undefined,
|
||||
} satisfies ViewRoomPayload);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not jump to date if we already switched to another room", async () => {
|
||||
// Render the component
|
||||
getComponent();
|
||||
|
||||
// Open the jump to date context menu
|
||||
fireEvent.click(screen.getByTestId("jump-to-date-separator-button"));
|
||||
|
||||
// Mimic the outcome of switching rooms while waiting for the jump to date
|
||||
// request to finish. Imagine that we started jumping to "last week", the
|
||||
// network request is taking a while, so we got bored, switched rooms; we
|
||||
// shouldn't jump back to the previous room after the network request
|
||||
// happens to finish later.
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!some-other-room");
|
||||
|
||||
// Jump to "last week"
|
||||
mockClient.timestampToEvent.mockResolvedValue({
|
||||
event_id: "$abc",
|
||||
origin_server_ts: 0,
|
||||
});
|
||||
const jumpToLastWeekButton = await screen.findByTestId("jump-to-date-last-week");
|
||||
fireEvent.click(jumpToLastWeekButton);
|
||||
|
||||
// Flush out the dispatcher which uses `window.setTimeout(...)` since we're
|
||||
// using `jest.useFakeTimers()` in these tests.
|
||||
await flushPromisesWithFakeTimers();
|
||||
|
||||
// We should not see any room switching going on (`Action.ViewRoom`)
|
||||
expect(dispatcher.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not show jump to date error if we already switched to another room", async () => {
|
||||
// Render the component
|
||||
getComponent();
|
||||
|
||||
// Open the jump to date context menu
|
||||
fireEvent.click(screen.getByTestId("jump-to-date-separator-button"));
|
||||
|
||||
// Mimic the outcome of switching rooms while waiting for the jump to date
|
||||
// request to finish. Imagine that we started jumping to "last week", the
|
||||
// network request is taking a while, so we got bored, switched rooms; we
|
||||
// shouldn't jump back to the previous room after the network request
|
||||
// happens to finish later.
|
||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!some-other-room");
|
||||
|
||||
// Try to jump to "last week" but we want an error to occur and ensure that
|
||||
// we don't show an error dialog for it since we already switched away to
|
||||
// another room and don't care about the outcome here anymore.
|
||||
mockClient.timestampToEvent.mockRejectedValue(new Error("Fake error in test"));
|
||||
const jumpToLastWeekButton = await screen.findByTestId("jump-to-date-last-week");
|
||||
fireEvent.click(jumpToLastWeekButton);
|
||||
|
||||
// Wait the necessary time in order to see if any modal will appear
|
||||
await waitEnoughCyclesForModal({
|
||||
useFakeTimers: true,
|
||||
});
|
||||
|
||||
// We should not see any error modal dialog
|
||||
//
|
||||
// We have to use `queryBy` so that it can return `null` for something that does not exist.
|
||||
expect(screen.queryByTestId("jump-to-date-error-content")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show error dialog with submit debug logs option when non-networking error occurs", async () => {
|
||||
// Render the component
|
||||
getComponent();
|
||||
|
||||
// Open the jump to date context menu
|
||||
fireEvent.click(screen.getByTestId("jump-to-date-separator-button"));
|
||||
|
||||
// Try to jump to "last week" but we want a non-network error to occur so it
|
||||
// shows the "Submit debug logs" UI
|
||||
mockClient.timestampToEvent.mockRejectedValue(new Error("Fake error in test"));
|
||||
const jumpToLastWeekButton = await screen.findByTestId("jump-to-date-last-week");
|
||||
fireEvent.click(jumpToLastWeekButton);
|
||||
|
||||
// Expect error to be shown. We have to wait for the UI to transition.
|
||||
expect(await screen.findByTestId("jump-to-date-error-content")).toBeInTheDocument();
|
||||
|
||||
// Expect an option to submit debug logs to be shown when a non-network error occurs
|
||||
expect(await screen.findByTestId("jump-to-date-error-submit-debug-logs-button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
[
|
||||
new ConnectionError("Fake connection error in test"),
|
||||
new HTTPError("Fake http error in test", 418),
|
||||
new MatrixError(
|
||||
{ errcode: "M_FAKE_ERROR_CODE", error: "Some fake error occured" },
|
||||
518,
|
||||
"https://fake-url/",
|
||||
),
|
||||
].forEach((fakeError) => {
|
||||
it(`should show error dialog without submit debug logs option when networking error (${fakeError.name}) occurs`, async () => {
|
||||
// Render the component
|
||||
getComponent();
|
||||
|
||||
// Open the jump to date context menu
|
||||
fireEvent.click(screen.getByTestId("jump-to-date-separator-button"));
|
||||
|
||||
// Try to jump to "last week" but we want a network error to occur
|
||||
mockClient.timestampToEvent.mockRejectedValue(fakeError);
|
||||
const jumpToLastWeekButton = await screen.findByTestId("jump-to-date-last-week");
|
||||
fireEvent.click(jumpToLastWeekButton);
|
||||
|
||||
// Expect error to be shown. We have to wait for the UI to transition.
|
||||
expect(await screen.findByTestId("jump-to-date-error-content")).toBeInTheDocument();
|
||||
|
||||
// The submit debug logs option should *NOT* be shown for network errors.
|
||||
//
|
||||
// We have to use `queryBy` so that it can return `null` for something that does not exist.
|
||||
expect(screen.queryByTestId("jump-to-date-error-submit-debug-logs-button")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { mkDecryptionFailureMatrixEvent } from "matrix-js-sdk/src/testing";
|
||||
import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { mkEvent } from "../../../../test-utils";
|
||||
import { DecryptionFailureBody } from "../../../../../src/components/views/messages/DecryptionFailureBody";
|
||||
import { LocalDeviceVerificationStateContext } from "../../../../../src/contexts/LocalDeviceVerificationStateContext";
|
||||
|
||||
describe("DecryptionFailureBody", () => {
|
||||
function customRender(event: MatrixEvent, localDeviceVerified: boolean = false) {
|
||||
return render(
|
||||
<LocalDeviceVerificationStateContext.Provider value={localDeviceVerified}>
|
||||
<DecryptionFailureBody mxEvent={event} />
|
||||
</LocalDeviceVerificationStateContext.Provider>,
|
||||
);
|
||||
}
|
||||
|
||||
it(`Should display "Unable to decrypt message"`, () => {
|
||||
// When
|
||||
const event = mkEvent({
|
||||
type: "m.room.message",
|
||||
room: "myfakeroom",
|
||||
user: "myfakeuser",
|
||||
content: {
|
||||
msgtype: "m.bad.encrypted",
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
const { container } = customRender(event);
|
||||
|
||||
// Then
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it(`Should display "The sender has blocked you from receiving this message"`, async () => {
|
||||
// When
|
||||
const event = await mkDecryptionFailureMatrixEvent({
|
||||
code: DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE,
|
||||
msg: "withheld",
|
||||
roomId: "myfakeroom",
|
||||
sender: "myfakeuser",
|
||||
});
|
||||
|
||||
const { container } = customRender(event);
|
||||
|
||||
// Then
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should handle historical messages with no key backup", async () => {
|
||||
// When
|
||||
const event = await mkDecryptionFailureMatrixEvent({
|
||||
code: DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP,
|
||||
msg: "No backup",
|
||||
roomId: "fakeroom",
|
||||
sender: "fakesender",
|
||||
});
|
||||
const { container } = customRender(event);
|
||||
|
||||
// Then
|
||||
expect(container).toHaveTextContent("Historical messages are not available on this device");
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
"should handle historical messages when there is a backup and device verification is %s",
|
||||
async (verified) => {
|
||||
// When
|
||||
const event = await mkDecryptionFailureMatrixEvent({
|
||||
code: DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED,
|
||||
msg: "Failure",
|
||||
roomId: "fakeroom",
|
||||
sender: "fakesender",
|
||||
});
|
||||
const { container } = customRender(event, verified);
|
||||
|
||||
// Then
|
||||
expect(container).toHaveTextContent(
|
||||
verified ? "Unable to decrypt" : "You need to verify this device for access to historical messages",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("should handle undecryptable pre-join messages", async () => {
|
||||
// When
|
||||
const event = await mkDecryptionFailureMatrixEvent({
|
||||
code: DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED,
|
||||
msg: "Not joined",
|
||||
roomId: "fakeroom",
|
||||
sender: "fakesender",
|
||||
});
|
||||
const { container } = customRender(event);
|
||||
|
||||
// Then
|
||||
expect(container).toHaveTextContent("You don't have access to this message");
|
||||
});
|
||||
|
||||
it("should handle messages from users who change identities after verification", async () => {
|
||||
// When
|
||||
const event = await mkDecryptionFailureMatrixEvent({
|
||||
code: DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED,
|
||||
msg: "User previously verified",
|
||||
roomId: "fakeroom",
|
||||
sender: "fakesender",
|
||||
});
|
||||
const { container } = customRender(event);
|
||||
|
||||
// Then
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should handle messages from unverified devices", async () => {
|
||||
// When
|
||||
const event = await mkDecryptionFailureMatrixEvent({
|
||||
code: DecryptionFailureCode.UNSIGNED_SENDER_DEVICE,
|
||||
msg: "Unsigned device",
|
||||
roomId: "fakeroom",
|
||||
sender: "fakesender",
|
||||
});
|
||||
const { container } = customRender(event);
|
||||
|
||||
// Then
|
||||
expect(container).toHaveTextContent("Encrypted by a device not verified by its owner");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import fetchMockJest from "fetch-mock-jest";
|
||||
import { fireEvent, render, screen, waitFor } from "jest-matrix-react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { stubClient } from "../../../../test-utils";
|
||||
import DownloadActionButton from "../../../../../src/components/views/messages/DownloadActionButton";
|
||||
import Modal from "../../../../../src/Modal";
|
||||
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
|
||||
import ErrorDialog from "../../../../../src/components/views/dialogs/ErrorDialog";
|
||||
|
||||
describe("DownloadActionButton", () => {
|
||||
it("should show error if media API returns one", async () => {
|
||||
const cli = stubClient();
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
mocked(cli.mxcUrlToHttp).mockImplementation(
|
||||
(mxc) => `https://matrix.org/_matrix/media/r0/download/${mxc.slice(6)}`,
|
||||
);
|
||||
|
||||
fetchMockJest.get("https://matrix.org/_matrix/media/r0/download/matrix.org/1234", {
|
||||
status: 404,
|
||||
body: { errcode: "M_NOT_FOUND", error: "Not found" },
|
||||
});
|
||||
|
||||
const event = new MatrixEvent({
|
||||
room_id: "!room:id",
|
||||
sender: "@user:id",
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
body: "test",
|
||||
msgtype: "m.image",
|
||||
url: "mxc://matrix.org/1234",
|
||||
},
|
||||
});
|
||||
const mediaEventHelper = new MediaEventHelper(event);
|
||||
|
||||
render(<DownloadActionButton mxEvent={event} mediaEventHelperGet={() => mediaEventHelper} />);
|
||||
|
||||
const spy = jest.spyOn(Modal, "createDialog");
|
||||
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
await waitFor(() =>
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
ErrorDialog,
|
||||
expect.objectContaining({
|
||||
title: "Download failed",
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
|
||||
import EncryptionEvent from "../../../../../src/components/views/messages/EncryptionEvent";
|
||||
import { createTestClient, mkMessage } from "../../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import { LocalRoom } from "../../../../../src/models/LocalRoom";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
|
||||
const renderEncryptionEvent = (client: MatrixClient, event: MatrixEvent) => {
|
||||
render(
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<EncryptionEvent mxEvent={event} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
};
|
||||
|
||||
const checkTexts = (title: string, subTitle: string) => {
|
||||
screen.getByText(title);
|
||||
screen.getByText(subTitle);
|
||||
};
|
||||
|
||||
describe("EncryptionEvent", () => {
|
||||
const roomId = "!room:example.com";
|
||||
const algorithm = "m.megolm.v1.aes-sha2";
|
||||
let client: MatrixClient;
|
||||
let event: MatrixEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
client = createTestClient();
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client);
|
||||
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(client);
|
||||
event = mkMessage({
|
||||
event: true,
|
||||
room: roomId,
|
||||
user: client.getUserId()!,
|
||||
});
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue({
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
} as unknown as DMRoomMap);
|
||||
});
|
||||
|
||||
describe("for an encrypted room", () => {
|
||||
beforeEach(() => {
|
||||
event.event.content!.algorithm = algorithm;
|
||||
mocked(client.isRoomEncrypted).mockReturnValue(true);
|
||||
const room = new Room(roomId, client, client.getUserId()!);
|
||||
mocked(client.getRoom).mockReturnValue(room);
|
||||
});
|
||||
|
||||
it("should show the expected texts", () => {
|
||||
renderEncryptionEvent(client, event);
|
||||
checkTexts(
|
||||
"Encryption enabled",
|
||||
"Messages in this room are end-to-end encrypted. " +
|
||||
"When people join, you can verify them in their profile, just tap on their profile picture.",
|
||||
);
|
||||
});
|
||||
|
||||
describe("with same previous algorithm", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(event, "getPrevContent").mockReturnValue({
|
||||
algorithm: algorithm,
|
||||
});
|
||||
});
|
||||
|
||||
it("should show the expected texts", () => {
|
||||
renderEncryptionEvent(client, event);
|
||||
checkTexts("Encryption enabled", "Some encryption parameters have been changed.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with unknown algorithm", () => {
|
||||
beforeEach(() => {
|
||||
event.event.content!.algorithm = "unknown";
|
||||
});
|
||||
|
||||
it("should show the expected texts", () => {
|
||||
renderEncryptionEvent(client, event);
|
||||
checkTexts("Encryption enabled", "Ignored attempt to disable encryption");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("for an unencrypted room", () => {
|
||||
beforeEach(() => {
|
||||
mocked(client.isRoomEncrypted).mockReturnValue(false);
|
||||
renderEncryptionEvent(client, event);
|
||||
});
|
||||
|
||||
it("should show the expected texts", () => {
|
||||
expect(client.isRoomEncrypted).toHaveBeenCalledWith(roomId);
|
||||
checkTexts("Encryption not enabled", "The encryption used by this room isn't supported.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("for an encrypted local room", () => {
|
||||
beforeEach(() => {
|
||||
event.event.content!.algorithm = algorithm;
|
||||
mocked(client.isRoomEncrypted).mockReturnValue(true);
|
||||
const localRoom = new LocalRoom(roomId, client, client.getUserId()!);
|
||||
mocked(client.getRoom).mockReturnValue(localRoom);
|
||||
renderEncryptionEvent(client, event);
|
||||
});
|
||||
|
||||
it("should show the expected texts", () => {
|
||||
expect(client.isRoomEncrypted).toHaveBeenCalledWith(roomId);
|
||||
checkTexts("Encryption enabled", "Messages in this chat will be end-to-end encrypted.");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import JumpToDatePicker from "../../../../../src/components/views/messages/JumpToDatePicker";
|
||||
|
||||
describe("JumpToDatePicker", () => {
|
||||
const nowDate = new Date("2021-12-17T08:09:00.000Z");
|
||||
beforeEach(() => {
|
||||
// Set a stable fake time here so the test is always consistent
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(nowDate.getTime());
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("renders the date picker correctly", () => {
|
||||
const { asFragment } = render(
|
||||
<JumpToDatePicker ts={new Date("2020-07-04T05:55:00.000Z").getTime()} onDatePicked={() => {}} />,
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { CallErrorCode, CallState } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import LegacyCallEvent from "../../../../../src/components/views/messages/LegacyCallEvent";
|
||||
import LegacyCallEventGrouper from "../../../../../src/components/structures/LegacyCallEventGrouper";
|
||||
|
||||
const THEIR_USER_ID = "@them:here";
|
||||
|
||||
describe("LegacyCallEvent", () => {
|
||||
let callInviteEvent: Record<string, any>;
|
||||
let callEventGrouper: Record<string, any>;
|
||||
|
||||
beforeEach(() => {
|
||||
callInviteEvent = {
|
||||
sender: {
|
||||
userId: THEIR_USER_ID,
|
||||
},
|
||||
};
|
||||
|
||||
callEventGrouper = {
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
invite: jest.fn().mockReturnValue(callInviteEvent),
|
||||
};
|
||||
});
|
||||
|
||||
const renderEvent = () => {
|
||||
render(
|
||||
<LegacyCallEvent
|
||||
mxEvent={callInviteEvent as unknown as MatrixEvent}
|
||||
callEventGrouper={callEventGrouper as unknown as LegacyCallEventGrouper}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
it("shows if the call was ended", () => {
|
||||
callEventGrouper.state = CallState.Ended;
|
||||
callEventGrouper.gotRejected = jest.fn().mockReturnValue(true);
|
||||
|
||||
renderEvent();
|
||||
|
||||
screen.getByText("Call declined");
|
||||
});
|
||||
|
||||
it("shows if the call was answered elsewhere", () => {
|
||||
callEventGrouper.state = CallState.Ended;
|
||||
callEventGrouper.hangupReason = CallErrorCode.AnsweredElsewhere;
|
||||
|
||||
renderEvent();
|
||||
|
||||
screen.getByText("Answered elsewhere");
|
||||
});
|
||||
|
||||
it("shows if the call was missed", () => {
|
||||
callEventGrouper.state = CallState.Ended;
|
||||
callEventGrouper.callWasMissed = jest.fn().mockReturnValue(true);
|
||||
|
||||
renderEvent();
|
||||
|
||||
screen.getByText("Missed call");
|
||||
});
|
||||
|
||||
it("shows if the call ended cleanly", () => {
|
||||
callEventGrouper.state = CallState.Ended;
|
||||
callEventGrouper.hangupReason = CallErrorCode.UserHangup;
|
||||
|
||||
renderEvent();
|
||||
|
||||
screen.getByText("Call ended");
|
||||
});
|
||||
|
||||
it("shows if the call is connecting", () => {
|
||||
callEventGrouper.state = CallState.Connecting;
|
||||
|
||||
renderEvent();
|
||||
|
||||
screen.getByText("Connecting");
|
||||
});
|
||||
|
||||
it("shows timer if the call is connected", () => {
|
||||
callEventGrouper.state = CallState.Connected;
|
||||
|
||||
renderEvent();
|
||||
|
||||
screen.getByText("00:00");
|
||||
});
|
||||
});
|
416
test/unit-tests/components/views/messages/MBeaconBody-test.tsx
Normal file
416
test/unit-tests/components/views/messages/MBeaconBody-test.tsx
Normal file
|
@ -0,0 +1,416 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
import { act, fireEvent, render } from "jest-matrix-react";
|
||||
import * as maplibregl from "maplibre-gl";
|
||||
import {
|
||||
BeaconEvent,
|
||||
getBeaconInfoIdentifier,
|
||||
RelationType,
|
||||
MatrixEvent,
|
||||
EventType,
|
||||
Relations,
|
||||
M_BEACON,
|
||||
Room,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import MBeaconBody from "../../../../../src/components/views/messages/MBeaconBody";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
makeBeaconEvent,
|
||||
makeBeaconInfoEvent,
|
||||
makeRoomWithBeacons,
|
||||
makeRoomWithStateEvents,
|
||||
} from "../../../../test-utils";
|
||||
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
||||
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import Modal from "../../../../../src/Modal";
|
||||
import { TILE_SERVER_WK_KEY } from "../../../../../src/utils/WellKnownUtils";
|
||||
import * as mapUtilHooks from "../../../../../src/utils/location/useMap";
|
||||
import { LocationShareError } from "../../../../../src/utils/location";
|
||||
|
||||
describe("<MBeaconBody />", () => {
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
// stable date for snapshots
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(now);
|
||||
const roomId = "!room:server";
|
||||
const aliceId = "@alice:server";
|
||||
|
||||
const mapOptions = { container: {} as unknown as HTMLElement, style: "" };
|
||||
const mockMap = new maplibregl.Map(mapOptions);
|
||||
const mockMarker = new maplibregl.Marker();
|
||||
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
getClientWellKnown: jest.fn().mockReturnValue({
|
||||
[TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" },
|
||||
}),
|
||||
getUserId: jest.fn().mockReturnValue(aliceId),
|
||||
getRoom: jest.fn(),
|
||||
redactEvent: jest.fn(),
|
||||
});
|
||||
|
||||
const defaultEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1");
|
||||
|
||||
const defaultProps: ComponentProps<typeof MBeaconBody> = {
|
||||
mxEvent: defaultEvent,
|
||||
highlights: [],
|
||||
highlightLink: "",
|
||||
onHeightChanged: jest.fn(),
|
||||
onMessageAllowed: jest.fn(),
|
||||
// we dont use these and they pollute the snapshots
|
||||
permalinkCreator: {} as unknown as RoomPermalinkCreator,
|
||||
mediaEventHelper: {} as unknown as MediaEventHelper,
|
||||
};
|
||||
|
||||
const getComponent = (props = {}) =>
|
||||
render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<MBeaconBody {...defaultProps} {...props} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
const modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
finished: Promise.resolve([true]),
|
||||
close: () => {},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const testBeaconStatuses = () => {
|
||||
it("renders stopped beacon UI for an explicitly stopped beacon", () => {
|
||||
const beaconInfoEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: false }, "$alice-room1-1");
|
||||
makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient });
|
||||
const component = getComponent({ mxEvent: beaconInfoEvent });
|
||||
expect(component.container).toHaveTextContent("Live location ended");
|
||||
});
|
||||
|
||||
it("renders stopped beacon UI for an expired beacon", () => {
|
||||
const beaconInfoEvent = makeBeaconInfoEvent(
|
||||
aliceId,
|
||||
roomId,
|
||||
// puts this beacons live period in the past
|
||||
{ isLive: true, timestamp: now - 600000, timeout: 500 },
|
||||
"$alice-room1-1",
|
||||
);
|
||||
makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient });
|
||||
const component = getComponent({ mxEvent: beaconInfoEvent });
|
||||
expect(component.container).toHaveTextContent("Live location ended");
|
||||
});
|
||||
|
||||
it("renders loading beacon UI for a beacon that has not started yet", () => {
|
||||
const beaconInfoEvent = makeBeaconInfoEvent(
|
||||
aliceId,
|
||||
roomId,
|
||||
// puts this beacons start timestamp in the future
|
||||
{ isLive: true, timestamp: now + 60000, timeout: 500 },
|
||||
"$alice-room1-1",
|
||||
);
|
||||
makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient });
|
||||
const component = getComponent({ mxEvent: beaconInfoEvent });
|
||||
expect(component.container).toHaveTextContent("Loading live location…");
|
||||
});
|
||||
|
||||
it("does not open maximised map when on click when beacon is stopped", () => {
|
||||
const beaconInfoEvent = makeBeaconInfoEvent(
|
||||
aliceId,
|
||||
roomId,
|
||||
// puts this beacons live period in the past
|
||||
{ isLive: true, timestamp: now - 600000, timeout: 500 },
|
||||
"$alice-room1-1",
|
||||
);
|
||||
makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient });
|
||||
const component = getComponent({ mxEvent: beaconInfoEvent });
|
||||
fireEvent.click(component.container.querySelector(".mx_MBeaconBody_map")!);
|
||||
|
||||
expect(modalSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders stopped UI when a beacon event is not the latest beacon for a user", () => {
|
||||
const aliceBeaconInfo1 = makeBeaconInfoEvent(
|
||||
aliceId,
|
||||
roomId,
|
||||
// this one is a little older
|
||||
{ isLive: true, timestamp: now - 500 },
|
||||
"$alice-room1-1",
|
||||
);
|
||||
aliceBeaconInfo1.event.origin_server_ts = now - 500;
|
||||
const aliceBeaconInfo2 = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-2");
|
||||
|
||||
makeRoomWithStateEvents([aliceBeaconInfo1, aliceBeaconInfo2], { roomId, mockClient });
|
||||
|
||||
const component = getComponent({ mxEvent: aliceBeaconInfo1 });
|
||||
// beacon1 has been superceded by beacon2
|
||||
expect(component.container).toHaveTextContent("Live location ended");
|
||||
});
|
||||
|
||||
it("renders stopped UI when a beacon event is replaced", () => {
|
||||
const aliceBeaconInfo1 = makeBeaconInfoEvent(
|
||||
aliceId,
|
||||
roomId,
|
||||
// this one is a little older
|
||||
{ isLive: true, timestamp: now - 500 },
|
||||
"$alice-room1-1",
|
||||
);
|
||||
aliceBeaconInfo1.event.origin_server_ts = now - 500;
|
||||
const aliceBeaconInfo2 = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-2");
|
||||
|
||||
const room = makeRoomWithStateEvents([aliceBeaconInfo1], { roomId, mockClient });
|
||||
const component = getComponent({ mxEvent: aliceBeaconInfo1 });
|
||||
|
||||
const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo1))!;
|
||||
// update alice's beacon with a new edition
|
||||
// beacon instance emits
|
||||
act(() => {
|
||||
beaconInstance.update(aliceBeaconInfo2);
|
||||
});
|
||||
|
||||
// beacon1 has been superceded by beacon2
|
||||
expect(component.container).toHaveTextContent("Live location ended");
|
||||
});
|
||||
};
|
||||
|
||||
testBeaconStatuses();
|
||||
|
||||
describe("on liveness change", () => {
|
||||
it("renders stopped UI when a beacon stops being live", () => {
|
||||
const aliceBeaconInfo = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1");
|
||||
|
||||
const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient });
|
||||
const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo))!;
|
||||
const component = getComponent({ mxEvent: aliceBeaconInfo });
|
||||
|
||||
act(() => {
|
||||
// @ts-ignore cheat to force beacon to not live
|
||||
beaconInstance._isLive = false;
|
||||
beaconInstance.emit(BeaconEvent.LivenessChange, false, beaconInstance);
|
||||
});
|
||||
|
||||
// stopped UI
|
||||
expect(component.container).toHaveTextContent("Live location ended");
|
||||
});
|
||||
});
|
||||
|
||||
describe("latestLocationState", () => {
|
||||
const aliceBeaconInfo = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1");
|
||||
|
||||
const location1 = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: aliceBeaconInfo.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
timestamp: now + 1,
|
||||
});
|
||||
const location2 = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: aliceBeaconInfo.getId(),
|
||||
geoUri: "geo:52,42",
|
||||
timestamp: now + 10000,
|
||||
});
|
||||
|
||||
it("renders a live beacon without a location correctly", () => {
|
||||
makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient });
|
||||
const component = getComponent({ mxEvent: aliceBeaconInfo });
|
||||
|
||||
expect(component.container).toHaveTextContent("Loading live location…");
|
||||
});
|
||||
|
||||
it("does nothing on click when a beacon has no location", () => {
|
||||
makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient });
|
||||
const component = getComponent({ mxEvent: aliceBeaconInfo });
|
||||
|
||||
fireEvent.click(component.container.querySelector(".mx_MBeaconBody_map")!);
|
||||
|
||||
expect(modalSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders a live beacon with a location correctly", () => {
|
||||
const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient });
|
||||
const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo))!;
|
||||
beaconInstance.addLocations([location1]);
|
||||
const component = getComponent({ mxEvent: aliceBeaconInfo });
|
||||
|
||||
expect(component.container.querySelector(".maplibregl-canvas-container")).toBeDefined();
|
||||
});
|
||||
|
||||
it("opens maximised map view on click when beacon has a live location", () => {
|
||||
const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient });
|
||||
const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo))!;
|
||||
beaconInstance.addLocations([location1]);
|
||||
const component = getComponent({ mxEvent: aliceBeaconInfo });
|
||||
|
||||
fireEvent.click(component.container.querySelector(".mx_Map")!);
|
||||
|
||||
// opens modal
|
||||
expect(modalSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("updates latest location", () => {
|
||||
const room = makeRoomWithStateEvents([aliceBeaconInfo], { roomId, mockClient });
|
||||
getComponent({ mxEvent: aliceBeaconInfo });
|
||||
|
||||
const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo))!;
|
||||
act(() => {
|
||||
beaconInstance.addLocations([location1]);
|
||||
});
|
||||
|
||||
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 41 });
|
||||
expect(mockMarker.setLngLat).toHaveBeenCalledWith({ lat: 51, lon: 41 });
|
||||
|
||||
act(() => {
|
||||
beaconInstance.addLocations([location2]);
|
||||
});
|
||||
|
||||
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 52, lon: 42 });
|
||||
expect(mockMarker.setLngLat).toHaveBeenCalledWith({ lat: 52, lon: 42 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("redaction", () => {
|
||||
const makeEvents = (): {
|
||||
beaconInfoEvent: MatrixEvent;
|
||||
location1: MatrixEvent;
|
||||
location2: MatrixEvent;
|
||||
} => {
|
||||
const beaconInfoEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1");
|
||||
|
||||
const location1 = makeBeaconEvent(
|
||||
aliceId,
|
||||
{ beaconInfoId: beaconInfoEvent.getId(), geoUri: "geo:51,41", timestamp: now + 1 },
|
||||
roomId,
|
||||
);
|
||||
location1.event.event_id = "1";
|
||||
const location2 = makeBeaconEvent(
|
||||
aliceId,
|
||||
{ beaconInfoId: beaconInfoEvent.getId(), geoUri: "geo:52,42", timestamp: now + 10000 },
|
||||
roomId,
|
||||
);
|
||||
location2.event.event_id = "2";
|
||||
return { beaconInfoEvent, location1, location2 };
|
||||
};
|
||||
|
||||
const redactionEvent = new MatrixEvent({ type: EventType.RoomRedaction, content: { reason: "test reason" } });
|
||||
|
||||
const setupRoomWithBeacon = (beaconInfoEvent: MatrixEvent, locationEvents: MatrixEvent[] = []): Room => {
|
||||
const room = makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient });
|
||||
const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(beaconInfoEvent))!;
|
||||
beaconInstance.addLocations(locationEvents);
|
||||
return room;
|
||||
};
|
||||
const mockGetRelationsForEvent = (locationEvents: MatrixEvent[] = []) => {
|
||||
const relations = new Relations(RelationType.Reference, M_BEACON.name, mockClient);
|
||||
jest.spyOn(relations, "getRelations").mockReturnValue(locationEvents);
|
||||
|
||||
const getRelationsForEvent = jest.fn().mockReturnValue(relations);
|
||||
|
||||
return getRelationsForEvent;
|
||||
};
|
||||
|
||||
it("does nothing when getRelationsForEvent is falsy", () => {
|
||||
const { beaconInfoEvent, location1, location2 } = makeEvents();
|
||||
const room = setupRoomWithBeacon(beaconInfoEvent, [location1, location2]);
|
||||
|
||||
getComponent({ mxEvent: beaconInfoEvent });
|
||||
|
||||
act(() => {
|
||||
beaconInfoEvent.makeRedacted(redactionEvent, room);
|
||||
});
|
||||
|
||||
// no error, no redactions
|
||||
expect(mockClient.redactEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cleans up redaction listener on unmount", () => {
|
||||
const { beaconInfoEvent, location1, location2 } = makeEvents();
|
||||
setupRoomWithBeacon(beaconInfoEvent, [location1, location2]);
|
||||
const removeListenerSpy = jest.spyOn(beaconInfoEvent, "removeListener");
|
||||
|
||||
const component = getComponent({ mxEvent: beaconInfoEvent });
|
||||
|
||||
act(() => {
|
||||
component.unmount();
|
||||
});
|
||||
|
||||
expect(removeListenerSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing when beacon has no related locations", async () => {
|
||||
const { beaconInfoEvent } = makeEvents();
|
||||
// no locations
|
||||
const room = setupRoomWithBeacon(beaconInfoEvent, []);
|
||||
const getRelationsForEvent = await mockGetRelationsForEvent();
|
||||
|
||||
getComponent({ mxEvent: beaconInfoEvent, getRelationsForEvent });
|
||||
|
||||
act(() => {
|
||||
beaconInfoEvent.makeRedacted(redactionEvent, room);
|
||||
});
|
||||
|
||||
expect(getRelationsForEvent).toHaveBeenCalledWith(
|
||||
beaconInfoEvent.getId(),
|
||||
RelationType.Reference,
|
||||
M_BEACON.name,
|
||||
);
|
||||
expect(mockClient.redactEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("redacts related locations on beacon redaction", async () => {
|
||||
const { beaconInfoEvent, location1, location2 } = makeEvents();
|
||||
const room = setupRoomWithBeacon(beaconInfoEvent, [location1, location2]);
|
||||
|
||||
const getRelationsForEvent = await mockGetRelationsForEvent([location1, location2]);
|
||||
|
||||
getComponent({ mxEvent: beaconInfoEvent, getRelationsForEvent });
|
||||
|
||||
act(() => {
|
||||
beaconInfoEvent.makeRedacted(redactionEvent, room);
|
||||
});
|
||||
|
||||
expect(getRelationsForEvent).toHaveBeenCalledWith(
|
||||
beaconInfoEvent.getId(),
|
||||
RelationType.Reference,
|
||||
M_BEACON.name,
|
||||
);
|
||||
expect(mockClient.redactEvent).toHaveBeenCalledTimes(2);
|
||||
expect(mockClient.redactEvent).toHaveBeenCalledWith(roomId, location1.getId(), undefined, {
|
||||
reason: "test reason",
|
||||
});
|
||||
expect(mockClient.redactEvent).toHaveBeenCalledWith(roomId, location2.getId(), undefined, {
|
||||
reason: "test reason",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when map display is not configured", () => {
|
||||
beforeEach(() => {
|
||||
// mock map utils to raise MapStyleUrlNotConfigured error
|
||||
jest.spyOn(mapUtilHooks, "useMap").mockImplementation(({ onError }) => {
|
||||
onError?.(new Error(LocationShareError.MapStyleUrlNotConfigured));
|
||||
return mockMap;
|
||||
});
|
||||
});
|
||||
|
||||
it("renders maps unavailable error for a live beacon with location", () => {
|
||||
const beaconInfoEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true }, "$alice-room1-1");
|
||||
const location1 = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: beaconInfoEvent.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
timestamp: now + 1,
|
||||
});
|
||||
|
||||
makeRoomWithBeacons(roomId, mockClient, [beaconInfoEvent], [location1]);
|
||||
|
||||
const component = getComponent({ mxEvent: beaconInfoEvent });
|
||||
expect(component.getByTestId("map-rendering-error")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// test that statuses display as expected with a map display error
|
||||
testBeaconStatuses();
|
||||
});
|
||||
});
|
88
test/unit-tests/components/views/messages/MFileBody-test.tsx
Normal file
88
test/unit-tests/components/views/messages/MFileBody-test.tsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
import { EventType, getHttpUriForMxc, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsCrypto,
|
||||
mockClientMethodsDevice,
|
||||
mockClientMethodsServer,
|
||||
mockClientMethodsUser,
|
||||
} from "../../../../test-utils";
|
||||
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import MFileBody from "../../../../../src/components/views/messages/MFileBody.tsx";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext.ts";
|
||||
|
||||
jest.mock("matrix-encrypt-attachment", () => ({
|
||||
decryptAttachment: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<MFileBody/>", () => {
|
||||
const userId = "@user:server";
|
||||
const deviceId = "DEADB33F";
|
||||
const cli = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsServer(),
|
||||
...mockClientMethodsDevice(deviceId),
|
||||
...mockClientMethodsCrypto(),
|
||||
getRooms: jest.fn().mockReturnValue([]),
|
||||
getIgnoredUsers: jest.fn(),
|
||||
getVersions: jest.fn().mockResolvedValue({
|
||||
unstable_features: {
|
||||
"org.matrix.msc3882": true,
|
||||
"org.matrix.msc3886": true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
cli.mxcUrlToHttp.mockImplementation(
|
||||
(mxcUrl: string, width?: number, height?: number, resizeMethod?: string, allowDirectLinks?: boolean) => {
|
||||
return getHttpUriForMxc("https://server", mxcUrl, width, height, resizeMethod, allowDirectLinks);
|
||||
},
|
||||
);
|
||||
const mediaEvent = new MatrixEvent({
|
||||
room_id: "!room:server",
|
||||
sender: userId,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "alt for a image",
|
||||
msgtype: "m.image",
|
||||
url: "mxc://server/image",
|
||||
},
|
||||
});
|
||||
|
||||
const props = {
|
||||
onHeightChanged: jest.fn(),
|
||||
onMessageAllowed: jest.fn(),
|
||||
permalinkCreator: new RoomPermalinkCreator(new Room(mediaEvent.getRoomId()!, cli, cli.getUserId()!)),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockRestore();
|
||||
});
|
||||
|
||||
it("should show a download button in file rendering type", async () => {
|
||||
const { container, getByRole } = render(
|
||||
<RoomContext.Provider value={{ timelineRenderingType: TimelineRenderingType.File } as any}>
|
||||
<MFileBody
|
||||
{...props}
|
||||
mxEvent={mediaEvent}
|
||||
mediaEventHelper={new MediaEventHelper(mediaEvent)}
|
||||
showGenericPlaceholder={false}
|
||||
/>
|
||||
</RoomContext.Provider>,
|
||||
);
|
||||
|
||||
expect(getByRole("link", { name: "Download" })).toBeInTheDocument();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
279
test/unit-tests/components/views/messages/MImageBody-test.tsx
Normal file
279
test/unit-tests/components/views/messages/MImageBody-test.tsx
Normal file
|
@ -0,0 +1,279 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, screen, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||
import { EventType, getHttpUriForMxc, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import encrypt from "matrix-encrypt-attachment";
|
||||
import { mocked } from "jest-mock";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import MImageBody from "../../../../../src/components/views/messages/MImageBody";
|
||||
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsCrypto,
|
||||
mockClientMethodsDevice,
|
||||
mockClientMethodsServer,
|
||||
mockClientMethodsUser,
|
||||
} from "../../../../test-utils";
|
||||
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
|
||||
jest.mock("matrix-encrypt-attachment", () => ({
|
||||
decryptAttachment: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<MImageBody/>", () => {
|
||||
const userId = "@user:server";
|
||||
const deviceId = "DEADB33F";
|
||||
const cli = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsServer(),
|
||||
...mockClientMethodsDevice(deviceId),
|
||||
...mockClientMethodsCrypto(),
|
||||
getRooms: jest.fn().mockReturnValue([]),
|
||||
getIgnoredUsers: jest.fn(),
|
||||
getVersions: jest.fn().mockResolvedValue({
|
||||
unstable_features: {
|
||||
"org.matrix.msc3882": true,
|
||||
"org.matrix.msc3886": true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const url = "https://server/_matrix/media/v3/download/server/encrypted-image";
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
cli.mxcUrlToHttp.mockImplementation(
|
||||
(mxcUrl: string, width?: number, height?: number, resizeMethod?: string, allowDirectLinks?: boolean) => {
|
||||
return getHttpUriForMxc("https://server", mxcUrl, width, height, resizeMethod, allowDirectLinks);
|
||||
},
|
||||
);
|
||||
const encryptedMediaEvent = new MatrixEvent({
|
||||
room_id: "!room:server",
|
||||
sender: userId,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "alt for a test image",
|
||||
info: {
|
||||
w: 40,
|
||||
h: 50,
|
||||
},
|
||||
file: {
|
||||
url: "mxc://server/encrypted-image",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const props = {
|
||||
onHeightChanged: jest.fn(),
|
||||
onMessageAllowed: jest.fn(),
|
||||
permalinkCreator: new RoomPermalinkCreator(new Room(encryptedMediaEvent.getRoomId()!, cli, cli.getUserId()!)),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockRestore();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
it("should show a thumbnail while image is being downloaded", async () => {
|
||||
fetchMock.getOnce(url, { status: 200 });
|
||||
|
||||
const { container } = render(
|
||||
<MImageBody
|
||||
{...props}
|
||||
mxEvent={encryptedMediaEvent}
|
||||
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
||||
/>,
|
||||
);
|
||||
|
||||
// thumbnail with dimensions present
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should show error when encrypted media cannot be downloaded", async () => {
|
||||
fetchMock.getOnce(url, { status: 500 });
|
||||
|
||||
render(
|
||||
<MImageBody
|
||||
{...props}
|
||||
mxEvent={encryptedMediaEvent}
|
||||
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(url);
|
||||
|
||||
await screen.findByText("Error downloading image");
|
||||
});
|
||||
|
||||
it("should show error when encrypted media cannot be decrypted", async () => {
|
||||
fetchMock.getOnce(url, "thisistotallyanencryptedpng");
|
||||
mocked(encrypt.decryptAttachment).mockRejectedValue(new Error("Failed to decrypt"));
|
||||
|
||||
render(
|
||||
<MImageBody
|
||||
{...props}
|
||||
mxEvent={encryptedMediaEvent}
|
||||
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText("Error decrypting image");
|
||||
});
|
||||
|
||||
describe("with image previews/thumbnails disabled", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("should not download image", async () => {
|
||||
fetchMock.getOnce(url, { status: 200 });
|
||||
|
||||
render(
|
||||
<MImageBody
|
||||
{...props}
|
||||
mxEvent={encryptedMediaEvent}
|
||||
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(fetchMock).not.toHaveFetched(url);
|
||||
});
|
||||
|
||||
it("should render hidden image placeholder", async () => {
|
||||
fetchMock.getOnce(url, { status: 200 });
|
||||
|
||||
render(
|
||||
<MImageBody
|
||||
{...props}
|
||||
mxEvent={encryptedMediaEvent}
|
||||
mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Show image")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
|
||||
// image fetched after clicking show image
|
||||
expect(fetchMock).toHaveFetched(url);
|
||||
|
||||
// spinner while downloading image
|
||||
expect(screen.getByRole("progressbar")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should fall back to /download/ if /thumbnail/ fails", async () => {
|
||||
const thumbUrl = "https://server/_matrix/media/v3/thumbnail/server/image?width=800&height=600&method=scale";
|
||||
const downloadUrl = "https://server/_matrix/media/v3/download/server/image";
|
||||
|
||||
const event = new MatrixEvent({
|
||||
room_id: "!room:server",
|
||||
sender: userId,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "alt for a test image",
|
||||
info: {
|
||||
w: 40,
|
||||
h: 50,
|
||||
},
|
||||
url: "mxc://server/image",
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<MImageBody {...props} mxEvent={event} mediaEventHelper={new MediaEventHelper(event)} />,
|
||||
);
|
||||
|
||||
const img = container.querySelector(".mx_MImageBody_thumbnail")!;
|
||||
expect(img).toHaveProperty("src", thumbUrl);
|
||||
|
||||
fireEvent.error(img);
|
||||
expect(img).toHaveProperty("src", downloadUrl);
|
||||
});
|
||||
|
||||
it("should generate a thumbnail if one isn't included for animated media", async () => {
|
||||
Object.defineProperty(global.Image.prototype, "src", {
|
||||
set(src) {
|
||||
window.setTimeout(() => this.onload?.());
|
||||
},
|
||||
});
|
||||
Object.defineProperty(global.Image.prototype, "height", {
|
||||
get() {
|
||||
return 600;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(global.Image.prototype, "width", {
|
||||
get() {
|
||||
return 800;
|
||||
},
|
||||
});
|
||||
|
||||
mocked(global.URL.createObjectURL).mockReturnValue("blob:generated-thumb");
|
||||
|
||||
fetchMock.getOnce(
|
||||
"https://server/_matrix/media/v3/download/server/image",
|
||||
{
|
||||
body: fs.readFileSync(path.resolve(__dirname, "..", "..", "..", "images", "animated-logo.webp")),
|
||||
},
|
||||
{ sendAsJson: false },
|
||||
);
|
||||
|
||||
const event = new MatrixEvent({
|
||||
room_id: "!room:server",
|
||||
sender: userId,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "alt for a test image",
|
||||
info: {
|
||||
w: 40,
|
||||
h: 50,
|
||||
mimetype: "image/webp",
|
||||
},
|
||||
url: "mxc://server/image",
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<MImageBody {...props} mxEvent={event} mediaEventHelper={new MediaEventHelper(event)} />,
|
||||
);
|
||||
|
||||
// Wait for spinners to go away
|
||||
await waitForElementToBeRemoved(screen.getAllByRole("progressbar"));
|
||||
// thumbnail with dimensions present
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should show banner on hover", async () => {
|
||||
const event = new MatrixEvent({
|
||||
room_id: "!room:server",
|
||||
sender: userId,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "alt for a test image",
|
||||
info: {
|
||||
w: 40,
|
||||
h: 50,
|
||||
},
|
||||
url: "mxc://server/image",
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<MImageBody {...props} mxEvent={event} mediaEventHelper={new MediaEventHelper(event)} />,
|
||||
);
|
||||
|
||||
const img = container.querySelector(".mx_MImageBody_thumbnail")!;
|
||||
await userEvent.hover(img);
|
||||
|
||||
expect(container.querySelector(".mx_MImageBody_banner")).toHaveTextContent("...alt for a test image");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { RenderResult, render } from "jest-matrix-react";
|
||||
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import MKeyVerificationRequest from "../../../../../src/components/views/messages/MKeyVerificationRequest";
|
||||
import TileErrorBoundary from "../../../../../src/components/views/messages/TileErrorBoundary";
|
||||
import { Layout } from "../../../../../src/settings/enums/Layout";
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
function renderEventNoClient(event: MatrixEvent): RenderResult {
|
||||
return render(
|
||||
<TileErrorBoundary mxEvent={event} layout={Layout.Group}>
|
||||
<MKeyVerificationRequest mxEvent={event} />
|
||||
</TileErrorBoundary>,
|
||||
);
|
||||
}
|
||||
|
||||
function renderEvent(client: MatrixClient, event: MatrixEvent): RenderResult {
|
||||
return render(
|
||||
<TileErrorBoundary mxEvent={event} layout={Layout.Group}>
|
||||
<MatrixClientContext.Provider value={client}>
|
||||
<MKeyVerificationRequest mxEvent={event} />
|
||||
</MatrixClientContext.Provider>
|
||||
,
|
||||
</TileErrorBoundary>,
|
||||
);
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
162
test/unit-tests/components/views/messages/MLocationBody-test.tsx
Normal file
162
test/unit-tests/components/views/messages/MLocationBody-test.tsx
Normal file
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, waitFor } from "jest-matrix-react";
|
||||
import { LocationAssetType, ClientEvent, RoomMember, SyncState } from "matrix-js-sdk/src/matrix";
|
||||
import * as maplibregl from "maplibre-gl";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import MLocationBody from "../../../../../src/components/views/messages/MLocationBody";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
||||
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
|
||||
import Modal from "../../../../../src/Modal";
|
||||
import SdkConfig from "../../../../../src/SdkConfig";
|
||||
import { TILE_SERVER_WK_KEY } from "../../../../../src/utils/WellKnownUtils";
|
||||
import { makeLocationEvent } from "../../../../test-utils/location";
|
||||
import { getMockClientWithEventEmitter } from "../../../../test-utils";
|
||||
|
||||
describe("MLocationBody", () => {
|
||||
const mapOptions = { container: {} as unknown as HTMLElement, style: "" };
|
||||
describe("<MLocationBody>", () => {
|
||||
const roomId = "!room:server";
|
||||
const userId = "@user:server";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
getClientWellKnown: jest.fn().mockReturnValue({
|
||||
[TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" },
|
||||
}),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
});
|
||||
const defaultEvent = makeLocationEvent("geo:51.5076,-0.1276", LocationAssetType.Pin);
|
||||
const defaultProps: MLocationBody["props"] = {
|
||||
mxEvent: defaultEvent,
|
||||
highlights: [],
|
||||
highlightLink: "",
|
||||
onHeightChanged: jest.fn(),
|
||||
onMessageAllowed: jest.fn(),
|
||||
permalinkCreator: {} as RoomPermalinkCreator,
|
||||
mediaEventHelper: {} as MediaEventHelper,
|
||||
};
|
||||
const getComponent = (props = {}) =>
|
||||
render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<MLocationBody {...defaultProps} {...props} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
const getMapErrorComponent = () => {
|
||||
const mockMap = new maplibregl.Map(mapOptions);
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
[TILE_SERVER_WK_KEY.name]: { map_style_url: "bad-tile-server.com" },
|
||||
});
|
||||
const component = getComponent();
|
||||
|
||||
sleep(10).then(() => {
|
||||
// simulate error initialising map in maplibregl
|
||||
// @ts-ignore
|
||||
mockMap.emit("error", { status: 404 });
|
||||
});
|
||||
|
||||
return component;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("with error", () => {
|
||||
let sdkConfigSpy: jest.SpyInstance<any>;
|
||||
|
||||
beforeEach(() => {
|
||||
// eat expected errors to keep console clean
|
||||
jest.spyOn(logger, "error").mockImplementation(() => {});
|
||||
mockClient.getClientWellKnown.mockReturnValue({});
|
||||
sdkConfigSpy = jest.spyOn(SdkConfig, "get").mockReturnValue({});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
sdkConfigSpy.mockRestore();
|
||||
jest.spyOn(logger, "error").mockRestore();
|
||||
});
|
||||
|
||||
it("displays correct fallback content without error style when map_style_url is not configured", async () => {
|
||||
const component = getComponent();
|
||||
|
||||
// The map code needs to be lazy loaded so this will take some time to appear
|
||||
await waitFor(() =>
|
||||
expect(component.container.querySelector(".mx_EventTile_body")).toBeInTheDocument(),
|
||||
);
|
||||
expect(component.container.querySelector(".mx_EventTile_body")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("displays correct fallback content when map_style_url is misconfigured", async () => {
|
||||
const component = getMapErrorComponent();
|
||||
await waitFor(() => expect(component.container.querySelector(".mx_EventTile_body")).toBeTruthy());
|
||||
await waitFor(() => expect(component.container.querySelector(".mx_EventTile_body")).toMatchSnapshot());
|
||||
});
|
||||
|
||||
it("should clear the error on reconnect", () => {
|
||||
const component = getMapErrorComponent();
|
||||
expect(component.container.querySelector(".mx_EventTile_tileError")).toBeDefined();
|
||||
mockClient.emit(ClientEvent.Sync, SyncState.Reconnecting, SyncState.Error);
|
||||
expect(component.container.querySelector(".mx_EventTile_tileError")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("without error", () => {
|
||||
beforeEach(() => {
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
[TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" },
|
||||
});
|
||||
|
||||
// MLocationBody uses random number for map id
|
||||
// stabilise for test
|
||||
jest.spyOn(global.Math, "random").mockReturnValue(0.123456);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(global.Math, "random").mockRestore();
|
||||
});
|
||||
|
||||
it("renders map correctly", () => {
|
||||
const mockMap = new maplibregl.Map(mapOptions);
|
||||
const component = getComponent();
|
||||
|
||||
expect(component.asFragment()).toMatchSnapshot();
|
||||
// map was centered
|
||||
expect(mockMap.setCenter).toHaveBeenCalledWith({
|
||||
lat: 51.5076,
|
||||
lon: -0.1276,
|
||||
});
|
||||
});
|
||||
|
||||
it("opens map dialog on click", async () => {
|
||||
const modalSpy = jest
|
||||
.spyOn(Modal, "createDialog")
|
||||
.mockReturnValue({ finished: new Promise(() => {}), close: jest.fn() });
|
||||
const component = getComponent();
|
||||
|
||||
await fireEvent.click(component.container.querySelector(".mx_Map")!);
|
||||
|
||||
expect(modalSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders marker correctly for a self share", () => {
|
||||
const selfShareEvent = makeLocationEvent("geo:51.5076,-0.1276", LocationAssetType.Self);
|
||||
const member = new RoomMember(roomId, userId);
|
||||
// @ts-ignore cheat assignment to property
|
||||
selfShareEvent.sender = member;
|
||||
const component = getComponent({ mxEvent: selfShareEvent });
|
||||
|
||||
// render self locations with user avatars
|
||||
expect(component.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
1065
test/unit-tests/components/views/messages/MPollBody-test.tsx
Normal file
1065
test/unit-tests/components/views/messages/MPollBody-test.tsx
Normal file
File diff suppressed because it is too large
Load diff
193
test/unit-tests/components/views/messages/MPollEndBody-test.tsx
Normal file
193
test/unit-tests/components/views/messages/MPollEndBody-test.tsx
Normal file
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, waitFor } from "jest-matrix-react";
|
||||
import { EventTimeline, MatrixEvent, Room, M_TEXT } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { IBodyProps } from "../../../../../src/components/views/messages/IBodyProps";
|
||||
import { MPollEndBody } from "../../../../../src/components/views/messages/MPollEndBody";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
||||
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
|
||||
import {
|
||||
flushPromises,
|
||||
getMockClientWithEventEmitter,
|
||||
makePollEndEvent,
|
||||
makePollStartEvent,
|
||||
mockClientMethodsEvents,
|
||||
mockClientMethodsUser,
|
||||
setupRoomWithPollEvents,
|
||||
} from "../../../../test-utils";
|
||||
|
||||
describe("<MPollEndBody />", () => {
|
||||
const userId = "@alice:domain.org";
|
||||
const roomId = "!room:domain.org";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsEvents(),
|
||||
getRoom: jest.fn(),
|
||||
relations: jest.fn(),
|
||||
fetchRoomEvent: jest.fn(),
|
||||
});
|
||||
const pollStartEvent = makePollStartEvent("Question?", userId, undefined, { roomId });
|
||||
const pollEndEvent = makePollEndEvent(pollStartEvent.getId()!, roomId, userId, 123);
|
||||
|
||||
const setupRoomWithEventsTimeline = async (pollEnd: MatrixEvent, pollStart?: MatrixEvent): Promise<Room> => {
|
||||
if (pollStart) {
|
||||
await setupRoomWithPollEvents([pollStart], [], [pollEnd], mockClient);
|
||||
}
|
||||
const room = mockClient.getRoom(roomId) || new Room(roomId, mockClient, userId);
|
||||
|
||||
// end events validate against this
|
||||
jest.spyOn(room.currentState, "maySendRedactionForEvent").mockImplementation(
|
||||
(_evt: MatrixEvent, id: string) => {
|
||||
return id === mockClient.getSafeUserId();
|
||||
},
|
||||
);
|
||||
|
||||
const timelineSet = room.getUnfilteredTimelineSet();
|
||||
const getTimelineForEventSpy = jest.spyOn(timelineSet, "getTimelineForEvent");
|
||||
// if we have a pollStart, mock the room timeline to include it
|
||||
if (pollStart) {
|
||||
const eventTimeline = {
|
||||
getEvents: jest.fn().mockReturnValue([pollEnd, pollStart]),
|
||||
} as unknown as EventTimeline;
|
||||
getTimelineForEventSpy.mockReturnValue(eventTimeline);
|
||||
}
|
||||
mockClient.getRoom.mockReturnValue(room);
|
||||
|
||||
return room;
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
mxEvent: pollEndEvent,
|
||||
highlightLink: "unused",
|
||||
mediaEventHelper: {} as unknown as MediaEventHelper,
|
||||
onHeightChanged: () => {},
|
||||
onMessageAllowed: () => {},
|
||||
permalinkCreator: {} as unknown as RoomPermalinkCreator,
|
||||
ref: undefined as any,
|
||||
};
|
||||
|
||||
const getComponent = (props: Partial<IBodyProps> = {}) =>
|
||||
render(<MPollEndBody {...defaultProps} {...props} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<MatrixClientContext.Provider value={mockClient}>{children}</MatrixClientContext.Provider>
|
||||
),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.getRoom.mockReset();
|
||||
mockClient.relations.mockResolvedValue({
|
||||
events: [],
|
||||
});
|
||||
mockClient.fetchRoomEvent.mockResolvedValue(pollStartEvent.getEffectiveEvent());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.spyOn(logger, "error").mockRestore();
|
||||
});
|
||||
|
||||
describe("when poll start event exists in current timeline", () => {
|
||||
it("renders an ended poll", async () => {
|
||||
await setupRoomWithEventsTimeline(pollEndEvent, pollStartEvent);
|
||||
const { container } = getComponent();
|
||||
|
||||
// ended poll rendered
|
||||
expect(container).toMatchSnapshot();
|
||||
|
||||
// didnt try to fetch start event while it was already in timeline
|
||||
expect(mockClient.fetchRoomEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not render a poll tile when end event is invalid", async () => {
|
||||
// sender of end event does not match start event
|
||||
const invalidEndEvent = makePollEndEvent(pollStartEvent.getId()!, roomId, "@mallory:domain.org", 123);
|
||||
await setupRoomWithEventsTimeline(invalidEndEvent, pollStartEvent);
|
||||
const { getByText } = getComponent({ mxEvent: invalidEndEvent });
|
||||
|
||||
// no poll tile rendered
|
||||
expect(getByText("The poll has ended. Something.")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when poll start event does not exist in current timeline", () => {
|
||||
it("fetches the related poll start event and displays a poll tile", async () => {
|
||||
await setupRoomWithEventsTimeline(pollEndEvent);
|
||||
const { container, getByTestId, getByRole } = getComponent();
|
||||
|
||||
// while fetching event, only icon is shown
|
||||
expect(container).toMatchSnapshot();
|
||||
|
||||
await waitFor(() => expect(getByRole("progressbar")).toBeInTheDocument());
|
||||
|
||||
expect(mockClient.fetchRoomEvent).toHaveBeenCalledWith(roomId, pollStartEvent.getId());
|
||||
|
||||
// quick check for poll tile
|
||||
expect(getByTestId("pollQuestion").innerHTML).toEqual("Question?");
|
||||
expect(getByTestId("totalVotes").innerHTML).toEqual("Final result based on 0 votes");
|
||||
});
|
||||
|
||||
it("does not render a poll tile when end event is invalid", async () => {
|
||||
// sender of end event does not match start event
|
||||
const invalidEndEvent = makePollEndEvent(pollStartEvent.getId()!, roomId, "@mallory:domain.org", 123);
|
||||
await setupRoomWithEventsTimeline(invalidEndEvent);
|
||||
const { getByText } = getComponent({ mxEvent: invalidEndEvent });
|
||||
|
||||
// flush the fetch event promise
|
||||
await flushPromises();
|
||||
|
||||
// no poll tile rendered
|
||||
expect(getByText("The poll has ended. Something.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("logs an error and displays the text fallback when fetching the start event fails", async () => {
|
||||
await setupRoomWithEventsTimeline(pollEndEvent);
|
||||
mockClient.fetchRoomEvent.mockRejectedValue({ code: 404 });
|
||||
const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {});
|
||||
const { getByText } = getComponent();
|
||||
|
||||
// flush the fetch event promise
|
||||
await flushPromises();
|
||||
|
||||
// poll end event fallback text used
|
||||
expect(getByText("The poll has ended. Something.")).toBeTruthy();
|
||||
expect(logSpy).toHaveBeenCalledWith("Failed to fetch related poll start event", { code: 404 });
|
||||
});
|
||||
|
||||
it("logs an error and displays the extensible event text when fetching the start event fails", async () => {
|
||||
await setupRoomWithEventsTimeline(pollEndEvent);
|
||||
mockClient.fetchRoomEvent.mockRejectedValue({ code: 404 });
|
||||
const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {});
|
||||
const { getByText } = getComponent();
|
||||
|
||||
// flush the fetch event promise
|
||||
await flushPromises();
|
||||
|
||||
// poll end event fallback text used
|
||||
expect(getByText("The poll has ended. Something.")).toBeTruthy();
|
||||
expect(logSpy).toHaveBeenCalledWith("Failed to fetch related poll start event", { code: 404 });
|
||||
});
|
||||
|
||||
it("displays fallback text when the poll end event does not have text", async () => {
|
||||
const endWithoutText = makePollEndEvent(pollStartEvent.getId()!, roomId, userId, 123);
|
||||
delete endWithoutText.getContent()[M_TEXT.name];
|
||||
await setupRoomWithEventsTimeline(endWithoutText);
|
||||
mockClient.fetchRoomEvent.mockRejectedValue({ code: 404 });
|
||||
const { getByText } = getComponent({ mxEvent: endWithoutText });
|
||||
|
||||
// flush the fetch event promise
|
||||
await flushPromises();
|
||||
|
||||
// default fallback text used
|
||||
expect(getByText("@alice:domain.org has ended a poll")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { EventType, getHttpUriForMxc, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsCrypto,
|
||||
mockClientMethodsDevice,
|
||||
mockClientMethodsServer,
|
||||
mockClientMethodsUser,
|
||||
} from "../../../../test-utils";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import MStickerBody from "../../../../../src/components/views/messages/MStickerBody";
|
||||
|
||||
describe("<MStickerBody/>", () => {
|
||||
const userId = "@user:server";
|
||||
const deviceId = "DEADB33F";
|
||||
const cli = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsServer(),
|
||||
...mockClientMethodsDevice(deviceId),
|
||||
...mockClientMethodsCrypto(),
|
||||
getRooms: jest.fn().mockReturnValue([]),
|
||||
getIgnoredUsers: jest.fn(),
|
||||
getVersions: jest.fn().mockResolvedValue({
|
||||
unstable_features: {
|
||||
"org.matrix.msc3882": true,
|
||||
"org.matrix.msc3886": true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const url = "https://server/_matrix/media/v3/download/server/sticker";
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
cli.mxcUrlToHttp.mockImplementation(
|
||||
(mxcUrl: string, width?: number, height?: number, resizeMethod?: string, allowDirectLinks?: boolean) => {
|
||||
return getHttpUriForMxc("https://server", mxcUrl, width, height, resizeMethod, allowDirectLinks);
|
||||
},
|
||||
);
|
||||
const mediaEvent = new MatrixEvent({
|
||||
room_id: "!room:server",
|
||||
sender: userId,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "sticker description",
|
||||
info: {
|
||||
w: 40,
|
||||
h: 50,
|
||||
},
|
||||
file: {
|
||||
url: "mxc://server/sticker",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const props = {
|
||||
onHeightChanged: jest.fn(),
|
||||
onMessageAllowed: jest.fn(),
|
||||
permalinkCreator: new RoomPermalinkCreator(new Room(mediaEvent.getRoomId()!, cli, cli.getUserId()!)),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockRestore();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
it("should show a tooltip on hover", async () => {
|
||||
fetchMock.getOnce(url, { status: 200 });
|
||||
|
||||
render(<MStickerBody {...props} mxEvent={mediaEvent} />);
|
||||
|
||||
expect(screen.queryByRole("tooltip")).toBeNull();
|
||||
await userEvent.hover(screen.getByRole("img"));
|
||||
await expect(screen.findByRole("tooltip")).resolves.toHaveTextContent("sticker description");
|
||||
});
|
||||
});
|
131
test/unit-tests/components/views/messages/MVideoBody-test.tsx
Normal file
131
test/unit-tests/components/views/messages/MVideoBody-test.tsx
Normal file
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { EventType, getHttpUriForMxc, IContent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { render, RenderResult } from "jest-matrix-react";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
||||
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsCrypto,
|
||||
mockClientMethodsDevice,
|
||||
mockClientMethodsServer,
|
||||
mockClientMethodsUser,
|
||||
} from "../../../../test-utils";
|
||||
import MVideoBody from "../../../../../src/components/views/messages/MVideoBody";
|
||||
|
||||
describe("MVideoBody", () => {
|
||||
it("does not crash when given a portrait image", () => {
|
||||
// Check for an unreliable crash caused by a fractional-sized
|
||||
// image dimension being used for a CanvasImageData.
|
||||
const { asFragment } = makeMVideoBody(720, 1280);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
// If we get here, we did not crash.
|
||||
});
|
||||
|
||||
it("should show poster for encrypted media before downloading it", async () => {
|
||||
const userId = "@user:server";
|
||||
const deviceId = "DEADB33F";
|
||||
const cli = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsServer(),
|
||||
...mockClientMethodsDevice(deviceId),
|
||||
...mockClientMethodsCrypto(),
|
||||
getRooms: jest.fn().mockReturnValue([]),
|
||||
getIgnoredUsers: jest.fn(),
|
||||
getVersions: jest.fn().mockResolvedValue({
|
||||
unstable_features: {
|
||||
"org.matrix.msc3882": true,
|
||||
"org.matrix.msc3886": true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const thumbUrl = "https://server/_matrix/media/v3/download/server/encrypted-poster";
|
||||
fetchMock.getOnce(thumbUrl, { status: 200 });
|
||||
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
cli.mxcUrlToHttp.mockImplementation(
|
||||
(mxcUrl: string, width?: number, height?: number, resizeMethod?: string, allowDirectLinks?: boolean) => {
|
||||
return getHttpUriForMxc("https://server", mxcUrl, width, height, resizeMethod, allowDirectLinks);
|
||||
},
|
||||
);
|
||||
const encryptedMediaEvent = new MatrixEvent({
|
||||
room_id: "!room:server",
|
||||
sender: userId,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "alt for a test video",
|
||||
info: {
|
||||
duration: 420,
|
||||
w: 40,
|
||||
h: 50,
|
||||
thumbnail_file: {
|
||||
url: "mxc://server/encrypted-poster",
|
||||
},
|
||||
},
|
||||
file: {
|
||||
url: "mxc://server/encrypted-image",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { asFragment } = render(
|
||||
<MVideoBody mxEvent={encryptedMediaEvent} mediaEventHelper={new MediaEventHelper(encryptedMediaEvent)} />,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
function makeMVideoBody(w: number, h: number): RenderResult {
|
||||
const content: IContent = {
|
||||
info: {
|
||||
"w": w,
|
||||
"h": h,
|
||||
"mimetype": "video/mp4",
|
||||
"size": 2495675,
|
||||
"thumbnail_file": {
|
||||
url: "",
|
||||
key: { alg: "", key_ops: [], kty: "", k: "", ext: true },
|
||||
iv: "",
|
||||
hashes: {},
|
||||
v: "",
|
||||
},
|
||||
"thumbnail_info": { mimetype: "" },
|
||||
"xyz.amorgan.blurhash": "TrGl6bofof~paxWC?bj[oL%2fPj]",
|
||||
},
|
||||
url: "http://example.com",
|
||||
};
|
||||
|
||||
const event = new MatrixEvent({
|
||||
content,
|
||||
});
|
||||
|
||||
const defaultProps: MVideoBody["props"] = {
|
||||
mxEvent: event,
|
||||
highlights: [],
|
||||
highlightLink: "",
|
||||
onHeightChanged: jest.fn(),
|
||||
onMessageAllowed: jest.fn(),
|
||||
permalinkCreator: {} as RoomPermalinkCreator,
|
||||
mediaEventHelper: { media: { isEncrypted: false } } as MediaEventHelper,
|
||||
};
|
||||
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
mxcUrlToHttp: jest.fn(),
|
||||
});
|
||||
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<MVideoBody {...defaultProps} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,519 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { act, render, fireEvent, screen, waitFor } from "jest-matrix-react";
|
||||
import {
|
||||
EventType,
|
||||
EventStatus,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
MsgType,
|
||||
Room,
|
||||
FeatureSupport,
|
||||
Thread,
|
||||
EventTimeline,
|
||||
RoomStateEvent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import MessageActionBar from "../../../../../src/components/views/messages/MessageActionBar";
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
mockClientMethodsUser,
|
||||
mockClientMethodsEvents,
|
||||
makeBeaconInfoEvent,
|
||||
} from "../../../../test-utils";
|
||||
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
|
||||
import { IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||
import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import PinningUtils from "../../../../../src/utils/PinningUtils";
|
||||
|
||||
jest.mock("../../../../../src/dispatcher/dispatcher");
|
||||
|
||||
describe("<MessageActionBar />", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const roomId = "!room:server.org";
|
||||
|
||||
const client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
...mockClientMethodsEvents(),
|
||||
getRoom: jest.fn(),
|
||||
setRoomAccountData: jest.fn(),
|
||||
sendStateEvent: jest.fn(),
|
||||
});
|
||||
const room = new Room(roomId, client, userId);
|
||||
|
||||
const alicesMessageEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
room_id: roomId,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Hello",
|
||||
},
|
||||
event_id: "$alices_message",
|
||||
});
|
||||
|
||||
const bobsMessageEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: "@bob:server.org",
|
||||
room_id: roomId,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "I am bob",
|
||||
},
|
||||
event_id: "$bobs_message",
|
||||
});
|
||||
|
||||
const redactedEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
});
|
||||
redactedEvent.makeRedacted(redactedEvent, room);
|
||||
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, any> = {};
|
||||
return {
|
||||
getItem: jest.fn().mockImplementation((key) => store[key] ?? null),
|
||||
setItem: jest.fn().mockImplementation((key, value) => {
|
||||
store[key] = value;
|
||||
}),
|
||||
clear: jest.fn().mockImplementation(() => {
|
||||
store = {};
|
||||
}),
|
||||
removeItem: jest.fn().mockImplementation((key) => delete store[key]),
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
value: localStorageMock,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
jest.spyOn(room, "getPendingEvents").mockReturnValue([]);
|
||||
|
||||
client.getRoom.mockReturnValue(room);
|
||||
|
||||
const defaultProps = {
|
||||
getTile: jest.fn(),
|
||||
getReplyChain: jest.fn(),
|
||||
toggleThreadExpanded: jest.fn(),
|
||||
mxEvent: alicesMessageEvent,
|
||||
permalinkCreator: new RoomPermalinkCreator(room),
|
||||
};
|
||||
const defaultRoomContext = {
|
||||
...RoomContext,
|
||||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
canSendMessages: true,
|
||||
canReact: true,
|
||||
room,
|
||||
} as unknown as IRoomState;
|
||||
const getComponent = (props = {}, roomContext: Partial<IRoomState> = {}) =>
|
||||
render(
|
||||
<RoomContext.Provider value={{ ...defaultRoomContext, ...roomContext }}>
|
||||
<MessageActionBar {...defaultProps} {...props} />
|
||||
</RoomContext.Provider>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
alicesMessageEvent.setStatus(EventStatus.SENT);
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockRestore();
|
||||
jest.spyOn(SettingsStore, "setValue").mockRestore();
|
||||
});
|
||||
|
||||
it("kills event listeners on unmount", () => {
|
||||
const offSpy = jest.spyOn(alicesMessageEvent, "off").mockClear();
|
||||
const wrapper = getComponent({ mxEvent: alicesMessageEvent });
|
||||
|
||||
act(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
expect(offSpy.mock.calls[0][0]).toEqual(MatrixEventEvent.Status);
|
||||
expect(offSpy.mock.calls[1][0]).toEqual(MatrixEventEvent.Decrypted);
|
||||
expect(offSpy.mock.calls[2][0]).toEqual(MatrixEventEvent.BeforeRedaction);
|
||||
|
||||
expect(client.decryptEventIfNeeded).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("decryption", () => {
|
||||
it("decrypts event if needed", () => {
|
||||
getComponent({ mxEvent: alicesMessageEvent });
|
||||
expect(client.decryptEventIfNeeded).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("updates component on decrypted event", () => {
|
||||
const decryptingEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
sender: userId,
|
||||
room_id: roomId,
|
||||
content: {},
|
||||
});
|
||||
jest.spyOn(decryptingEvent, "isBeingDecrypted").mockReturnValue(true);
|
||||
const { queryByLabelText } = getComponent({ mxEvent: decryptingEvent });
|
||||
|
||||
// still encrypted event is not actionable => no reply button
|
||||
expect(queryByLabelText("Reply")).toBeFalsy();
|
||||
|
||||
act(() => {
|
||||
// ''decrypt'' the event
|
||||
decryptingEvent.event.type = alicesMessageEvent.getType();
|
||||
decryptingEvent.event.content = alicesMessageEvent.getContent();
|
||||
decryptingEvent.emit(MatrixEventEvent.Decrypted, decryptingEvent);
|
||||
});
|
||||
|
||||
// new available actions after decryption
|
||||
expect(queryByLabelText("Reply")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("status", () => {
|
||||
it("updates component when event status changes", () => {
|
||||
alicesMessageEvent.setStatus(EventStatus.QUEUED);
|
||||
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
|
||||
|
||||
// pending event status, cancel action available
|
||||
expect(queryByLabelText("Delete")).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
alicesMessageEvent.setStatus(EventStatus.SENT);
|
||||
});
|
||||
|
||||
// event is sent, no longer cancelable
|
||||
expect(queryByLabelText("Delete")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("redaction", () => {
|
||||
// this doesn't do what it's supposed to
|
||||
// because beforeRedaction event is fired... before redaction
|
||||
// event is unchanged at point when this component updates
|
||||
// TODO file bug
|
||||
it.skip("updates component on before redaction event", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
room_id: roomId,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Hello",
|
||||
},
|
||||
});
|
||||
const { queryByLabelText } = getComponent({ mxEvent: event });
|
||||
|
||||
// no pending redaction => no delete button
|
||||
expect(queryByLabelText("Delete")).toBeFalsy();
|
||||
|
||||
act(() => {
|
||||
const redactionEvent = new MatrixEvent({
|
||||
type: EventType.RoomRedaction,
|
||||
sender: userId,
|
||||
room_id: roomId,
|
||||
});
|
||||
redactionEvent.setStatus(EventStatus.QUEUED);
|
||||
event.markLocallyRedacted(redactionEvent);
|
||||
});
|
||||
|
||||
// updated with local redaction event, delete now available
|
||||
expect(queryByLabelText("Delete")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("options button", () => {
|
||||
it("renders options menu", () => {
|
||||
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
|
||||
expect(queryByLabelText("Options")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("opens message context menu on click", () => {
|
||||
const { getByTestId, queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
|
||||
fireEvent.click(queryByLabelText("Options")!);
|
||||
expect(getByTestId("mx_MessageContextMenu")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("reply button", () => {
|
||||
it("renders reply button on own actionable event", () => {
|
||||
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
|
||||
expect(queryByLabelText("Reply")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders reply button on others actionable event", () => {
|
||||
const { queryByLabelText } = getComponent({ mxEvent: bobsMessageEvent }, { canSendMessages: true });
|
||||
expect(queryByLabelText("Reply")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not render reply button on non-actionable event", () => {
|
||||
// redacted event is not actionable
|
||||
const { queryByLabelText } = getComponent({ mxEvent: redactedEvent });
|
||||
expect(queryByLabelText("Reply")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not render reply button when user cannot send messaged", () => {
|
||||
// redacted event is not actionable
|
||||
const { queryByLabelText } = getComponent({ mxEvent: redactedEvent }, { canSendMessages: false });
|
||||
expect(queryByLabelText("Reply")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("dispatches reply event on click", () => {
|
||||
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
|
||||
|
||||
fireEvent.click(queryByLabelText("Reply")!);
|
||||
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: "reply_to_event",
|
||||
event: alicesMessageEvent,
|
||||
context: TimelineRenderingType.Room,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("react button", () => {
|
||||
it("renders react button on own actionable event", () => {
|
||||
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
|
||||
expect(queryByLabelText("React")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders react button on others actionable event", () => {
|
||||
const { queryByLabelText } = getComponent({ mxEvent: bobsMessageEvent });
|
||||
expect(queryByLabelText("React")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not render react button on non-actionable event", () => {
|
||||
// redacted event is not actionable
|
||||
const { queryByLabelText } = getComponent({ mxEvent: redactedEvent });
|
||||
expect(queryByLabelText("React")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not render react button when user cannot react", () => {
|
||||
// redacted event is not actionable
|
||||
const { queryByLabelText } = getComponent({ mxEvent: redactedEvent }, { canReact: false });
|
||||
expect(queryByLabelText("React")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("opens reaction picker on click", () => {
|
||||
const { queryByLabelText, getByTestId } = getComponent({ mxEvent: alicesMessageEvent });
|
||||
fireEvent.click(queryByLabelText("React")!);
|
||||
expect(getByTestId("mx_EmojiPicker")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancel button", () => {
|
||||
it("renders cancel button for an event with a cancelable status", () => {
|
||||
alicesMessageEvent.setStatus(EventStatus.QUEUED);
|
||||
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
|
||||
expect(queryByLabelText("Delete")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders cancel button for an event with a pending edit", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
room_id: roomId,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Hello",
|
||||
},
|
||||
});
|
||||
event.setStatus(EventStatus.SENT);
|
||||
const replacingEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
room_id: roomId,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "replacing event body",
|
||||
},
|
||||
});
|
||||
replacingEvent.setStatus(EventStatus.QUEUED);
|
||||
event.makeReplaced(replacingEvent);
|
||||
const { queryByLabelText } = getComponent({ mxEvent: event });
|
||||
expect(queryByLabelText("Delete")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders cancel button for an event with a pending redaction", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
room_id: roomId,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Hello",
|
||||
},
|
||||
});
|
||||
event.setStatus(EventStatus.SENT);
|
||||
|
||||
const redactionEvent = new MatrixEvent({
|
||||
type: EventType.RoomRedaction,
|
||||
sender: userId,
|
||||
room_id: roomId,
|
||||
});
|
||||
redactionEvent.setStatus(EventStatus.QUEUED);
|
||||
|
||||
event.markLocallyRedacted(redactionEvent);
|
||||
const { queryByLabelText } = getComponent({ mxEvent: event });
|
||||
expect(queryByLabelText("Delete")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders cancel and retry button for an event with NOT_SENT status", () => {
|
||||
alicesMessageEvent.setStatus(EventStatus.NOT_SENT);
|
||||
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
|
||||
expect(queryByLabelText("Retry")).toBeTruthy();
|
||||
expect(queryByLabelText("Delete")).toBeTruthy();
|
||||
});
|
||||
|
||||
it.todo("unsends event on cancel click");
|
||||
it.todo("retrys event on retry click");
|
||||
});
|
||||
|
||||
describe("thread button", () => {
|
||||
beforeEach(() => {
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
});
|
||||
|
||||
describe("when threads feature is enabled", () => {
|
||||
it("renders thread button on own actionable event", () => {
|
||||
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
|
||||
expect(queryByLabelText("Reply in thread")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not render thread button for a beacon_info event", () => {
|
||||
const beaconInfoEvent = makeBeaconInfoEvent(userId, roomId);
|
||||
const { queryByLabelText } = getComponent({ mxEvent: beaconInfoEvent });
|
||||
expect(queryByLabelText("Reply in thread")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("opens thread on click", () => {
|
||||
const { getByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
|
||||
|
||||
fireEvent.click(getByLabelText("Reply in thread"));
|
||||
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ShowThread,
|
||||
rootEvent: alicesMessageEvent,
|
||||
push: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("opens parent thread for a thread reply message", () => {
|
||||
const threadReplyEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
room_id: roomId,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "this is a thread reply",
|
||||
},
|
||||
});
|
||||
// mock the thread stuff
|
||||
jest.spyOn(threadReplyEvent, "isThreadRoot", "get").mockReturnValue(false);
|
||||
// set alicesMessageEvent as the root event
|
||||
jest.spyOn(threadReplyEvent, "getThread").mockReturnValue({
|
||||
rootEvent: alicesMessageEvent,
|
||||
} as unknown as Thread);
|
||||
const { getByLabelText } = getComponent({ mxEvent: threadReplyEvent });
|
||||
|
||||
fireEvent.click(getByLabelText("Reply in thread"));
|
||||
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ShowThread,
|
||||
rootEvent: alicesMessageEvent,
|
||||
initialEvent: threadReplyEvent,
|
||||
highlighted: true,
|
||||
scroll_into_view: true,
|
||||
push: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.each([["React"], ["Reply"], ["Reply in thread"], ["Edit"], ["Pin"]])(
|
||||
"does not show context menu when right-clicking",
|
||||
(buttonLabel: string) => {
|
||||
// For favourite and pin buttons
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
|
||||
const event = new MouseEvent("contextmenu", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
event.stopPropagation = jest.fn();
|
||||
event.preventDefault = jest.fn();
|
||||
|
||||
const { queryByTestId, queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
|
||||
fireEvent(queryByLabelText(buttonLabel)!, event);
|
||||
expect(event.stopPropagation).toHaveBeenCalled();
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
expect(queryByTestId("mx_MessageContextMenu")).toBeFalsy();
|
||||
},
|
||||
);
|
||||
|
||||
it("does shows context menu when right-clicking options", () => {
|
||||
const { queryByTestId, queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
|
||||
fireEvent.contextMenu(queryByLabelText("Options")!);
|
||||
expect(queryByTestId("mx_MessageContextMenu")).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("pin button", () => {
|
||||
beforeEach(() => {
|
||||
// enable pin button
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
jest.spyOn(PinningUtils, "isPinned").mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.spyOn(
|
||||
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"mayClientSendStateEvent",
|
||||
).mockRestore();
|
||||
});
|
||||
|
||||
it("should not render pin button when user can't send state event", () => {
|
||||
jest.spyOn(
|
||||
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"mayClientSendStateEvent",
|
||||
).mockReturnValue(false);
|
||||
|
||||
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
|
||||
expect(queryByLabelText("Pin")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should render pin button", () => {
|
||||
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
|
||||
expect(queryByLabelText("Pin")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should listen to room pinned events", async () => {
|
||||
getComponent({ mxEvent: alicesMessageEvent });
|
||||
expect(screen.getByLabelText("Pin")).toBeInTheDocument();
|
||||
|
||||
// Event is considered pinned
|
||||
jest.spyOn(PinningUtils, "isPinned").mockReturnValue(true);
|
||||
// Emit that the room pinned events have changed
|
||||
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
|
||||
roomState.emit(
|
||||
RoomStateEvent.Events,
|
||||
{
|
||||
getType: () => EventType.RoomPinnedEvents,
|
||||
} as MatrixEvent,
|
||||
roomState,
|
||||
null,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(screen.getByLabelText("Unpin")).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
});
|
132
test/unit-tests/components/views/messages/MessageEvent-test.tsx
Normal file
132
test/unit-tests/components/views/messages/MessageEvent-test.tsx
Normal file
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, RenderResult } from "jest-matrix-react";
|
||||
import { MatrixClient, MatrixEvent, EventType, Room, MsgType } from "matrix-js-sdk/src/matrix";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
|
||||
import { mkEvent, mkRoom, stubClient } from "../../../../test-utils";
|
||||
import MessageEvent from "../../../../../src/components/views/messages/MessageEvent";
|
||||
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
||||
|
||||
jest.mock("../../../../../src/components/views/messages/UnknownBody", () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="unknown-body" />,
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/voice-broadcast/components/VoiceBroadcastBody", () => ({
|
||||
VoiceBroadcastBody: () => <div data-testid="voice-broadcast-body" />,
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/components/views/messages/MImageBody", () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="image-body" />,
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/components/views/messages/MImageReplyBody", () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="image-reply-body" />,
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/components/views/messages/MStickerBody", () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="sticker-body" />,
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/components/views/messages/TextualBody.tsx", () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="textual-body" />,
|
||||
}));
|
||||
|
||||
describe("MessageEvent", () => {
|
||||
let room: Room;
|
||||
let client: MatrixClient;
|
||||
let event: MatrixEvent;
|
||||
|
||||
const renderMessageEvent = (): RenderResult => {
|
||||
return render(
|
||||
<MessageEvent
|
||||
mxEvent={event}
|
||||
onHeightChanged={jest.fn()}
|
||||
permalinkCreator={new RoomPermalinkCreator(room)}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
room = mkRoom(client, "!room:example.com");
|
||||
jest.spyOn(SettingsStore, "getValue");
|
||||
jest.spyOn(SettingsStore, "watchSetting");
|
||||
jest.spyOn(SettingsStore, "unwatchSetting").mockImplementation(jest.fn());
|
||||
});
|
||||
|
||||
describe("when a voice broadcast start event occurs", () => {
|
||||
let result: RenderResult;
|
||||
|
||||
beforeEach(() => {
|
||||
event = mkEvent({
|
||||
event: true,
|
||||
type: VoiceBroadcastInfoEventType,
|
||||
user: client.getUserId()!,
|
||||
room: room.roomId,
|
||||
content: {
|
||||
state: VoiceBroadcastInfoState.Started,
|
||||
},
|
||||
});
|
||||
result = renderMessageEvent();
|
||||
});
|
||||
|
||||
it("should render a VoiceBroadcast component", () => {
|
||||
result.getByTestId("voice-broadcast-body");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when an image with a caption is sent", () => {
|
||||
let result: RenderResult;
|
||||
|
||||
beforeEach(() => {
|
||||
event = mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomMessage,
|
||||
user: client.getUserId()!,
|
||||
room: room.roomId,
|
||||
content: {
|
||||
body: "caption for a test image",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "<strong>caption for a test image</strong>",
|
||||
msgtype: MsgType.Image,
|
||||
filename: "image.webp",
|
||||
info: {
|
||||
w: 40,
|
||||
h: 50,
|
||||
},
|
||||
url: "mxc://server/image",
|
||||
},
|
||||
});
|
||||
result = renderMessageEvent();
|
||||
});
|
||||
|
||||
it("should render a TextualBody and an ImageBody", () => {
|
||||
fetchMock.getOnce(
|
||||
"https://server/_matrix/media/v3/download/server/image",
|
||||
{
|
||||
body: fs.readFileSync(path.resolve(__dirname, "..", "..", "..", "images", "animated-logo.webp")),
|
||||
},
|
||||
{ sendAsJson: false },
|
||||
);
|
||||
result.getByTestId("image-body");
|
||||
result.getByTestId("textual-body");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import MessageTimestamp from "../../../../../src/components/views/messages/MessageTimestamp";
|
||||
|
||||
jest.mock("../../../../../src/settings/SettingsStore");
|
||||
|
||||
describe("MessageTimestamp", () => {
|
||||
// Friday Dec 17 2021, 9:09am
|
||||
const nowDate = new Date("2021-12-17T08:09:00.000Z");
|
||||
|
||||
const HOUR_MS = 3600000;
|
||||
const DAY_MS = HOUR_MS * 24;
|
||||
|
||||
it("should render HH:MM", () => {
|
||||
const { asFragment } = render(<MessageTimestamp ts={nowDate.getTime()} />);
|
||||
expect(asFragment()).toMatchInlineSnapshot(`
|
||||
<DocumentFragment>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
aria-live="off"
|
||||
class="mx_MessageTimestamp"
|
||||
>
|
||||
08:09
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`);
|
||||
});
|
||||
|
||||
it("should show full date & time on hover", async () => {
|
||||
const { container } = render(<MessageTimestamp ts={nowDate.getTime()} />);
|
||||
await userEvent.hover(container.querySelector(".mx_MessageTimestamp")!);
|
||||
expect((await screen.findByRole("tooltip")).textContent).toMatchInlineSnapshot(`"Fri, Dec 17, 2021, 08:09:00"`);
|
||||
});
|
||||
|
||||
it("should show sent & received time on hover if passed", async () => {
|
||||
const { container } = render(
|
||||
<MessageTimestamp ts={nowDate.getTime()} receivedTs={nowDate.getTime() + DAY_MS} />,
|
||||
);
|
||||
await userEvent.hover(container.querySelector(".mx_MessageTimestamp")!);
|
||||
expect((await screen.findByRole("tooltip")).textContent).toMatchInlineSnapshot(
|
||||
`"Sent at: Fri, Dec 17, 2021, 08:09:00Received at: Sat, Dec 18, 2021, 08:09:00"`,
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import { PinnedMessageBadge } from "../../../../../src/components/views/messages/PinnedMessageBadge.tsx";
|
||||
|
||||
describe("PinnedMessageBadge", () => {
|
||||
it("should render", () => {
|
||||
const { asFragment } = render(<PinnedMessageBadge />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 Beeper
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { IContent, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { getMockClientWithEventEmitter } from "../../../../test-utils";
|
||||
import ReactionsRowButton, { IProps } from "../../../../../src/components/views/messages/ReactionsRowButton";
|
||||
|
||||
describe("ReactionsRowButton", () => {
|
||||
const userId = "@alice:server";
|
||||
const roomId = "!randomcharacters:aser.ver";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
mxcUrlToHttp: jest.fn().mockReturnValue("https://not.a.real.url"),
|
||||
getRoom: jest.fn(),
|
||||
});
|
||||
const room = new Room(roomId, mockClient, userId);
|
||||
|
||||
const createProps = (relationContent: IContent): IProps => ({
|
||||
mxEvent: new MatrixEvent({
|
||||
room_id: roomId,
|
||||
event_id: "$test:example.com",
|
||||
content: { body: "test" },
|
||||
}),
|
||||
content: relationContent["m.relates_to"]?.key || "",
|
||||
count: 2,
|
||||
reactionEvents: [
|
||||
new MatrixEvent({
|
||||
type: "m.reaction",
|
||||
sender: "@user1:example.com",
|
||||
content: relationContent,
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.reaction",
|
||||
sender: "@user2:example.com",
|
||||
content: relationContent,
|
||||
}),
|
||||
],
|
||||
customReactionImagesEnabled: true,
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks();
|
||||
mockClient.credentials = { userId: userId };
|
||||
mockClient.getRoom.mockImplementation((roomId: string): Room | null => {
|
||||
return roomId === room.roomId ? room : null;
|
||||
});
|
||||
});
|
||||
|
||||
it("renders reaction row button emojis correctly", () => {
|
||||
const props = createProps({
|
||||
"m.relates_to": {
|
||||
event_id: "$user2:example.com",
|
||||
key: "👍",
|
||||
rel_type: "m.annotation",
|
||||
},
|
||||
});
|
||||
const root = render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<ReactionsRowButton {...props} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
expect(root.asFragment()).toMatchSnapshot();
|
||||
|
||||
// Try hover and make sure that the ReactionsRowButtonTooltip works
|
||||
const reactionButton = root.getByRole("button");
|
||||
const event = new MouseEvent("mouseover", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
reactionButton.dispatchEvent(event);
|
||||
|
||||
expect(root.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders reaction row button custom image reactions correctly", () => {
|
||||
const props = createProps({
|
||||
"com.beeper.reaction.shortcode": ":test:",
|
||||
"shortcode": ":test:",
|
||||
"m.relates_to": {
|
||||
event_id: "$user1:example.com",
|
||||
key: "mxc://example.com/123456789",
|
||||
rel_type: "m.annotation",
|
||||
},
|
||||
});
|
||||
|
||||
const root = render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<ReactionsRowButton {...props} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
expect(root.asFragment()).toMatchSnapshot();
|
||||
|
||||
// Try hover and make sure that the ReactionsRowButtonTooltip works
|
||||
const reactionButton = root.getByRole("button");
|
||||
const event = new MouseEvent("mouseover", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
reactionButton.dispatchEvent(event);
|
||||
|
||||
expect(root.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders without a room", () => {
|
||||
mockClient.getRoom.mockImplementation(() => null);
|
||||
|
||||
const props = createProps({});
|
||||
|
||||
const root = render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<ReactionsRowButton {...props} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
expect(root.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,284 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { act, render, screen, waitFor } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
import { EventType, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import dis from "../../../../../src/dispatcher/dispatcher";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import {
|
||||
guessServerNameFromRoomId,
|
||||
RoomPredecessorTile,
|
||||
} from "../../../../../src/components/views/messages/RoomPredecessorTile";
|
||||
import { stubClient, upsertRoomStateEvents } from "../../../../test-utils/test-utils";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import RoomContext from "../../../../../src/contexts/RoomContext";
|
||||
import { filterConsole, getRoomContext } from "../../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
|
||||
jest.mock("../../../../../src/dispatcher/dispatcher");
|
||||
|
||||
describe("<RoomPredecessorTile />", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const roomId = "!room:server.org";
|
||||
stubClient();
|
||||
const client = mocked(MatrixClientPeg.safeGet());
|
||||
|
||||
function makeRoom({
|
||||
createEventHasPredecessor = false,
|
||||
predecessorEventExists = false,
|
||||
predecessorEventHasEventId = false,
|
||||
predecessorEventHasViaServers = false,
|
||||
}): Room {
|
||||
const room = new Room(roomId, client, userId);
|
||||
|
||||
const createInfo = {
|
||||
type: EventType.RoomCreate,
|
||||
state_key: "",
|
||||
sender: userId,
|
||||
room_id: roomId,
|
||||
content: {},
|
||||
event_id: "$create",
|
||||
};
|
||||
|
||||
if (createEventHasPredecessor) {
|
||||
createInfo.content = {
|
||||
predecessor: { room_id: "old_room_id", event_id: "$tombstone_event_id" },
|
||||
};
|
||||
}
|
||||
|
||||
const createEvent = new MatrixEvent(createInfo);
|
||||
upsertRoomStateEvents(room, [createEvent]);
|
||||
|
||||
if (predecessorEventExists) {
|
||||
const predecessorInfo = {
|
||||
type: EventType.RoomPredecessor,
|
||||
state_key: "",
|
||||
sender: userId,
|
||||
room_id: roomId,
|
||||
content: {
|
||||
predecessor_room_id: "old_room_id_from_predecessor",
|
||||
last_known_event_id: predecessorEventHasEventId
|
||||
? "$tombstone_event_id_from_predecessor"
|
||||
: undefined,
|
||||
via_servers: predecessorEventHasViaServers ? ["a.example.com", "b.example.com"] : undefined,
|
||||
},
|
||||
event_id: "$predecessor",
|
||||
};
|
||||
|
||||
const predecessorEvent = new MatrixEvent(predecessorInfo);
|
||||
upsertRoomStateEvents(room, [predecessorEvent]);
|
||||
}
|
||||
return room;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mocked(dis.dispatch).mockReset();
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
|
||||
stubClient();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockRestore();
|
||||
jest.spyOn(SettingsStore, "setValue").mockRestore();
|
||||
});
|
||||
|
||||
function renderTile(room: Room) {
|
||||
// Find this room's create event (it should have one!)
|
||||
const createEvent = room.currentState.getStateEvents("m.room.create")[0];
|
||||
expect(createEvent).toBeTruthy();
|
||||
|
||||
return render(
|
||||
<RoomContext.Provider value={getRoomContext(room, {})}>
|
||||
<RoomPredecessorTile mxEvent={createEvent} />
|
||||
</RoomContext.Provider>,
|
||||
);
|
||||
}
|
||||
|
||||
it("Renders as expected", () => {
|
||||
const roomCreate = renderTile(makeRoom({ createEventHasPredecessor: true }));
|
||||
expect(roomCreate.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("Links to the old version of the room", () => {
|
||||
renderTile(makeRoom({ createEventHasPredecessor: true }));
|
||||
expect(screen.getByText("Click here to see older messages.")).toHaveAttribute(
|
||||
"href",
|
||||
"https://matrix.to/#/old_room_id/$tombstone_event_id",
|
||||
);
|
||||
});
|
||||
|
||||
describe("(filtering warnings about no predecessor)", () => {
|
||||
filterConsole("RoomPredecessorTile unexpectedly used in a room with no predecessor.");
|
||||
|
||||
it("Shows an empty div if there is no predecessor", () => {
|
||||
renderTile(makeRoom({}));
|
||||
expect(screen.queryByText("Click here to see older messages.", { exact: false })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("Opens the old room on click", async () => {
|
||||
renderTile(makeRoom({ createEventHasPredecessor: true }));
|
||||
const link = screen.getByText("Click here to see older messages.");
|
||||
|
||||
await act(() => userEvent.click(link));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
event_id: "$tombstone_event_id",
|
||||
highlighted: true,
|
||||
room_id: "old_room_id",
|
||||
metricsTrigger: "Predecessor",
|
||||
metricsViaKeyboard: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("Ignores m.predecessor if labs flag is off", () => {
|
||||
renderTile(makeRoom({ createEventHasPredecessor: true, predecessorEventExists: true }));
|
||||
expect(screen.getByText("Click here to see older messages.")).toHaveAttribute(
|
||||
"href",
|
||||
"https://matrix.to/#/old_room_id/$tombstone_event_id",
|
||||
);
|
||||
});
|
||||
|
||||
describe("If the predecessor room is not found", () => {
|
||||
filterConsole("Failed to find predecessor room with id old_room_id");
|
||||
|
||||
beforeEach(() => {
|
||||
mocked(MatrixClientPeg.safeGet().getRoom).mockReturnValue(null);
|
||||
});
|
||||
|
||||
it("Shows an error if there are no via servers", () => {
|
||||
renderTile(makeRoom({ createEventHasPredecessor: true, predecessorEventExists: true }));
|
||||
expect(screen.getByText("Can't find the old version of this room", { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("When feature_dynamic_room_predecessors = true", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(settingName) => settingName === "feature_dynamic_room_predecessors",
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReset();
|
||||
});
|
||||
|
||||
it("Uses the create event if there is no m.predecessor", () => {
|
||||
renderTile(makeRoom({ createEventHasPredecessor: true }));
|
||||
expect(screen.getByText("Click here to see older messages.")).toHaveAttribute(
|
||||
"href",
|
||||
"https://matrix.to/#/old_room_id/$tombstone_event_id",
|
||||
);
|
||||
});
|
||||
|
||||
it("Uses m.predecessor when it's there", () => {
|
||||
renderTile(makeRoom({ createEventHasPredecessor: true, predecessorEventExists: true }));
|
||||
expect(screen.getByText("Click here to see older messages.")).toHaveAttribute(
|
||||
"href",
|
||||
"https://matrix.to/#/old_room_id_from_predecessor",
|
||||
);
|
||||
});
|
||||
|
||||
it("Links to the event in the room if event ID is provided", () => {
|
||||
renderTile(
|
||||
makeRoom({
|
||||
createEventHasPredecessor: true,
|
||||
predecessorEventExists: true,
|
||||
predecessorEventHasEventId: true,
|
||||
}),
|
||||
);
|
||||
expect(screen.getByText("Click here to see older messages.")).toHaveAttribute(
|
||||
"href",
|
||||
"https://matrix.to/#/old_room_id_from_predecessor/$tombstone_event_id_from_predecessor",
|
||||
);
|
||||
});
|
||||
|
||||
describe("If the predecessor room is not found", () => {
|
||||
filterConsole("Failed to find predecessor room with id old_room_id");
|
||||
|
||||
beforeEach(() => {
|
||||
mocked(MatrixClientPeg.safeGet().getRoom).mockReturnValue(null);
|
||||
});
|
||||
|
||||
it("Shows an error if there are no via servers", () => {
|
||||
renderTile(makeRoom({ createEventHasPredecessor: true, predecessorEventExists: true }));
|
||||
expect(
|
||||
screen.getByText("Can't find the old version of this room", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("Shows a tile if there are via servers", () => {
|
||||
renderTile(
|
||||
makeRoom({
|
||||
createEventHasPredecessor: true,
|
||||
predecessorEventExists: true,
|
||||
predecessorEventHasViaServers: true,
|
||||
}),
|
||||
);
|
||||
expect(screen.getByText("Click here to see older messages.")).toHaveAttribute(
|
||||
"href",
|
||||
"https://matrix.to/#/old_room_id_from_predecessor?via=a.example.com&via=b.example.com",
|
||||
);
|
||||
});
|
||||
|
||||
it("Shows a tile linking to an event if there are via servers", () => {
|
||||
renderTile(
|
||||
makeRoom({
|
||||
createEventHasPredecessor: true,
|
||||
predecessorEventExists: true,
|
||||
predecessorEventHasEventId: true,
|
||||
predecessorEventHasViaServers: true,
|
||||
}),
|
||||
);
|
||||
expect(screen.getByText("Click here to see older messages.")).toHaveAttribute(
|
||||
"href",
|
||||
"https://matrix.to/#/old_room_id_from_predecessor/$tombstone_event_id_from_predecessor?via=a.example.com&via=b.example.com",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("guessServerNameFromRoomId", () => {
|
||||
it("Extracts the domain name from a standard room ID", () => {
|
||||
expect(guessServerNameFromRoomId("!436456:example.com")).toEqual("example.com");
|
||||
});
|
||||
|
||||
it("Extracts the domain name and port when included", () => {
|
||||
expect(guessServerNameFromRoomId("!436456:example.com:8888")).toEqual("example.com:8888");
|
||||
});
|
||||
|
||||
it("Handles an IPv4 address for server name", () => {
|
||||
expect(guessServerNameFromRoomId("!436456:127.0.0.1")).toEqual("127.0.0.1");
|
||||
});
|
||||
|
||||
it("Handles an IPv4 address and port", () => {
|
||||
expect(guessServerNameFromRoomId("!436456:127.0.0.1:81")).toEqual("127.0.0.1:81");
|
||||
});
|
||||
|
||||
it("Handles an IPv6 address for server name", () => {
|
||||
expect(guessServerNameFromRoomId("!436456:::1")).toEqual("::1");
|
||||
});
|
||||
|
||||
it("Handles an IPv6 address and port", () => {
|
||||
expect(guessServerNameFromRoomId("!436456:::1:8080")).toEqual("::1:8080");
|
||||
});
|
||||
|
||||
it("Returns null when the room ID contains no colon", () => {
|
||||
expect(guessServerNameFromRoomId("!436456")).toBeNull();
|
||||
});
|
||||
});
|
418
test/unit-tests/components/views/messages/TextualBody-test.tsx
Normal file
418
test/unit-tests/components/views/messages/TextualBody-test.tsx
Normal file
|
@ -0,0 +1,418 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2019-2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import { getMockClientWithEventEmitter, mkEvent, mkMessage, mkStubRoom } from "../../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import * as languageHandler from "../../../../../src/languageHandler";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import TextualBody from "../../../../../src/components/views/messages/TextualBody";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
||||
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
|
||||
|
||||
const room1Id = "!room1:example.com";
|
||||
const room2Id = "!room2:example.com";
|
||||
const room2Name = "Room 2";
|
||||
|
||||
interface MkRoomTextMessageOpts {
|
||||
roomId?: string;
|
||||
}
|
||||
|
||||
const mkRoomTextMessage = (body: string, mkRoomTextMessageOpts?: MkRoomTextMessageOpts): MatrixEvent => {
|
||||
return mkMessage({
|
||||
msg: body,
|
||||
room: mkRoomTextMessageOpts?.roomId ?? room1Id,
|
||||
user: "sender",
|
||||
event: true,
|
||||
});
|
||||
};
|
||||
|
||||
const mkFormattedMessage = (body: string, formattedBody: string): MatrixEvent => {
|
||||
return mkMessage({
|
||||
msg: body,
|
||||
formattedMsg: formattedBody,
|
||||
format: "org.matrix.custom.html",
|
||||
room: room1Id,
|
||||
user: "sender",
|
||||
event: true,
|
||||
});
|
||||
};
|
||||
|
||||
describe("<TextualBody />", () => {
|
||||
afterEach(() => {
|
||||
jest.spyOn(MatrixClientPeg, "get").mockRestore();
|
||||
jest.spyOn(global.Math, "random").mockRestore();
|
||||
});
|
||||
|
||||
const defaultRoom = mkStubRoom(room1Id, "test room", undefined);
|
||||
const otherRoom = mkStubRoom(room2Id, room2Name, undefined);
|
||||
let defaultMatrixClient: MockedObject<MatrixClient>;
|
||||
|
||||
const defaultEvent = mkEvent({
|
||||
type: "m.room.message",
|
||||
room: room1Id,
|
||||
user: "sender",
|
||||
content: {
|
||||
body: "winks",
|
||||
msgtype: "m.emote",
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
defaultMatrixClient = getMockClientWithEventEmitter({
|
||||
getRoom: (roomId: string | undefined) => {
|
||||
if (roomId === room1Id) return defaultRoom;
|
||||
if (roomId === room2Id) return otherRoom;
|
||||
return null;
|
||||
},
|
||||
getRooms: () => [defaultRoom, otherRoom],
|
||||
getAccountData: (): MatrixEvent | undefined => undefined,
|
||||
isGuest: () => false,
|
||||
mxcUrlToHttp: (s: string) => s,
|
||||
getUserId: () => "@user:example.com",
|
||||
fetchRoomEvent: () => {
|
||||
throw new Error("MockClient event not found");
|
||||
},
|
||||
});
|
||||
|
||||
mocked(defaultRoom).findEventById.mockImplementation((eventId: string) => {
|
||||
if (eventId === defaultEvent.getId()) return defaultEvent;
|
||||
return undefined;
|
||||
});
|
||||
jest.spyOn(global.Math, "random").mockReturnValue(0.123456);
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
mxEvent: defaultEvent,
|
||||
highlights: [] as string[],
|
||||
highlightLink: "",
|
||||
onMessageAllowed: jest.fn(),
|
||||
onHeightChanged: jest.fn(),
|
||||
permalinkCreator: new RoomPermalinkCreator(defaultRoom),
|
||||
mediaEventHelper: {} as MediaEventHelper,
|
||||
};
|
||||
|
||||
const getComponent = (props = {}, matrixClient: MatrixClient = defaultMatrixClient, renderingFn?: any) =>
|
||||
(renderingFn ?? render)(
|
||||
<MatrixClientContext.Provider value={matrixClient}>
|
||||
<TextualBody {...defaultProps} {...props} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
it("renders m.emote correctly", () => {
|
||||
DMRoomMap.makeShared(defaultMatrixClient);
|
||||
|
||||
const ev = mkEvent({
|
||||
type: "m.room.message",
|
||||
room: room1Id,
|
||||
user: "sender",
|
||||
content: {
|
||||
body: "winks",
|
||||
msgtype: "m.emote",
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
|
||||
const { container } = getComponent({ mxEvent: ev });
|
||||
expect(container).toHaveTextContent("* sender winks");
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders m.notice correctly", () => {
|
||||
DMRoomMap.makeShared(defaultMatrixClient);
|
||||
|
||||
const ev = mkEvent({
|
||||
type: "m.room.message",
|
||||
room: room1Id,
|
||||
user: "bot_sender",
|
||||
content: {
|
||||
body: "this is a notice, probably from a bot",
|
||||
msgtype: "m.notice",
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
|
||||
const { container } = getComponent({ mxEvent: ev });
|
||||
expect(container).toHaveTextContent(ev.getContent().body);
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("renders plain-text m.text correctly", () => {
|
||||
beforeEach(() => {
|
||||
DMRoomMap.makeShared(defaultMatrixClient);
|
||||
});
|
||||
|
||||
it("simple message renders as expected", () => {
|
||||
const ev = mkRoomTextMessage("this is a plaintext message");
|
||||
const { container } = getComponent({ mxEvent: ev });
|
||||
expect(container).toHaveTextContent(ev.getContent().body);
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// If pills were rendered within a Portal/same shadow DOM then it'd be easier to test
|
||||
it("linkification get applied correctly into the DOM", () => {
|
||||
const ev = mkRoomTextMessage("Visit https://matrix.org/");
|
||||
const { container } = getComponent({ mxEvent: ev });
|
||||
expect(container).toHaveTextContent(ev.getContent().body);
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should not pillify MXIDs", () => {
|
||||
const ev = mkRoomTextMessage("Chat with @user:example.com");
|
||||
const { container } = getComponent({ mxEvent: ev });
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content.innerHTML).toMatchInlineSnapshot(
|
||||
`"Chat with <a href="https://matrix.to/#/@user:example.com" class="linkified" rel="noreferrer noopener">@user:example.com</a>"`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should pillify an MXID permalink", () => {
|
||||
const ev = mkRoomTextMessage("Chat with https://matrix.to/#/@user:example.com");
|
||||
const { container } = getComponent({ mxEvent: ev });
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content.innerHTML).toMatchInlineSnapshot(
|
||||
`"Chat with <span><bdi><a class="mx_Pill mx_UserPill mx_UserPill_me" href="https://matrix.to/#/@user:example.com"><span aria-label="Profile picture" aria-hidden="true" data-testid="avatar-img" data-type="round" data-color="2" class="_avatar_mcap2_17 mx_BaseAvatar" style="--cpd-avatar-size: 16px;"><img loading="lazy" alt="" src="mxc://avatar.url/image.png" referrerpolicy="no-referrer" class="_image_mcap2_50" data-type="round" width="16px" height="16px"></span><span class="mx_Pill_text">Member</span></a></bdi></span>"`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should not pillify room aliases", () => {
|
||||
const ev = mkRoomTextMessage("Visit #room:example.com");
|
||||
const { container } = getComponent({ mxEvent: ev });
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content.innerHTML).toMatchInlineSnapshot(
|
||||
`"Visit <a href="https://matrix.to/#/#room:example.com" class="linkified" rel="noreferrer noopener">#room:example.com</a>"`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should pillify a room alias permalink", () => {
|
||||
const ev = mkRoomTextMessage("Visit https://matrix.to/#/#room:example.com");
|
||||
const { container } = getComponent({ mxEvent: ev });
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content.innerHTML).toMatchInlineSnapshot(
|
||||
`"Visit <span><bdi><a class="mx_Pill mx_RoomPill" href="https://matrix.to/#/#room:example.com"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24" class="mx_Pill_LinkIcon mx_BaseAvatar"><path d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"></path></svg><span class="mx_Pill_text">#room:example.com</span></a></bdi></span>"`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should pillify a permalink to a message in the same room with the label »Message from Member«", () => {
|
||||
const ev = mkRoomTextMessage(`Visit https://matrix.to/#/${room1Id}/${defaultEvent.getId()}`);
|
||||
const { container } = getComponent({ mxEvent: ev });
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content.innerHTML.replace(defaultEvent.getId(), "%event_id%")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should pillify a permalink to an unknown message in the same room with the label »Message«", () => {
|
||||
const ev = mkRoomTextMessage(`Visit https://matrix.to/#/${room1Id}/!abc123`);
|
||||
const { container } = getComponent({ mxEvent: ev });
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should pillify a permalink to an event in another room with the label »Message in Room 2«", () => {
|
||||
const ev = mkRoomTextMessage(`Visit https://matrix.to/#/${room2Id}/${defaultEvent.getId()}`);
|
||||
const { container } = getComponent({ mxEvent: ev });
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content.innerHTML.replace(defaultEvent.getId(), "%event_id%")).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("renders formatted m.text correctly", () => {
|
||||
let matrixClient: MatrixClient;
|
||||
beforeEach(() => {
|
||||
matrixClient = getMockClientWithEventEmitter({
|
||||
getRoom: () => mkStubRoom(room1Id, "room name", undefined),
|
||||
getAccountData: (): MatrixEvent | undefined => undefined,
|
||||
getUserId: () => "@me:my_server",
|
||||
getHomeserverUrl: () => "https://my_server/",
|
||||
on: (): void => undefined,
|
||||
removeListener: (): void => undefined,
|
||||
isGuest: () => false,
|
||||
mxcUrlToHttp: (s: string) => s,
|
||||
});
|
||||
DMRoomMap.makeShared(defaultMatrixClient);
|
||||
});
|
||||
|
||||
it("italics, bold, underline and strikethrough render as expected", () => {
|
||||
const ev = mkFormattedMessage(
|
||||
"foo *baz* __bar__ <del>del</del> <u>u</u>",
|
||||
"foo <em>baz</em> <strong>bar</strong> <del>del</del> <u>u</u>",
|
||||
);
|
||||
const { container } = getComponent({ mxEvent: ev }, matrixClient);
|
||||
expect(container).toHaveTextContent("foo baz bar del u");
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("spoilers get injected properly into the DOM", () => {
|
||||
const ev = mkFormattedMessage(
|
||||
"Hey [Spoiler for movie](mxc://someserver/somefile)",
|
||||
'Hey <span data-mx-spoiler="movie">the movie was awesome</span>',
|
||||
);
|
||||
const { container } = getComponent({ mxEvent: ev }, matrixClient);
|
||||
expect(container).toHaveTextContent("Hey (movie) the movie was awesome");
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("linkification is not applied to code blocks", () => {
|
||||
const ev = mkFormattedMessage(
|
||||
"Visit `https://matrix.org/`\n```\nhttps://matrix.org/\n```",
|
||||
"<p>Visit <code>https://matrix.org/</code></p>\n<pre>https://matrix.org/\n</pre>\n",
|
||||
);
|
||||
const { container } = getComponent({ mxEvent: ev }, matrixClient);
|
||||
expect(container).toHaveTextContent("Visit https://matrix.org/ 1https://matrix.org/");
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// If pills were rendered within a Portal/same shadow DOM then it'd be easier to test
|
||||
it("pills get injected correctly into the DOM", () => {
|
||||
const ev = mkFormattedMessage("Hey User", 'Hey <a href="https://matrix.to/#/@user:server">Member</a>');
|
||||
const { container } = getComponent({ mxEvent: ev }, matrixClient);
|
||||
expect(container).toHaveTextContent("Hey Member");
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("pills do not appear in code blocks", () => {
|
||||
const ev = mkFormattedMessage(
|
||||
"`@room`\n```\n@room\n```",
|
||||
"<p><code>@room</code></p>\n<pre><code>@room\n</code></pre>\n",
|
||||
);
|
||||
const { container } = getComponent({ mxEvent: ev });
|
||||
expect(container).toHaveTextContent("@room 1@room");
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("pills do not appear for event permalinks with a custom label", () => {
|
||||
const ev = mkFormattedMessage(
|
||||
"An [event link](https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/" +
|
||||
"$16085560162aNpaH:example.com?via=example.com) with text",
|
||||
'An <a href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/' +
|
||||
'$16085560162aNpaH:example.com?via=example.com">event link</a> with text',
|
||||
);
|
||||
const { asFragment, container } = getComponent({ mxEvent: ev }, matrixClient);
|
||||
expect(container).toHaveTextContent("An event link with text");
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("pills appear for event permalinks without a custom label", () => {
|
||||
const ev = mkFormattedMessage(
|
||||
"See this message https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/$16085560162aNpaH:example.com?via=example.com",
|
||||
'See this message <a href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/$16085560162aNpaH:example.com?via=example.com">' +
|
||||
"https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/$16085560162aNpaH:example.com?via=example.com</a>",
|
||||
);
|
||||
const { asFragment } = getComponent({ mxEvent: ev }, matrixClient);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("pills appear for room links with vias", () => {
|
||||
const ev = mkFormattedMessage(
|
||||
"A [room link](https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com" +
|
||||
"?via=example.com&via=bob.com) with vias",
|
||||
'A <a href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com' +
|
||||
'?via=example.com&via=bob.com">room link</a> with vias',
|
||||
);
|
||||
const { asFragment, container } = getComponent({ mxEvent: ev }, matrixClient);
|
||||
expect(container).toHaveTextContent("A room name with vias");
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("pills appear for an MXID permalink", () => {
|
||||
const ev = mkFormattedMessage(
|
||||
"Chat with [@user:example.com](https://matrix.to/#/@user:example.com)",
|
||||
'Chat with <a href="https://matrix.to/#/@user:example.com">@user:example.com</a>',
|
||||
);
|
||||
const { container } = getComponent({ mxEvent: ev }, matrixClient);
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders formatted body without html correctly", () => {
|
||||
const ev = mkEvent({
|
||||
type: "m.room.message",
|
||||
room: "room_id",
|
||||
user: "sender",
|
||||
content: {
|
||||
body: "escaped \\*markdown\\*",
|
||||
msgtype: "m.text",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "escaped *markdown*",
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
|
||||
const { container } = getComponent({ mxEvent: ev }, matrixClient);
|
||||
|
||||
const content = container.querySelector(".mx_EventTile_body");
|
||||
expect(content).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders url previews correctly", () => {
|
||||
languageHandler.setMissingEntryGenerator((key) => key.split("|", 2)[1]);
|
||||
|
||||
const matrixClient = getMockClientWithEventEmitter({
|
||||
getRoom: () => mkStubRoom("room_id", "room name", undefined),
|
||||
getAccountData: (): MatrixClient | undefined => undefined,
|
||||
getUrlPreview: (url: string) => new Promise(() => {}),
|
||||
isGuest: () => false,
|
||||
mxcUrlToHttp: (s: string) => s,
|
||||
});
|
||||
DMRoomMap.makeShared(defaultMatrixClient);
|
||||
|
||||
const ev = mkRoomTextMessage("Visit https://matrix.org/");
|
||||
const { container, rerender } = getComponent(
|
||||
{ mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn() },
|
||||
matrixClient,
|
||||
);
|
||||
|
||||
expect(container).toHaveTextContent(ev.getContent().body);
|
||||
expect(container.querySelector("a")).toHaveAttribute("href", "https://matrix.org/");
|
||||
|
||||
// simulate an event edit and check the transition from the old URL preview to the new one
|
||||
const ev2 = mkEvent({
|
||||
type: "m.room.message",
|
||||
room: "room_id",
|
||||
user: "sender",
|
||||
content: {
|
||||
"m.new_content": {
|
||||
body: "Visit https://vector.im/ and https://riot.im/",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
jest.spyOn(ev, "replacingEventDate").mockReturnValue(new Date(1993, 7, 3));
|
||||
ev.makeReplaced(ev2);
|
||||
|
||||
getComponent(
|
||||
{ mxEvent: ev, showUrlPreview: true, onHeightChanged: jest.fn(), replacingEventId: ev.getId() },
|
||||
matrixClient,
|
||||
rerender,
|
||||
);
|
||||
|
||||
expect(container).toHaveTextContent(ev2.getContent()["m.new_content"].body + "(edited)");
|
||||
|
||||
const links = ["https://vector.im/", "https://riot.im/"];
|
||||
const anchorNodes = container.querySelectorAll("a");
|
||||
Array.from(anchorNodes).forEach((node, index) => {
|
||||
expect(node).toHaveAttribute("href", links[index]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DateSeparator renders the date separator correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-label="today"
|
||||
class="mx_TimelineSeparator"
|
||||
role="separator"
|
||||
>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
<div
|
||||
class="mx_DateSeparator_dateContent"
|
||||
>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
class="mx_DateSeparator_dateHeading"
|
||||
>
|
||||
today
|
||||
</h2>
|
||||
</div>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`DateSeparator when feature_jump_to_date is enabled renders the date separator correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-label="Fri, Dec 17, 2021"
|
||||
class="mx_TimelineSeparator"
|
||||
role="separator"
|
||||
>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Jump to date"
|
||||
class="mx_AccessibleButton mx_DateSeparator_jumpToDateMenu mx_DateSeparator_dateContent"
|
||||
data-testid="jump-to-date-separator-button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
class="mx_DateSeparator_dateHeading"
|
||||
>
|
||||
Fri, Dec 17, 2021
|
||||
</h2>
|
||||
<div
|
||||
class="mx_DateSeparator_chevron"
|
||||
/>
|
||||
</div>
|
||||
<hr
|
||||
role="none"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,50 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DecryptionFailureBody Should display "The sender has blocked you from receiving this message" 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBody mx_EventTile_content"
|
||||
>
|
||||
The sender has blocked you from receiving this message because your device is unverified
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DecryptionFailureBody Should display "Unable to decrypt message" 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBody mx_EventTile_content"
|
||||
>
|
||||
Unable to decrypt message
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DecryptionFailureBody should handle messages from users who change identities after verification 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBody mx_EventTile_content mx_DecryptionFailureVerifiedIdentityChanged"
|
||||
>
|
||||
<span>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_16"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.713 17.713A.968.968 0 0 1 12 18a.968.968 0 0 1-.713-.287A.967.967 0 0 1 11 17a.97.97 0 0 1 .287-.712A.968.968 0 0 1 12 16a.97.97 0 0 1 .713.288A.968.968 0 0 1 13 17a.97.97 0 0 1-.287.713Zm0-4A.968.968 0 0 1 12 14a.968.968 0 0 1-.713-.287A.967.967 0 0 1 11 13V9a.97.97 0 0 1 .287-.712A.968.968 0 0 1 12 8a.97.97 0 0 1 .713.288A.968.968 0 0 1 13 9v4a.97.97 0 0 1-.287.713Z"
|
||||
/>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M10.264 3.039c.767-1.344 2.705-1.344 3.472 0l8.554 14.969c.762 1.333-.2 2.992-1.736 2.992H3.446c-1.535 0-2.498-1.659-1.736-2.992l8.553-14.969ZM3.446 19 12 4.031l8.554 14.97H3.446Z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Verified identity has changed
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,41 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`JumpToDatePicker renders the date picker correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<form
|
||||
class="mx_JumpToDatePicker_form"
|
||||
>
|
||||
<span
|
||||
class="mx_JumpToDatePicker_label"
|
||||
>
|
||||
Jump to date
|
||||
</span>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_JumpToDatePicker_datePicker"
|
||||
>
|
||||
<input
|
||||
id="mx_Field_1"
|
||||
label="Pick a date to jump to"
|
||||
max="2021-12-17"
|
||||
placeholder="Pick a date to jump to"
|
||||
tabindex="-1"
|
||||
type="date"
|
||||
value="2020-07-04"
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_1"
|
||||
>
|
||||
Pick a date to jump to
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
class="mx_AccessibleButton mx_JumpToDatePicker_submitButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
type="submit"
|
||||
>
|
||||
Go
|
||||
</button>
|
||||
</form>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,22 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<MBeaconBody /> when map display is not configured renders maps unavailable error for a live beacon with location 1`] = `
|
||||
<div
|
||||
class="mx_MapError mx_MBeaconBody_mapError mx_MBeaconBody_mapErrorInteractive mx_MapError_isMinimised"
|
||||
data-testid="map-rendering-error"
|
||||
>
|
||||
<div
|
||||
class="mx_MapError_icon"
|
||||
/>
|
||||
<h3
|
||||
class="mx_Heading_h3 mx_MapError_heading"
|
||||
>
|
||||
Unable to load map
|
||||
</h3>
|
||||
<p
|
||||
class="mx_MapError_message"
|
||||
>
|
||||
This homeserver is not configured to display maps.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,39 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<MFileBody/> should show a download button in file rendering type 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="mx_MFileBody"
|
||||
>
|
||||
<div
|
||||
class="mx_MFileBody_download"
|
||||
>
|
||||
<a
|
||||
class="_button_i91xf_17 _has-icon_i91xf_66"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
download="alt for a image"
|
||||
href="https://server/_matrix/media/v3/download/server/image"
|
||||
rel="noreferrer noopener"
|
||||
role="link"
|
||||
tabindex="0"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 15.575c-.133 0-.258-.02-.375-.063a.877.877 0 0 1-.325-.212l-3.6-3.6a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7c.183-.183.42-.28.712-.288.292-.008.53.08.713.263L11 12.15V5c0-.283.096-.52.287-.713A.968.968 0 0 1 12 4c.283 0 .52.096.713.287.191.192.287.43.287.713v7.15l1.875-1.875c.183-.183.42-.27.713-.263.291.009.529.105.712.288a.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7l-3.6 3.6c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063ZM6 20c-.55 0-1.02-.196-1.412-.587A1.926 1.926 0 0 1 4 18v-2c0-.283.096-.52.287-.713A.967.967 0 0 1 5 15c.283 0 .52.096.713.287.191.192.287.43.287.713v2h12v-2a.97.97 0 0 1 .288-.713A.968.968 0 0 1 19 15a.97.97 0 0 1 .712.287c.192.192.288.43.288.713v2c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 18 20H6Z"
|
||||
/>
|
||||
</svg>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,86 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<MImageBody/> should generate a thumbnail if one isn't included for animated media 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_MImageBody"
|
||||
>
|
||||
<a
|
||||
href="https://server/_matrix/media/v3/download/server/image"
|
||||
>
|
||||
<div
|
||||
class="mx_MImageBody_thumbnail_container"
|
||||
style="max-height: 50px; max-width: 40px;"
|
||||
>
|
||||
<div
|
||||
class="mx_MImageBody_placeholder"
|
||||
>
|
||||
<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
|
||||
style="max-height: 50px; max-width: 40px;"
|
||||
>
|
||||
<img
|
||||
alt="alt for a test image"
|
||||
class="mx_MImageBody_thumbnail"
|
||||
src="blob:generated-thumb"
|
||||
/>
|
||||
<p
|
||||
class="mx_MImageBody_gifLabel"
|
||||
>
|
||||
GIF
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
style="height: 50px; width: 40px;"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MImageBody/> should show a thumbnail while image is being downloaded 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_MImageBody"
|
||||
>
|
||||
<div
|
||||
class="mx_MImageBody_thumbnail_container"
|
||||
style="max-height: 50px; max-width: 40px;"
|
||||
>
|
||||
<div
|
||||
class="mx_MImageBody_placeholder"
|
||||
>
|
||||
<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
|
||||
style="max-height: 50px; max-width: 40px;"
|
||||
/>
|
||||
<div
|
||||
style="height: 50px; width: 40px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,103 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MLocationBody <MLocationBody> with error displays correct fallback content when map_style_url is misconfigured 1`] = `
|
||||
<div
|
||||
class="mx_EventTile_body mx_MLocationBody"
|
||||
>
|
||||
<span
|
||||
class="mx_EventTile_tileError"
|
||||
>
|
||||
Unable to load map: This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.
|
||||
</span>
|
||||
<br />
|
||||
Shared a location: Found at geo:51.5076,-0.1276 at 2021-12-21T12:22+0000
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`MLocationBody <MLocationBody> with error displays correct fallback content without error style when map_style_url is not configured 1`] = `
|
||||
<div
|
||||
class="mx_EventTile_body mx_MLocationBody"
|
||||
>
|
||||
<span
|
||||
class=""
|
||||
>
|
||||
Unable to load map: This homeserver is not configured to display maps.
|
||||
</span>
|
||||
<br />
|
||||
Shared a location: Found at geo:51.5076,-0.1276 at 2021-12-21T12:22+0000
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`MLocationBody <MLocationBody> without error renders map correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_MLocationBody"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="floating-ui-18"
|
||||
class="mx_MLocationBody_map"
|
||||
>
|
||||
<div
|
||||
class="mx_Map mx_MLocationBody_map"
|
||||
id="mx_Map_mx_MLocationBody_$2_vY7Q4uEh"
|
||||
>
|
||||
<span>
|
||||
<div
|
||||
class="mx_Marker mx_Marker_defaultColor"
|
||||
id="mx_MLocationBody_$2_vY7Q4uEh-marker"
|
||||
>
|
||||
<div
|
||||
class="mx_Marker_border"
|
||||
>
|
||||
<div
|
||||
class="mx_Marker_icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`MLocationBody <MLocationBody> without error renders marker correctly for a self share 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_MLocationBody"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="floating-ui-30"
|
||||
class="mx_MLocationBody_map"
|
||||
>
|
||||
<div
|
||||
class="mx_Map mx_MLocationBody_map"
|
||||
id="mx_Map_mx_MLocationBody_$3_vY7Q4uEh"
|
||||
>
|
||||
<span>
|
||||
<div
|
||||
class="mx_Marker mx_Marker_defaultColor"
|
||||
id="mx_MLocationBody_$3_vY7Q4uEh-marker"
|
||||
>
|
||||
<div
|
||||
class="mx_Marker_border"
|
||||
>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="3"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
title="@user:server"
|
||||
>
|
||||
u
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,117 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<MPollEndBody /> when poll start event does not exist in current timeline fetches the related poll start event and displays a poll tile 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_MPollEndBody_icon"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<MPollEndBody /> when poll start event exists in current timeline renders an ended poll 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_MPollEndBody"
|
||||
>
|
||||
<span
|
||||
class="mx_Caption"
|
||||
>
|
||||
Ended a poll
|
||||
</span>
|
||||
<div
|
||||
class="mx_MPollBody"
|
||||
>
|
||||
<h2
|
||||
data-testid="pollQuestion"
|
||||
>
|
||||
Question?
|
||||
</h2>
|
||||
<div
|
||||
class="mx_MPollBody_allOptions"
|
||||
>
|
||||
<div
|
||||
class="mx_PollOption mx_PollOption_ended"
|
||||
data-testid="pollOption-socks"
|
||||
>
|
||||
<div
|
||||
class="mx_PollOption_endedOption"
|
||||
data-value="socks"
|
||||
>
|
||||
<div
|
||||
class="mx_PollOption_content"
|
||||
>
|
||||
<div
|
||||
class="mx_PollOption_optionText"
|
||||
>
|
||||
Socks
|
||||
</div>
|
||||
<div
|
||||
class="mx_PollOption_optionVoteCount"
|
||||
>
|
||||
0 votes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_PollOption_popularityBackground"
|
||||
>
|
||||
<div
|
||||
class="mx_PollOption_popularityAmount"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_PollOption mx_PollOption_ended"
|
||||
data-testid="pollOption-shoes"
|
||||
>
|
||||
<div
|
||||
class="mx_PollOption_endedOption"
|
||||
data-value="shoes"
|
||||
>
|
||||
<div
|
||||
class="mx_PollOption_content"
|
||||
>
|
||||
<div
|
||||
class="mx_PollOption_optionText"
|
||||
>
|
||||
Shoes
|
||||
</div>
|
||||
<div
|
||||
class="mx_PollOption_optionVoteCount"
|
||||
>
|
||||
0 votes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_PollOption_popularityBackground"
|
||||
>
|
||||
<div
|
||||
class="mx_PollOption_popularityAmount"
|
||||
style="width: 0%;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_MPollBody_totalVotes"
|
||||
data-testid="totalVotes"
|
||||
>
|
||||
Final result based on 0 votes
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 16px; height: 16px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,50 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MVideoBody does not crash when given a portrait image 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
class="mx_MVideoBody"
|
||||
>
|
||||
<div
|
||||
class="mx_MVideoBody_container"
|
||||
style="max-width: 182px; max-height: 324px;"
|
||||
>
|
||||
<video
|
||||
class="mx_MVideoBody"
|
||||
controls=""
|
||||
controlslist="nodownload"
|
||||
poster="data:image/png;base64,00"
|
||||
preload="none"
|
||||
/>
|
||||
<div
|
||||
style="width: 182px; height: 324px;"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`MVideoBody should show poster for encrypted media before downloading it 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
class="mx_MVideoBody"
|
||||
>
|
||||
<div
|
||||
class="mx_MVideoBody_container"
|
||||
style="max-width: 40px; max-height: 50px;"
|
||||
>
|
||||
<video
|
||||
class="mx_MVideoBody"
|
||||
controls=""
|
||||
controlslist="nodownload"
|
||||
poster="https://server/_matrix/media/v3/download/server/encrypted-poster"
|
||||
preload="none"
|
||||
title="alt for a test video"
|
||||
/>
|
||||
<div
|
||||
style="width: 40px; height: 50px;"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,22 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PinnedMessageBadge should render 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_PinnedMessageBadge"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16px"
|
||||
viewBox="0 0 24 24"
|
||||
width="16px"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5.769 2.857A.5.5 0 0 1 6.119 2h11.762a.5.5 0 0 1 .35.857L16.15 4.9a.5.5 0 0 0-.15.357v4.487a.5.5 0 0 0 .15.356l3.7 3.644a.5.5 0 0 1 .15.356v1.4a.5.5 0 0 1-.5.5H13v6a1 1 0 1 1-2 0v-6H4.5a.5.5 0 0 1-.5-.5v-1.4a.5.5 0 0 1 .15-.356l3.7-3.644A.5.5 0 0 0 8 9.744V5.257a.5.5 0 0 0-.15-.357L5.77 2.857Z"
|
||||
/>
|
||||
</svg>
|
||||
Pinned message
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,120 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReactionsRowButton renders reaction row button custom image reactions correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-label="@user1:example.com and @user2:example.com reacted with :test:"
|
||||
class="mx_AccessibleButton mx_ReactionsRowButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<img
|
||||
alt=":test:"
|
||||
class="mx_ReactionsRowButton_content"
|
||||
height="16"
|
||||
src="https://not.a.real.url"
|
||||
width="16"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="mx_ReactionsRowButton_count"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`ReactionsRowButton renders reaction row button custom image reactions correctly 2`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-label="@user1:example.com and @user2:example.com reacted with :test:"
|
||||
class="mx_AccessibleButton mx_ReactionsRowButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<img
|
||||
alt=":test:"
|
||||
class="mx_ReactionsRowButton_content"
|
||||
height="16"
|
||||
src="https://not.a.real.url"
|
||||
width="16"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="mx_ReactionsRowButton_count"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`ReactionsRowButton renders reaction row button emojis correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-label="@user1:example.com and @user2:example.com reacted with 👍"
|
||||
class="mx_AccessibleButton mx_ReactionsRowButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="mx_ReactionsRowButton_content"
|
||||
>
|
||||
👍
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="mx_ReactionsRowButton_count"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`ReactionsRowButton renders reaction row button emojis correctly 2`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-label="@user1:example.com and @user2:example.com reacted with 👍"
|
||||
class="mx_AccessibleButton mx_ReactionsRowButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="mx_ReactionsRowButton_content"
|
||||
>
|
||||
👍
|
||||
</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="mx_ReactionsRowButton_count"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`ReactionsRowButton renders without a room 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_ReactionsRowButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="mx_ReactionsRowButton_content"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="mx_ReactionsRowButton_count"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,24 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<RoomPredecessorTile /> Renders as expected 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_EventTileBubble mx_CreateEvent"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTileBubble_title"
|
||||
>
|
||||
This room is a continuation of another conversation.
|
||||
</div>
|
||||
<div
|
||||
class="mx_EventTileBubble_subtitle"
|
||||
>
|
||||
<a
|
||||
href="https://matrix.to/#/old_room_id/$tombstone_event_id"
|
||||
>
|
||||
Click here to see older messages.
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,432 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<TextualBody /> renders formatted m.text correctly italics, bold, underline and strikethrough render as expected 1`] = `
|
||||
<div
|
||||
class="mx_EventTile_body markdown-body translate"
|
||||
dir="auto"
|
||||
>
|
||||
foo
|
||||
<em>
|
||||
baz
|
||||
</em>
|
||||
|
||||
<strong>
|
||||
bar
|
||||
</strong>
|
||||
|
||||
<del>
|
||||
del
|
||||
</del>
|
||||
|
||||
<u>
|
||||
u
|
||||
</u>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<TextualBody /> renders formatted m.text correctly linkification is not applied to code blocks 1`] = `
|
||||
<div
|
||||
class="mx_EventTile_body markdown-body translate"
|
||||
dir="auto"
|
||||
>
|
||||
<p>
|
||||
Visit
|
||||
<code>
|
||||
https://matrix.org/
|
||||
</code>
|
||||
</p>
|
||||
|
||||
|
||||
<div
|
||||
class="mx_EventTile_pre_container"
|
||||
>
|
||||
<pre
|
||||
class="mx_EventTile_collapsedCodeBlock"
|
||||
>
|
||||
<span
|
||||
class="mx_EventTile_lineNumbers"
|
||||
>
|
||||
<span>
|
||||
1
|
||||
</span>
|
||||
</span>
|
||||
<code>
|
||||
https://matrix.org/
|
||||
|
||||
</code>
|
||||
<span />
|
||||
</pre>
|
||||
<span
|
||||
class="mx_EventTile_button mx_EventTile_copyButton "
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<TextualBody /> renders formatted m.text correctly pills appear for an MXID permalink 1`] = `
|
||||
<div
|
||||
class="mx_EventTile_body markdown-body translate"
|
||||
dir="auto"
|
||||
>
|
||||
Chat with
|
||||
<span>
|
||||
<bdi>
|
||||
<a
|
||||
class="mx_Pill mx_UserPill"
|
||||
href="https://matrix.to/#/@user:example.com"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
aria-label="Profile picture"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 16px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_mcap2_50"
|
||||
data-type="round"
|
||||
height="16px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="mxc://avatar.url/image.png"
|
||||
width="16px"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="mx_Pill_text"
|
||||
>
|
||||
Member
|
||||
</span>
|
||||
</a>
|
||||
</bdi>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<TextualBody /> renders formatted m.text correctly pills appear for event permalinks without a custom label 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body markdown-body translate"
|
||||
dir="auto"
|
||||
>
|
||||
See this message
|
||||
<span>
|
||||
<bdi>
|
||||
<a
|
||||
class="mx_Pill mx_EventPill"
|
||||
href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/$16085560162aNpaH:example.com?via=example.com"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
aria-label="Avatar"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 16px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_mcap2_50"
|
||||
data-type="round"
|
||||
height="16px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="mxc://avatar.url/room.png"
|
||||
width="16px"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="mx_Pill_text"
|
||||
>
|
||||
Message in room name
|
||||
</span>
|
||||
</a>
|
||||
</bdi>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<TextualBody /> renders formatted m.text correctly pills appear for room links with vias 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body markdown-body translate"
|
||||
dir="auto"
|
||||
>
|
||||
A
|
||||
<span>
|
||||
<bdi>
|
||||
<a
|
||||
class="mx_Pill mx_RoomPill"
|
||||
href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com?via=example.com&via=bob.com"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
aria-label="Avatar"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 16px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_mcap2_50"
|
||||
data-type="round"
|
||||
height="16px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="mxc://avatar.url/room.png"
|
||||
width="16px"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="mx_Pill_text"
|
||||
>
|
||||
room name
|
||||
</span>
|
||||
</a>
|
||||
</bdi>
|
||||
</span>
|
||||
with vias
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<TextualBody /> renders formatted m.text correctly pills do not appear for event permalinks with a custom label 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_MTextBody mx_EventTile_content"
|
||||
>
|
||||
<div
|
||||
class="mx_EventTile_body markdown-body translate"
|
||||
dir="auto"
|
||||
>
|
||||
An
|
||||
<a
|
||||
href="https://matrix.to/#/!ZxbRYPQXDXKGmDnJNg:example.com/$16085560162aNpaH:example.com?via=example.com"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
event link
|
||||
</a>
|
||||
with text
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<TextualBody /> renders formatted m.text correctly pills do not appear in code blocks 1`] = `
|
||||
<div
|
||||
class="mx_EventTile_body markdown-body translate"
|
||||
dir="auto"
|
||||
>
|
||||
<p>
|
||||
<code>
|
||||
@room
|
||||
</code>
|
||||
</p>
|
||||
|
||||
|
||||
<div
|
||||
class="mx_EventTile_pre_container"
|
||||
>
|
||||
<pre
|
||||
class="mx_EventTile_collapsedCodeBlock"
|
||||
>
|
||||
<span
|
||||
class="mx_EventTile_lineNumbers"
|
||||
>
|
||||
<span>
|
||||
1
|
||||
</span>
|
||||
</span>
|
||||
<code>
|
||||
@room
|
||||
|
||||
</code>
|
||||
<span />
|
||||
</pre>
|
||||
<span
|
||||
class="mx_EventTile_button mx_EventTile_copyButton "
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<TextualBody /> renders formatted m.text correctly pills get injected correctly into the DOM 1`] = `
|
||||
<div
|
||||
class="mx_EventTile_body markdown-body translate"
|
||||
dir="auto"
|
||||
>
|
||||
Hey
|
||||
<span>
|
||||
<bdi>
|
||||
<a
|
||||
class="mx_Pill mx_UserPill"
|
||||
href="https://matrix.to/#/@user:server"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
aria-label="Profile picture"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 16px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_mcap2_50"
|
||||
data-type="round"
|
||||
height="16px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="mxc://avatar.url/image.png"
|
||||
width="16px"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="mx_Pill_text"
|
||||
>
|
||||
Member
|
||||
</span>
|
||||
</a>
|
||||
</bdi>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<TextualBody /> renders formatted m.text correctly renders formatted body without html correctly 1`] = `
|
||||
<div
|
||||
class="mx_EventTile_body translate"
|
||||
dir="auto"
|
||||
>
|
||||
escaped *markdown*
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<TextualBody /> renders formatted m.text correctly spoilers get injected properly into the DOM 1`] = `
|
||||
<div
|
||||
class="mx_EventTile_body markdown-body translate"
|
||||
dir="auto"
|
||||
>
|
||||
Hey
|
||||
<span>
|
||||
<button
|
||||
class="mx_EventTile_spoiler"
|
||||
>
|
||||
<span
|
||||
class="mx_EventTile_spoiler_reason"
|
||||
>
|
||||
(movie)
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="mx_EventTile_spoiler_content"
|
||||
>
|
||||
<span>
|
||||
the movie was awesome
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<TextualBody /> renders m.emote correctly 1`] = `
|
||||
<span
|
||||
class="mx_EventTile_body translate"
|
||||
>
|
||||
winks
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`<TextualBody /> renders m.notice correctly 1`] = `
|
||||
<div
|
||||
class="mx_EventTile_body translate"
|
||||
dir="auto"
|
||||
>
|
||||
this is a notice, probably from a bot
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<TextualBody /> renders plain-text m.text correctly linkification get applied correctly into the DOM 1`] = `
|
||||
<div
|
||||
class="mx_EventTile_body translate"
|
||||
dir="auto"
|
||||
>
|
||||
Visit
|
||||
<a
|
||||
class="linkified"
|
||||
href="https://matrix.org/"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
https://matrix.org/
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<TextualBody /> renders plain-text m.text correctly should pillify a permalink to a message in the same room with the label »Message from Member« 1`] = `"Visit <span><bdi><a class="mx_Pill mx_EventPill" href="https://matrix.to/#/!room1:example.com/%event_id%"><span aria-label="Profile picture" aria-hidden="true" data-testid="avatar-img" data-type="round" data-color="2" class="_avatar_mcap2_17 mx_BaseAvatar" style="--cpd-avatar-size: 16px;"><img loading="lazy" alt="" src="mxc://avatar.url/image.png" referrerpolicy="no-referrer" class="_image_mcap2_50" data-type="round" width="16px" height="16px"></span><span class="mx_Pill_text">Message from Member</span></a></bdi></span>"`;
|
||||
|
||||
exports[`<TextualBody /> renders plain-text m.text correctly should pillify a permalink to an event in another room with the label »Message in Room 2« 1`] = `"Visit <span><bdi><a class="mx_Pill mx_EventPill" href="https://matrix.to/#/!room2:example.com/%event_id%"><span aria-label="Avatar" aria-hidden="true" data-testid="avatar-img" data-type="round" data-color="2" class="_avatar_mcap2_17 mx_BaseAvatar" style="--cpd-avatar-size: 16px;"><img loading="lazy" alt="" src="mxc://avatar.url/room.png" referrerpolicy="no-referrer" class="_image_mcap2_50" data-type="round" width="16px" height="16px"></span><span class="mx_Pill_text">Message in Room 2</span></a></bdi></span>"`;
|
||||
|
||||
exports[`<TextualBody /> renders plain-text m.text correctly should pillify a permalink to an unknown message in the same room with the label »Message« 1`] = `
|
||||
<div
|
||||
class="mx_EventTile_body translate"
|
||||
dir="auto"
|
||||
>
|
||||
Visit
|
||||
<span>
|
||||
<bdi>
|
||||
<a
|
||||
class="mx_Pill mx_EventPill"
|
||||
href="https://matrix.to/#/!room1:example.com/!abc123"
|
||||
>
|
||||
<svg
|
||||
class="mx_Pill_LinkIcon mx_BaseAvatar"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="mx_Pill_text"
|
||||
>
|
||||
Message
|
||||
</span>
|
||||
</a>
|
||||
</bdi>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<TextualBody /> renders plain-text m.text correctly simple message renders as expected 1`] = `
|
||||
<div
|
||||
class="mx_EventTile_body translate"
|
||||
dir="auto"
|
||||
>
|
||||
this is a plaintext message
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import MediaProcessingError from "../../../../../../src/components/views/messages/shared/MediaProcessingError";
|
||||
|
||||
describe("<MediaProcessingError />", () => {
|
||||
const defaultProps = {
|
||||
className: "test-classname",
|
||||
children: "Something went wrong",
|
||||
};
|
||||
const getComponent = (props = {}) => render(<MediaProcessingError {...defaultProps} {...props} />);
|
||||
|
||||
it("renders", () => {
|
||||
const { container } = getComponent();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<MediaProcessingError /> renders 1`] = `
|
||||
<div>
|
||||
<span
|
||||
class="test-classname"
|
||||
>
|
||||
<div
|
||||
class="mx_MediaProcessingError_Icon"
|
||||
height="16"
|
||||
width="16"
|
||||
/>
|
||||
Something went wrong
|
||||
</span>
|
||||
</div>
|
||||
`;
|
Loading…
Add table
Add a link
Reference in a new issue