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:
Michael Telatynski 2024-10-15 14:57:26 +01:00
commit f0ee7f7905
No known key found for this signature in database
GPG key ID: A2B008A5F49F5D0D
3265 changed files with 484599 additions and 699 deletions

View file

@ -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();
});
});

View file

@ -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();
});
});

View file

@ -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>,
);
}

View file

@ -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();
});
});
});

View file

@ -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();
});
});
});

View file

@ -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();
});
});

View file

@ -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);
});
});

View file

@ -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>
`;

View file

@ -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>
`;

View file

@ -0,0 +1,98 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SpaceContextMenu /> renders menu correctly 1`] = `
"<body>
<div />
<div
id="mx_ContextualMenu_Container"
>
<div
class="mx_ContextualMenu_wrapper"
>
<div
class="mx_ContextualMenu_background"
/>
<div
class="mx_ContextualMenu"
role="menu"
>
<ul
class="mx_IconizedContextMenu mx_SpacePanel_contextMenu mx_IconizedContextMenu_compact"
role="none"
>
<div
class="mx_SpacePanel_contextMenu_header"
>
test space
</div>
<div
class="mx_IconizedContextMenu_optionList"
>
<li
aria-label="Space home"
class="mx_AccessibleButton mx_IconizedContextMenu_item"
role="menuitem"
tabindex="0"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconHome"
/>
<span
class="mx_IconizedContextMenu_label"
>
Space home
</span>
</li>
<li
aria-label="Explore rooms"
class="mx_AccessibleButton mx_IconizedContextMenu_item"
role="menuitem"
tabindex="-1"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconExplore"
/>
<span
class="mx_IconizedContextMenu_label"
>
Explore rooms
</span>
</li>
<li
aria-label="Preferences"
class="mx_AccessibleButton mx_IconizedContextMenu_item"
role="menuitem"
tabindex="-1"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconPreferences"
/>
<span
class="mx_IconizedContextMenu_label"
>
Preferences
</span>
</li>
<li
aria-label="Leave space"
class="mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
data-testid="leave-option"
role="menuitem"
tabindex="-1"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconLeave"
/>
<span
class="mx_IconizedContextMenu_label"
>
Leave space
</span>
</li>
</div>
</ul>
</div>
</div>
</div>
</body>"
`;