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
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
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 ContextMenu, { ChevronFace } from "../../../../../src/components/structures/ContextMenu";
|
||||
import UIStore from "../../../../../src/stores/UIStore";
|
||||
import Modal from "../../../../../src/Modal";
|
||||
import BaseDialog from "../../../../../src/components/views/dialogs/BaseDialog";
|
||||
|
||||
describe("<ContextMenu />", () => {
|
||||
// Hardcode window and menu dimensions
|
||||
const windowSize = 300;
|
||||
const menuSize = 200;
|
||||
jest.spyOn(UIStore, "instance", "get").mockImplementation(
|
||||
() =>
|
||||
({
|
||||
windowWidth: windowSize,
|
||||
windowHeight: windowSize,
|
||||
}) as unknown as UIStore,
|
||||
);
|
||||
window.Element.prototype.getBoundingClientRect = jest.fn().mockReturnValue({
|
||||
width: menuSize,
|
||||
height: menuSize,
|
||||
});
|
||||
|
||||
const targetChevronOffset = 25;
|
||||
|
||||
it("near top edge of window", () => {
|
||||
const targetY = -50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
render(
|
||||
<ContextMenu
|
||||
bottom={windowSize - targetY - menuSize}
|
||||
right={menuSize}
|
||||
onFinished={onFinished}
|
||||
chevronFace={ChevronFace.Left}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
const chevron = document.querySelector<HTMLElement>(".mx_ContextualMenu_chevron_left")!;
|
||||
|
||||
const bottomStyle = parseInt(
|
||||
document.querySelector<HTMLElement>(".mx_ContextualMenu_wrapper")!.style.getPropertyValue("bottom"),
|
||||
);
|
||||
const actualY = windowSize - bottomStyle - menuSize;
|
||||
const actualChevronOffset = parseInt(chevron.style.getPropertyValue("top"));
|
||||
|
||||
// stays within the window
|
||||
expect(actualY).toBeGreaterThanOrEqual(0);
|
||||
// positions the chevron correctly
|
||||
expect(actualChevronOffset).toEqual(targetChevronOffset + targetY - actualY);
|
||||
});
|
||||
|
||||
it("near right edge of window", () => {
|
||||
const targetX = windowSize - menuSize + 50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
render(
|
||||
<ContextMenu
|
||||
bottom={0}
|
||||
onFinished={onFinished}
|
||||
left={targetX}
|
||||
chevronFace={ChevronFace.Top}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
const chevron = document.querySelector<HTMLElement>(".mx_ContextualMenu_chevron_top")!;
|
||||
|
||||
const actualX = parseInt(
|
||||
document.querySelector<HTMLElement>(".mx_ContextualMenu_wrapper")!.style.getPropertyValue("left"),
|
||||
);
|
||||
const actualChevronOffset = parseInt(chevron.style.getPropertyValue("left"));
|
||||
|
||||
// stays within the window
|
||||
expect(actualX + menuSize).toBeLessThanOrEqual(windowSize);
|
||||
// positions the chevron correctly
|
||||
expect(actualChevronOffset).toEqual(targetChevronOffset + targetX - actualX);
|
||||
});
|
||||
|
||||
it("near bottom edge of window", () => {
|
||||
const targetY = windowSize - menuSize + 50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
render(
|
||||
<ContextMenu
|
||||
top={targetY}
|
||||
left={0}
|
||||
onFinished={onFinished}
|
||||
chevronFace={ChevronFace.Right}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
const chevron = document.querySelector<HTMLElement>(".mx_ContextualMenu_chevron_right")!;
|
||||
|
||||
const actualY = parseInt(
|
||||
document.querySelector<HTMLElement>(".mx_ContextualMenu_wrapper")!.style.getPropertyValue("top"),
|
||||
);
|
||||
const actualChevronOffset = parseInt(chevron.style.getPropertyValue("top"));
|
||||
|
||||
// stays within the window
|
||||
expect(actualY + menuSize).toBeLessThanOrEqual(windowSize);
|
||||
// positions the chevron correctly
|
||||
expect(actualChevronOffset).toEqual(targetChevronOffset + targetY - actualY);
|
||||
});
|
||||
|
||||
it("near left edge of window", () => {
|
||||
const targetX = -50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
render(
|
||||
<ContextMenu
|
||||
top={0}
|
||||
right={windowSize - targetX - menuSize}
|
||||
chevronFace={ChevronFace.Bottom}
|
||||
onFinished={onFinished}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
const chevron = document.querySelector<HTMLElement>(".mx_ContextualMenu_chevron_bottom")!;
|
||||
|
||||
const rightStyle = parseInt(
|
||||
document.querySelector<HTMLElement>(".mx_ContextualMenu_wrapper")!.style.getPropertyValue("right"),
|
||||
);
|
||||
const actualX = windowSize - rightStyle - menuSize;
|
||||
const actualChevronOffset = parseInt(chevron.style.getPropertyValue("left"));
|
||||
|
||||
// stays within the window
|
||||
expect(actualX).toBeGreaterThanOrEqual(0);
|
||||
// positions the chevron correctly
|
||||
expect(actualChevronOffset).toEqual(targetChevronOffset + targetX - actualX);
|
||||
});
|
||||
|
||||
it("should automatically close when a modal is opened", () => {
|
||||
const targetX = -50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
render(
|
||||
<ContextMenu
|
||||
top={0}
|
||||
right={windowSize - targetX - menuSize}
|
||||
chevronFace={ChevronFace.Bottom}
|
||||
onFinished={onFinished}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
Modal.createDialog(BaseDialog);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not automatically close when a modal is opened under the existing one", () => {
|
||||
const targetX = -50;
|
||||
const onFinished = jest.fn();
|
||||
|
||||
Modal.createDialog(BaseDialog);
|
||||
render(
|
||||
<ContextMenu
|
||||
top={0}
|
||||
right={windowSize - targetX - menuSize}
|
||||
chevronFace={ChevronFace.Bottom}
|
||||
onFinished={onFinished}
|
||||
chevronOffset={targetChevronOffset}
|
||||
>
|
||||
<React.Fragment />
|
||||
</ContextMenu>,
|
||||
);
|
||||
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
Modal.createDialog(BaseDialog, {}, "", false, true);
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
Modal.appendDialog(BaseDialog);
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
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 fetchMock from "fetch-mock-jest";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { _t } from "../../../../../src/languageHandler";
|
||||
import EmbeddedPage from "../../../../../src/components/structures/EmbeddedPage";
|
||||
|
||||
jest.mock("../../../../../src/languageHandler", () => ({
|
||||
_t: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<EmbeddedPage />", () => {
|
||||
it("should translate _t strings", async () => {
|
||||
mocked(_t).mockReturnValue("Przeglądaj pokoje");
|
||||
fetchMock.get("https://home.page", {
|
||||
body: '<h1>_t("Explore rooms")</h1>',
|
||||
});
|
||||
|
||||
const { asFragment } = render(<EmbeddedPage url="https://home.page" />);
|
||||
await screen.findByText("Przeglądaj pokoje");
|
||||
expect(_t).toHaveBeenCalledWith("Explore rooms");
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should show error if unable to load", async () => {
|
||||
mocked(_t).mockReturnValue("Couldn't load page");
|
||||
fetchMock.get("https://other.page", {
|
||||
status: 404,
|
||||
});
|
||||
|
||||
const { asFragment } = render(<EmbeddedPage url="https://other.page" />);
|
||||
await screen.findByText("Couldn't load page");
|
||||
expect(_t).toHaveBeenCalledWith("cant_load_page");
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render nothing if no url given", () => {
|
||||
const { asFragment } = render(<EmbeddedPage />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,553 @@
|
|||
/*
|
||||
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, RenderResult, screen, waitFor } from "jest-matrix-react";
|
||||
import {
|
||||
EventStatus,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
PendingEventOrdering,
|
||||
BeaconIdentifier,
|
||||
Beacon,
|
||||
getBeaconInfoIdentifier,
|
||||
EventType,
|
||||
FeatureSupport,
|
||||
Thread,
|
||||
M_POLL_KIND_DISCLOSED,
|
||||
EventTimeline,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
||||
import { mocked } from "jest-mock";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
|
||||
import { IRoomState } from "../../../../../src/components/structures/RoomView";
|
||||
import { canEditContent } from "../../../../../src/utils/EventUtils";
|
||||
import { copyPlaintext, getSelectedText } from "../../../../../src/utils/strings";
|
||||
import MessageContextMenu from "../../../../../src/components/views/context_menus/MessageContextMenu";
|
||||
import { makeBeaconEvent, makeBeaconInfoEvent, makeLocationEvent, stubClient } from "../../../../test-utils";
|
||||
import dispatcher from "../../../../../src/dispatcher/dispatcher";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import { ReadPinsEventId } from "../../../../../src/components/views/right_panel/types";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
|
||||
import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
|
||||
import { createMessageEventContent } from "../../../../test-utils/events";
|
||||
|
||||
jest.mock("../../../../../src/utils/strings", () => ({
|
||||
copyPlaintext: jest.fn(),
|
||||
getSelectedText: jest.fn(),
|
||||
}));
|
||||
jest.mock("../../../../../src/utils/EventUtils", () => ({
|
||||
...(jest.requireActual("../../../../../src/utils/EventUtils") as object),
|
||||
canEditContent: jest.fn(),
|
||||
}));
|
||||
jest.mock("../../../../../src/dispatcher/dispatcher");
|
||||
|
||||
const roomId = "roomid";
|
||||
|
||||
describe("MessageContextMenu", () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
stubClient();
|
||||
});
|
||||
|
||||
it("does show copy link button when supplied a link", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const props = {
|
||||
link: "https://google.com/",
|
||||
};
|
||||
createMenuWithContent(eventContent, props);
|
||||
const copyLinkButton = document.querySelector('a[aria-label="Copy link"]');
|
||||
expect(copyLinkButton).toHaveAttribute("href", props.link);
|
||||
});
|
||||
|
||||
it("does not show copy link button when not supplied a link", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
createMenuWithContent(eventContent);
|
||||
const copyLinkButton = document.querySelector('a[aria-label="Copy link"]');
|
||||
expect(copyLinkButton).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("message pinning", () => {
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
room = makeDefaultRoom();
|
||||
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
jest.spyOn(
|
||||
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"mayClientSendStateEvent",
|
||||
).mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockRestore();
|
||||
});
|
||||
|
||||
it("does not show pin option when user does not have rights to pin", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const event = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
|
||||
// mock permission to disallow adding pinned messages to room
|
||||
jest.spyOn(
|
||||
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"mayClientSendStateEvent",
|
||||
).mockReturnValue(false);
|
||||
|
||||
createMenu(event, { rightClick: true }, {}, undefined, room);
|
||||
|
||||
expect(screen.queryByRole("menuitem", { name: "Pin" })).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not show pin option for beacon_info event", () => {
|
||||
const deadBeaconEvent = makeBeaconInfoEvent("@alice:server.org", roomId, { isLive: false });
|
||||
|
||||
createMenu(deadBeaconEvent, { rightClick: true }, {}, undefined, room);
|
||||
|
||||
expect(screen.queryByRole("menuitem", { name: "Pin" })).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows pin option when pinning feature is enabled", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const pinnableEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: eventContent,
|
||||
room_id: roomId,
|
||||
});
|
||||
|
||||
createMenu(pinnableEvent, { rightClick: true }, {}, undefined, room);
|
||||
|
||||
expect(screen.getByRole("menuitem", { name: "Pin" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("pins event on pin option click", async () => {
|
||||
const onFinished = jest.fn();
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const pinnableEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: eventContent,
|
||||
room_id: roomId,
|
||||
});
|
||||
pinnableEvent.event.event_id = "!3";
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
|
||||
jest.spyOn(room.getLiveTimeline().getState(EventTimeline.FORWARDS)!, "getStateEvents").mockReturnValue({
|
||||
// @ts-ignore
|
||||
getContent: () => ({ pinned: ["!1", "!2"] }),
|
||||
});
|
||||
|
||||
// mock read pins account data
|
||||
const pinsAccountData = new MatrixEvent({ content: { event_ids: ["!1", "!2"] } });
|
||||
jest.spyOn(room, "getAccountData").mockReturnValue(pinsAccountData);
|
||||
|
||||
createMenu(pinnableEvent, { onFinished, rightClick: true }, {}, undefined, room);
|
||||
|
||||
await userEvent.click(screen.getByRole("menuitem", { name: "Pin" }));
|
||||
|
||||
// added to account data
|
||||
await waitFor(() =>
|
||||
expect(client.setRoomAccountData).toHaveBeenCalledWith(roomId, ReadPinsEventId, {
|
||||
event_ids: [
|
||||
// from account data
|
||||
"!1",
|
||||
"!2",
|
||||
pinnableEvent.getId(),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
// add to room's pins
|
||||
await waitFor(() =>
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
EventType.RoomPinnedEvents,
|
||||
{
|
||||
pinned: ["!1", "!2", pinnableEvent.getId()],
|
||||
},
|
||||
"",
|
||||
),
|
||||
);
|
||||
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("unpins event on pin option click when event is pinned", async () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const pinnableEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: eventContent,
|
||||
room_id: roomId,
|
||||
});
|
||||
pinnableEvent.event.event_id = "!3";
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
|
||||
// make the event already pinned in the room
|
||||
const pinEvent = new MatrixEvent({
|
||||
type: EventType.RoomPinnedEvents,
|
||||
room_id: roomId,
|
||||
state_key: "",
|
||||
content: { pinned: [pinnableEvent.getId(), "!another-event"] },
|
||||
});
|
||||
room.getLiveTimeline().getState(EventTimeline.FORWARDS)!.setStateEvents([pinEvent]);
|
||||
|
||||
// mock read pins account data
|
||||
const pinsAccountData = new MatrixEvent({ content: { event_ids: ["!1", "!2"] } });
|
||||
jest.spyOn(room, "getAccountData").mockReturnValue(pinsAccountData);
|
||||
|
||||
createMenu(pinnableEvent, { rightClick: true }, {}, undefined, room);
|
||||
|
||||
await userEvent.click(screen.getByRole("menuitem", { name: "Unpin" }));
|
||||
|
||||
expect(client.setRoomAccountData).not.toHaveBeenCalled();
|
||||
|
||||
// add to room's pins
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
EventType.RoomPinnedEvents,
|
||||
// pinnableEvent's id removed, other pins intact
|
||||
{ pinned: ["!another-event"] },
|
||||
"",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("message forwarding", () => {
|
||||
it("allows forwarding a room message", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
createMenuWithContent(eventContent);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not allow forwarding a poll", () => {
|
||||
const eventContent = PollStartEvent.from("why?", ["42"], M_POLL_KIND_DISCLOSED);
|
||||
createMenuWithContent(eventContent);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not allow forwarding a voice broadcast", () => {
|
||||
const broadcastStartEvent = mkVoiceBroadcastInfoStateEvent(
|
||||
roomId,
|
||||
VoiceBroadcastInfoState.Started,
|
||||
"@user:example.com",
|
||||
"ABC123",
|
||||
);
|
||||
createMenu(broadcastStartEvent);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("forwarding beacons", () => {
|
||||
const aliceId = "@alice:server.org";
|
||||
|
||||
it("does not allow forwarding a beacon that is not live", () => {
|
||||
const deadBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: false });
|
||||
const beacon = new Beacon(deadBeaconEvent);
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(deadBeaconEvent), beacon);
|
||||
createMenu(deadBeaconEvent, {}, {}, beacons);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not allow forwarding a beacon that is not live but has a latestLocation", () => {
|
||||
const deadBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: false });
|
||||
const beaconLocation = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: deadBeaconEvent.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
});
|
||||
const beacon = new Beacon(deadBeaconEvent);
|
||||
// @ts-ignore illegally set private prop
|
||||
beacon._latestLocationEvent = beaconLocation;
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(deadBeaconEvent), beacon);
|
||||
createMenu(deadBeaconEvent, {}, {}, beacons);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not allow forwarding a live beacon that does not have a latestLocation", () => {
|
||||
const beaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true });
|
||||
|
||||
const beacon = new Beacon(beaconEvent);
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(beaconEvent), beacon);
|
||||
createMenu(beaconEvent, {}, {}, beacons);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("allows forwarding a live beacon that has a location", () => {
|
||||
const liveBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true });
|
||||
const beaconLocation = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: liveBeaconEvent.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
});
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
// @ts-ignore illegally set private prop
|
||||
beacon._latestLocationEvent = beaconLocation;
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(liveBeaconEvent), beacon);
|
||||
createMenu(liveBeaconEvent, {}, {}, beacons);
|
||||
expect(document.querySelector('li[aria-label="Forward"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it("opens forward dialog with correct event", () => {
|
||||
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
const liveBeaconEvent = makeBeaconInfoEvent(aliceId, roomId, { isLive: true });
|
||||
const beaconLocation = makeBeaconEvent(aliceId, {
|
||||
beaconInfoId: liveBeaconEvent.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
});
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
// @ts-ignore illegally set private prop
|
||||
beacon._latestLocationEvent = beaconLocation;
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(liveBeaconEvent), beacon);
|
||||
createMenu(liveBeaconEvent, {}, {}, beacons);
|
||||
|
||||
fireEvent.click(document.querySelector('li[aria-label="Forward"]')!);
|
||||
|
||||
// called with forwardableEvent, not beaconInfo event
|
||||
expect(dispatchSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: beaconLocation,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("open as map link", () => {
|
||||
it("does not allow opening a plain message in open street maps", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
createMenuWithContent(eventContent);
|
||||
expect(document.querySelector('a[aria-label="Open in OpenStreetMap"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not allow opening a beacon that does not have a shareable location event", () => {
|
||||
const deadBeaconEvent = makeBeaconInfoEvent("@alice", roomId, { isLive: false });
|
||||
const beacon = new Beacon(deadBeaconEvent);
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(deadBeaconEvent), beacon);
|
||||
createMenu(deadBeaconEvent, {}, {}, beacons);
|
||||
expect(document.querySelector('a[aria-label="Open in OpenStreetMap"]')).toBeFalsy();
|
||||
});
|
||||
|
||||
it("allows opening a location event in open street map", () => {
|
||||
const locationEvent = makeLocationEvent("geo:50,50");
|
||||
createMenu(locationEvent);
|
||||
// exists with a href with the lat/lon from the location event
|
||||
expect(document.querySelector('a[aria-label="Open in OpenStreetMap"]')).toHaveAttribute(
|
||||
"href",
|
||||
"https://www.openstreetmap.org/?mlat=50&mlon=50#map=16/50/50",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows opening a beacon that has a shareable location event", () => {
|
||||
const liveBeaconEvent = makeBeaconInfoEvent("@alice", roomId, { isLive: true });
|
||||
const beaconLocation = makeBeaconEvent("@alice", {
|
||||
beaconInfoId: liveBeaconEvent.getId(),
|
||||
geoUri: "geo:51,41",
|
||||
});
|
||||
const beacon = new Beacon(liveBeaconEvent);
|
||||
// @ts-ignore illegally set private prop
|
||||
beacon._latestLocationEvent = beaconLocation;
|
||||
const beacons = new Map<BeaconIdentifier, Beacon>();
|
||||
beacons.set(getBeaconInfoIdentifier(liveBeaconEvent), beacon);
|
||||
createMenu(liveBeaconEvent, {}, {}, beacons);
|
||||
// exists with a href with the lat/lon from the location event
|
||||
expect(document.querySelector('a[aria-label="Open in OpenStreetMap"]')).toHaveAttribute(
|
||||
"href",
|
||||
"https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("right click", () => {
|
||||
it("copy button does work as expected", () => {
|
||||
const text = "hello";
|
||||
const eventContent = createMessageEventContent(text);
|
||||
mocked(getSelectedText).mockReturnValue(text);
|
||||
|
||||
createRightClickMenuWithContent(eventContent);
|
||||
const copyButton = document.querySelector('li[aria-label="Copy"]')!;
|
||||
fireEvent.mouseDown(copyButton);
|
||||
expect(copyPlaintext).toHaveBeenCalledWith(text);
|
||||
});
|
||||
|
||||
it("copy button is not shown when there is nothing to copy", () => {
|
||||
const text = "hello";
|
||||
const eventContent = createMessageEventContent(text);
|
||||
mocked(getSelectedText).mockReturnValue("");
|
||||
|
||||
createRightClickMenuWithContent(eventContent);
|
||||
const copyButton = document.querySelector('li[aria-label="Copy"]');
|
||||
expect(copyButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows edit button when we can edit", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
mocked(canEditContent).mockReturnValue(true);
|
||||
|
||||
createRightClickMenuWithContent(eventContent);
|
||||
const editButton = document.querySelector('li[aria-label="Edit"]');
|
||||
expect(editButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not show edit button when we cannot edit", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
mocked(canEditContent).mockReturnValue(false);
|
||||
|
||||
createRightClickMenuWithContent(eventContent);
|
||||
const editButton = document.querySelector('li[aria-label="Edit"]');
|
||||
expect(editButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows reply button when we can reply", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const context = {
|
||||
canSendMessages: true,
|
||||
};
|
||||
|
||||
createRightClickMenuWithContent(eventContent, context);
|
||||
const replyButton = document.querySelector('li[aria-label="Reply"]');
|
||||
expect(replyButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not show reply button when we cannot reply", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const context = {
|
||||
canSendMessages: true,
|
||||
};
|
||||
const unsentMessage = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
// queued messages are not actionable
|
||||
unsentMessage.setStatus(EventStatus.QUEUED);
|
||||
|
||||
createMenu(unsentMessage, {}, context);
|
||||
const replyButton = document.querySelector('li[aria-label="Reply"]');
|
||||
expect(replyButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows react button when we can react", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const context = {
|
||||
canReact: true,
|
||||
};
|
||||
|
||||
createRightClickMenuWithContent(eventContent, context);
|
||||
const reactButton = document.querySelector('li[aria-label="React"]');
|
||||
expect(reactButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not show react button when we cannot react", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const context = {
|
||||
canReact: false,
|
||||
};
|
||||
|
||||
createRightClickMenuWithContent(eventContent, context);
|
||||
const reactButton = document.querySelector('li[aria-label="React"]');
|
||||
expect(reactButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("shows view in room button when the event is a thread root", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const mxEvent = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
mxEvent.getThread = () => ({ rootEvent: mxEvent }) as Thread;
|
||||
const props = {
|
||||
rightClick: true,
|
||||
};
|
||||
const context = {
|
||||
timelineRenderingType: TimelineRenderingType.Thread,
|
||||
};
|
||||
|
||||
createMenu(mxEvent, props, context);
|
||||
const reactButton = document.querySelector('li[aria-label="View in room"]');
|
||||
expect(reactButton).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not show view in room button when the event is not a thread root", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
|
||||
createRightClickMenuWithContent(eventContent);
|
||||
const reactButton = document.querySelector('li[aria-label="View in room"]');
|
||||
expect(reactButton).toBeFalsy();
|
||||
});
|
||||
|
||||
it("creates a new thread on reply in thread click", () => {
|
||||
const eventContent = createMessageEventContent("hello");
|
||||
const mxEvent = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
|
||||
Thread.hasServerSideSupport = FeatureSupport.Stable;
|
||||
const context = {
|
||||
canSendMessages: true,
|
||||
};
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
|
||||
createRightClickMenu(mxEvent, context);
|
||||
|
||||
const replyInThreadButton = document.querySelector('li[aria-label="Reply in thread"]')!;
|
||||
fireEvent.click(replyInThreadButton);
|
||||
|
||||
expect(dispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ShowThread,
|
||||
rootEvent: mxEvent,
|
||||
push: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createRightClickMenuWithContent(eventContent: object, context?: Partial<IRoomState>): RenderResult {
|
||||
return createMenuWithContent(eventContent, { rightClick: true }, context);
|
||||
}
|
||||
|
||||
function createRightClickMenu(mxEvent: MatrixEvent, context?: Partial<IRoomState>): RenderResult {
|
||||
return createMenu(mxEvent, { rightClick: true }, context);
|
||||
}
|
||||
|
||||
function createMenuWithContent(
|
||||
eventContent: object,
|
||||
props?: Partial<MessageContextMenu["props"]>,
|
||||
context?: Partial<IRoomState>,
|
||||
): RenderResult {
|
||||
// XXX: We probably shouldn't be assuming all events are going to be message events, but considering this
|
||||
// test is for the Message context menu, it's a fairly safe assumption.
|
||||
const mxEvent = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||
return createMenu(mxEvent, props, context);
|
||||
}
|
||||
|
||||
function makeDefaultRoom(): Room {
|
||||
return new Room(roomId, MatrixClientPeg.safeGet(), "@user:example.com", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
}
|
||||
|
||||
function createMenu(
|
||||
mxEvent: MatrixEvent,
|
||||
props?: Partial<MessageContextMenu["props"]>,
|
||||
context: Partial<IRoomState> = {},
|
||||
beacons: Map<BeaconIdentifier, Beacon> = new Map(),
|
||||
room: Room = makeDefaultRoom(),
|
||||
): RenderResult {
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
|
||||
// @ts-ignore illegally set private prop
|
||||
room.currentState.beacons = beacons;
|
||||
|
||||
mxEvent.setStatus(EventStatus.SENT);
|
||||
|
||||
client.getUserId = jest.fn().mockReturnValue("@user:example.com");
|
||||
client.getRoom = jest.fn().mockReturnValue(room);
|
||||
|
||||
return render(
|
||||
<RoomContext.Provider value={context as IRoomState}>
|
||||
<MessageContextMenu mxEvent={mxEvent} onFinished={jest.fn()} {...props} />
|
||||
</RoomContext.Provider>,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
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 { fireEvent, getByLabelText, render, screen } from "jest-matrix-react";
|
||||
import { mocked } from "jest-mock";
|
||||
import { ReceiptType, MatrixClient, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import React from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { ChevronFace } from "../../../../../src/components/structures/ContextMenu";
|
||||
import {
|
||||
RoomGeneralContextMenu,
|
||||
RoomGeneralContextMenuProps,
|
||||
} from "../../../../../src/components/views/context_menus/RoomGeneralContextMenu";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import { DefaultTagID } from "../../../../../src/stores/room-list/models";
|
||||
import RoomListStore from "../../../../../src/stores/room-list/RoomListStore";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import { mkMessage, stubClient } from "../../../../test-utils/test-utils";
|
||||
import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../../../src/settings/UIFeature";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import { clearAllModals } from "../../../../test-utils";
|
||||
|
||||
jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("RoomGeneralContextMenu", () => {
|
||||
const ROOM_ID = "!123:matrix.org";
|
||||
|
||||
let room: Room;
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
let onFinished: () => void;
|
||||
|
||||
function getComponent(props?: Partial<RoomGeneralContextMenuProps>) {
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<RoomGeneralContextMenu
|
||||
room={room}
|
||||
onFinished={onFinished}
|
||||
{...props}
|
||||
managed={true}
|
||||
mountAsChild={true}
|
||||
left={1}
|
||||
top={1}
|
||||
chevronFace={ChevronFace.Left}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
mockClient = mocked(MatrixClientPeg.safeGet());
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
const dmRoomMap = {
|
||||
getUserIdForRoomId: jest.fn(),
|
||||
} as unknown as DMRoomMap;
|
||||
DMRoomMap.setShared(dmRoomMap);
|
||||
|
||||
jest.spyOn(RoomListStore.instance, "getTagsForRoom").mockReturnValueOnce([
|
||||
DefaultTagID.DM,
|
||||
DefaultTagID.Favourite,
|
||||
]);
|
||||
|
||||
onFinished = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clearAllModals();
|
||||
});
|
||||
|
||||
it("renders an empty context menu for archived rooms", async () => {
|
||||
jest.spyOn(RoomListStore.instance, "getTagsForRoom").mockReturnValueOnce([DefaultTagID.Archived]);
|
||||
|
||||
const { container } = getComponent({});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders the default context menu", async () => {
|
||||
const { container } = getComponent({});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("does not render invite menu item when UIComponent customisations disable room invite", () => {
|
||||
room.updateMyMembership(KnownMembership.Join);
|
||||
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
||||
mocked(shouldShowComponent).mockReturnValue(false);
|
||||
|
||||
getComponent({});
|
||||
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.InviteUsers);
|
||||
expect(screen.queryByRole("menuitem", { name: "Invite" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders invite menu item when UIComponent customisations enables room invite", () => {
|
||||
room.updateMyMembership(KnownMembership.Join);
|
||||
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
|
||||
getComponent({});
|
||||
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.InviteUsers);
|
||||
expect(screen.getByRole("menuitem", { name: "Invite" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("marks the room as read", async () => {
|
||||
const event = mkMessage({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
ts: 1000,
|
||||
});
|
||||
room.addLiveEvents([event], {});
|
||||
|
||||
const { container } = getComponent({});
|
||||
|
||||
const markAsReadBtn = getByLabelText(container, "Mark as read");
|
||||
fireEvent.click(markAsReadBtn);
|
||||
|
||||
await sleep(0);
|
||||
|
||||
expect(mockClient.sendReadReceipt).toHaveBeenCalledWith(event, ReceiptType.Read, true);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("marks the room as unread", async () => {
|
||||
room.updateMyMembership("join");
|
||||
|
||||
const { container } = getComponent({});
|
||||
|
||||
const markAsUnreadBtn = getByLabelText(container, "Mark as unread");
|
||||
fireEvent.click(markAsUnreadBtn);
|
||||
|
||||
await sleep(0);
|
||||
|
||||
expect(mockClient.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "com.famedly.marked_unread", {
|
||||
unread: true,
|
||||
});
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("when developer mode is disabled, it should not render the developer tools option", () => {
|
||||
getComponent();
|
||||
expect(screen.queryByText("Developer tools")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when developer mode is enabled", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "developerMode");
|
||||
getComponent();
|
||||
});
|
||||
|
||||
it("should render the developer tools option", async () => {
|
||||
const developerToolsItem = screen.getByRole("menuitem", { name: "Developer tools" });
|
||||
expect(developerToolsItem).toBeInTheDocument();
|
||||
|
||||
// click open developer tools dialog
|
||||
await userEvent.click(developerToolsItem);
|
||||
|
||||
// assert that the dialog is displayed by searching some if its contents
|
||||
expect(await screen.findByText("Toolbox")).toBeInTheDocument();
|
||||
expect(await screen.findByText(`Room ID: ${ROOM_ID}`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
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 { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { Mocked, mocked } from "jest-mock";
|
||||
import { prettyDOM, render, RenderResult, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import SpaceContextMenu from "../../../../../src/components/views/context_menus/SpaceContextMenu";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import {
|
||||
shouldShowSpaceSettings,
|
||||
showCreateNewRoom,
|
||||
showCreateNewSubspace,
|
||||
showSpaceInvite,
|
||||
showSpaceSettings,
|
||||
} from "../../../../../src/utils/space";
|
||||
import { leaveSpace } from "../../../../../src/utils/leave-behaviour";
|
||||
import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../../../src/settings/UIFeature";
|
||||
|
||||
jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/utils/space", () => ({
|
||||
shouldShowSpaceSettings: jest.fn(),
|
||||
showCreateNewRoom: jest.fn(),
|
||||
showCreateNewSubspace: jest.fn(),
|
||||
showSpaceInvite: jest.fn(),
|
||||
showSpacePreferences: jest.fn(),
|
||||
showSpaceSettings: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../../src/utils/leave-behaviour", () => ({
|
||||
leaveSpace: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("<SpaceContextMenu />", () => {
|
||||
const userId = "@test:server";
|
||||
|
||||
const mockClient = {
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
getSafeUserId: jest.fn().mockReturnValue(userId),
|
||||
} as unknown as Mocked<MatrixClient>;
|
||||
|
||||
const makeMockSpace = (props = {}) =>
|
||||
({
|
||||
name: "test space",
|
||||
getJoinRule: jest.fn(),
|
||||
canInvite: jest.fn(),
|
||||
currentState: {
|
||||
maySendStateEvent: jest.fn(),
|
||||
},
|
||||
client: mockClient,
|
||||
getMyMembership: jest.fn(),
|
||||
...props,
|
||||
}) as unknown as Room;
|
||||
|
||||
const defaultProps = {
|
||||
space: makeMockSpace(),
|
||||
onFinished: jest.fn(),
|
||||
};
|
||||
|
||||
const renderComponent = (props = {}): RenderResult =>
|
||||
render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<SpaceContextMenu {...defaultProps} {...props} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
mockClient.getUserId.mockReturnValue(userId);
|
||||
mockClient.getSafeUserId.mockReturnValue(userId);
|
||||
});
|
||||
|
||||
it("renders menu correctly", () => {
|
||||
const { baseElement } = renderComponent();
|
||||
expect(prettyDOM(baseElement)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders invite option when space is public", () => {
|
||||
const space = makeMockSpace({
|
||||
getJoinRule: jest.fn().mockReturnValue("public"),
|
||||
});
|
||||
renderComponent({ space });
|
||||
expect(screen.getByTestId("invite-option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders invite option when user is has invite rights for space", () => {
|
||||
const space = makeMockSpace({
|
||||
canInvite: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
renderComponent({ space });
|
||||
expect(space.canInvite).toHaveBeenCalledWith(userId);
|
||||
expect(screen.getByTestId("invite-option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens invite dialog when invite option is clicked", async () => {
|
||||
const space = makeMockSpace({
|
||||
getJoinRule: jest.fn().mockReturnValue("public"),
|
||||
});
|
||||
const onFinished = jest.fn();
|
||||
renderComponent({ space, onFinished });
|
||||
|
||||
await userEvent.click(screen.getByTestId("invite-option"));
|
||||
|
||||
expect(showSpaceInvite).toHaveBeenCalledWith(space);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders space settings option when user has rights", () => {
|
||||
mocked(shouldShowSpaceSettings).mockReturnValue(true);
|
||||
renderComponent();
|
||||
expect(shouldShowSpaceSettings).toHaveBeenCalledWith(defaultProps.space);
|
||||
expect(screen.getByTestId("settings-option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens space settings when space settings option is clicked", async () => {
|
||||
mocked(shouldShowSpaceSettings).mockReturnValue(true);
|
||||
const onFinished = jest.fn();
|
||||
renderComponent({ onFinished });
|
||||
|
||||
await userEvent.click(screen.getByTestId("settings-option"));
|
||||
|
||||
expect(showSpaceSettings).toHaveBeenCalledWith(defaultProps.space);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders leave option when user does not have rights to see space settings", () => {
|
||||
renderComponent();
|
||||
expect(screen.getByTestId("leave-option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("leaves space when leave option is clicked", async () => {
|
||||
const onFinished = jest.fn();
|
||||
renderComponent({ onFinished });
|
||||
await userEvent.click(screen.getByTestId("leave-option"));
|
||||
expect(leaveSpace).toHaveBeenCalledWith(defaultProps.space);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("add children section", () => {
|
||||
const space = makeMockSpace();
|
||||
|
||||
beforeEach(() => {
|
||||
// set space to allow adding children to space
|
||||
mocked(space.currentState.maySendStateEvent).mockReturnValue(true);
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("does not render section when user does not have permission to add children", () => {
|
||||
mocked(space.currentState.maySendStateEvent).mockReturnValue(false);
|
||||
renderComponent({ space });
|
||||
|
||||
expect(screen.queryByTestId("add-to-space-header")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-room-option")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-subspace-option")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render section when UIComponent customisations disable room and space creation", () => {
|
||||
mocked(shouldShowComponent).mockReturnValue(false);
|
||||
renderComponent({ space });
|
||||
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.CreateRooms);
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.CreateSpaces);
|
||||
|
||||
expect(screen.queryByTestId("add-to-space-header")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-room-option")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-subspace-option")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders section with add room button when UIComponent customisation allows CreateRoom", () => {
|
||||
// only allow CreateRoom
|
||||
mocked(shouldShowComponent).mockImplementation((feature) => feature === UIComponent.CreateRooms);
|
||||
renderComponent({ space });
|
||||
|
||||
expect(screen.getByTestId("add-to-space-header")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("new-room-option")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-subspace-option")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders section with add space button when UIComponent customisation allows CreateSpace", () => {
|
||||
// only allow CreateSpaces
|
||||
mocked(shouldShowComponent).mockImplementation((feature) => feature === UIComponent.CreateSpaces);
|
||||
renderComponent({ space });
|
||||
|
||||
expect(screen.getByTestId("add-to-space-header")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("new-room-option")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("new-subspace-option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens create room dialog on add room button click", async () => {
|
||||
const onFinished = jest.fn();
|
||||
renderComponent({ space, onFinished });
|
||||
|
||||
await userEvent.click(screen.getByTestId("new-room-option"));
|
||||
expect(showCreateNewRoom).toHaveBeenCalledWith(space);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens create space dialog on add space button click", async () => {
|
||||
const onFinished = jest.fn();
|
||||
renderComponent({ space, onFinished });
|
||||
|
||||
await userEvent.click(screen.getByTestId("new-subspace-option"));
|
||||
expect(showCreateNewSubspace).toHaveBeenCalledWith(space);
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
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 { getByTestId, render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, PendingEventOrdering, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import React from "react";
|
||||
|
||||
import ThreadListContextMenu, {
|
||||
ThreadListContextMenuProps,
|
||||
} from "../../../../../src/components/views/context_menus/ThreadListContextMenu";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
||||
import { stubClient } from "../../../../test-utils/test-utils";
|
||||
import { mkThread } from "../../../../test-utils/threads";
|
||||
|
||||
describe("ThreadListContextMenu", () => {
|
||||
const ROOM_ID = "!123:matrix.org";
|
||||
|
||||
let room: Room;
|
||||
let mockClient: MatrixClient;
|
||||
let event: MatrixEvent;
|
||||
|
||||
function getComponent(props: Partial<ThreadListContextMenuProps>) {
|
||||
return render(<ThreadListContextMenu mxEvent={event} {...props} />);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
mockClient = mocked(MatrixClientPeg.safeGet());
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
const res = mkThread({
|
||||
room,
|
||||
client: mockClient,
|
||||
authorId: mockClient.getUserId()!,
|
||||
participantUserIds: [mockClient.getUserId()!],
|
||||
});
|
||||
|
||||
event = res.rootEvent;
|
||||
});
|
||||
|
||||
it("does not render the permalink", async () => {
|
||||
const { container } = getComponent({});
|
||||
|
||||
const btn = getByTestId(container, "threadlist-dropdown-button");
|
||||
await userEvent.click(btn);
|
||||
expect(screen.queryByTestId("copy-thread-link")).toBeNull();
|
||||
});
|
||||
|
||||
it("does render the permalink", async () => {
|
||||
const { container } = getComponent({
|
||||
permalinkCreator: new RoomPermalinkCreator(room, room.roomId, false),
|
||||
});
|
||||
|
||||
const btn = getByTestId(container, "threadlist-dropdown-button");
|
||||
await userEvent.click(btn);
|
||||
expect(screen.queryByTestId("copy-thread-link")).not.toBeNull();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 Mikhail Aheichyk
|
||||
Copyright 2023 Nordeck IT + Consulting GmbH.
|
||||
|
||||
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 { screen, render } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixWidgetType } from "matrix-widget-api";
|
||||
import {
|
||||
ApprovalOpts,
|
||||
WidgetInfo,
|
||||
WidgetLifecycle,
|
||||
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
||||
|
||||
import { WidgetContextMenu } from "../../../../../src/components/views/context_menus/WidgetContextMenu";
|
||||
import { IApp } from "../../../../../src/stores/WidgetStore";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import WidgetUtils from "../../../../../src/utils/WidgetUtils";
|
||||
import { ModuleRunner } from "../../../../../src/modules/ModuleRunner";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
|
||||
describe("<WidgetContextMenu />", () => {
|
||||
const widgetId = "w1";
|
||||
const eventId = "e1";
|
||||
const roomId = "r1";
|
||||
const userId = "@user-id:server";
|
||||
|
||||
const app: IApp = {
|
||||
id: widgetId,
|
||||
eventId,
|
||||
roomId,
|
||||
type: MatrixWidgetType.Custom,
|
||||
url: "https://example.com",
|
||||
name: "Example 1",
|
||||
creatorUserId: userId,
|
||||
avatar_url: undefined,
|
||||
};
|
||||
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
let onFinished: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
onFinished = jest.fn();
|
||||
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
|
||||
|
||||
mockClient = {
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
} as unknown as MatrixClient;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function getComponent(props: Partial<ComponentProps<typeof WidgetContextMenu>> = {}): JSX.Element {
|
||||
return (
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<WidgetContextMenu app={app} onFinished={onFinished} {...props} />
|
||||
</MatrixClientContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
it("renders revoke button", async () => {
|
||||
const { rerender } = render(getComponent());
|
||||
|
||||
const revokeButton = screen.getByLabelText("Revoke permissions");
|
||||
expect(revokeButton).toBeInTheDocument();
|
||||
|
||||
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
|
||||
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === widgetId) {
|
||||
(opts as ApprovalOpts).approved = true;
|
||||
}
|
||||
});
|
||||
|
||||
rerender(getComponent());
|
||||
expect(revokeButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("revokes permissions", async () => {
|
||||
render(getComponent());
|
||||
await userEvent.click(screen.getByLabelText("Revoke permissions"));
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
expect(SettingsStore.getValue("allowedWidgets", roomId)[eventId]).toBe(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<EmbeddedPage /> should render nothing if no url given 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="undefined_guest"
|
||||
>
|
||||
<div
|
||||
class="undefined_body"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmbeddedPage /> should show error if unable to load 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="undefined_guest"
|
||||
>
|
||||
<div
|
||||
class="undefined_body"
|
||||
>
|
||||
Couldn't load page
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<EmbeddedPage /> should translate _t strings 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="undefined_guest"
|
||||
>
|
||||
<div
|
||||
class="undefined_body"
|
||||
>
|
||||
<h1>
|
||||
Przeglądaj pokoje
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,97 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RoomGeneralContextMenu renders an empty context menu for archived rooms 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_ContextualMenu_wrapper"
|
||||
style="top: 1px; left: 1px;"
|
||||
>
|
||||
<div
|
||||
class="mx_ContextualMenu_background"
|
||||
/>
|
||||
<div
|
||||
class="mx_ContextualMenu mx_ContextualMenu_withChevron_left"
|
||||
role="menu"
|
||||
>
|
||||
<div
|
||||
class="mx_ContextualMenu_chevron_left"
|
||||
/>
|
||||
<ul
|
||||
class="mx_IconizedContextMenu mx_RoomGeneralContextMenu mx_IconizedContextMenu_compact"
|
||||
role="none"
|
||||
>
|
||||
<div
|
||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst"
|
||||
/>
|
||||
<div
|
||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst mx_IconizedContextMenu_optionList_red"
|
||||
>
|
||||
<li
|
||||
aria-label="Forget Room"
|
||||
class="mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_icon mx_RoomGeneralContextMenu_iconSignOut"
|
||||
/>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_label"
|
||||
>
|
||||
Forget Room
|
||||
</span>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomGeneralContextMenu renders the default context menu 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_ContextualMenu_wrapper"
|
||||
style="top: 1px; left: 1px;"
|
||||
>
|
||||
<div
|
||||
class="mx_ContextualMenu_background"
|
||||
/>
|
||||
<div
|
||||
class="mx_ContextualMenu mx_ContextualMenu_withChevron_left"
|
||||
role="menu"
|
||||
>
|
||||
<div
|
||||
class="mx_ContextualMenu_chevron_left"
|
||||
/>
|
||||
<ul
|
||||
class="mx_IconizedContextMenu mx_RoomGeneralContextMenu mx_IconizedContextMenu_compact"
|
||||
role="none"
|
||||
>
|
||||
<div
|
||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst"
|
||||
/>
|
||||
<div
|
||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst mx_IconizedContextMenu_optionList_red"
|
||||
>
|
||||
<li
|
||||
aria-label="Forget Room"
|
||||
class="mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_icon mx_RoomGeneralContextMenu_iconSignOut"
|
||||
/>
|
||||
<span
|
||||
class="mx_IconizedContextMenu_label"
|
||||
>
|
||||
Forget Room
|
||||
</span>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,98 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SpaceContextMenu /> renders menu correctly 1`] = `
|
||||
"[36m<body>[39m
|
||||
[36m<div />[39m
|
||||
[36m<div[39m
|
||||
[33mid[39m=[32m"mx_ContextualMenu_Container"[39m
|
||||
[36m>[39m
|
||||
[36m<div[39m
|
||||
[33mclass[39m=[32m"mx_ContextualMenu_wrapper"[39m
|
||||
[36m>[39m
|
||||
[36m<div[39m
|
||||
[33mclass[39m=[32m"mx_ContextualMenu_background"[39m
|
||||
[36m/>[39m
|
||||
[36m<div[39m
|
||||
[33mclass[39m=[32m"mx_ContextualMenu"[39m
|
||||
[33mrole[39m=[32m"menu"[39m
|
||||
[36m>[39m
|
||||
[36m<ul[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu mx_SpacePanel_contextMenu mx_IconizedContextMenu_compact"[39m
|
||||
[33mrole[39m=[32m"none"[39m
|
||||
[36m>[39m
|
||||
[36m<div[39m
|
||||
[33mclass[39m=[32m"mx_SpacePanel_contextMenu_header"[39m
|
||||
[36m>[39m
|
||||
[0mtest space[0m
|
||||
[36m</div>[39m
|
||||
[36m<div[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_optionList"[39m
|
||||
[36m>[39m
|
||||
[36m<li[39m
|
||||
[33maria-label[39m=[32m"Space home"[39m
|
||||
[33mclass[39m=[32m"mx_AccessibleButton mx_IconizedContextMenu_item"[39m
|
||||
[33mrole[39m=[32m"menuitem"[39m
|
||||
[33mtabindex[39m=[32m"0"[39m
|
||||
[36m>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_icon mx_SpacePanel_iconHome"[39m
|
||||
[36m/>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_label"[39m
|
||||
[36m>[39m
|
||||
[0mSpace home[0m
|
||||
[36m</span>[39m
|
||||
[36m</li>[39m
|
||||
[36m<li[39m
|
||||
[33maria-label[39m=[32m"Explore rooms"[39m
|
||||
[33mclass[39m=[32m"mx_AccessibleButton mx_IconizedContextMenu_item"[39m
|
||||
[33mrole[39m=[32m"menuitem"[39m
|
||||
[33mtabindex[39m=[32m"-1"[39m
|
||||
[36m>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_icon mx_SpacePanel_iconExplore"[39m
|
||||
[36m/>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_label"[39m
|
||||
[36m>[39m
|
||||
[0mExplore rooms[0m
|
||||
[36m</span>[39m
|
||||
[36m</li>[39m
|
||||
[36m<li[39m
|
||||
[33maria-label[39m=[32m"Preferences"[39m
|
||||
[33mclass[39m=[32m"mx_AccessibleButton mx_IconizedContextMenu_item"[39m
|
||||
[33mrole[39m=[32m"menuitem"[39m
|
||||
[33mtabindex[39m=[32m"-1"[39m
|
||||
[36m>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_icon mx_SpacePanel_iconPreferences"[39m
|
||||
[36m/>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_label"[39m
|
||||
[36m>[39m
|
||||
[0mPreferences[0m
|
||||
[36m</span>[39m
|
||||
[36m</li>[39m
|
||||
[36m<li[39m
|
||||
[33maria-label[39m=[32m"Leave space"[39m
|
||||
[33mclass[39m=[32m"mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"[39m
|
||||
[33mdata-testid[39m=[32m"leave-option"[39m
|
||||
[33mrole[39m=[32m"menuitem"[39m
|
||||
[33mtabindex[39m=[32m"-1"[39m
|
||||
[36m>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_icon mx_SpacePanel_iconLeave"[39m
|
||||
[36m/>[39m
|
||||
[36m<span[39m
|
||||
[33mclass[39m=[32m"mx_IconizedContextMenu_label"[39m
|
||||
[36m>[39m
|
||||
[0mLeave space[0m
|
||||
[36m</span>[39m
|
||||
[36m</li>[39m
|
||||
[36m</div>[39m
|
||||
[36m</ul>[39m
|
||||
[36m</div>[39m
|
||||
[36m</div>[39m
|
||||
[36m</div>[39m
|
||||
[36m</body>[39m"
|
||||
`;
|
Loading…
Add table
Add a link
Reference in a new issue