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,164 @@
/*
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 { fireEvent, getByText, render } from "jest-matrix-react";
import React from "react";
import AccessibleButton from "../../../../../src/components/views/elements/AccessibleButton";
import { Key } from "../../../../../src/Keyboard";
import { mockPlatformPeg, unmockPlatformPeg } from "../../../../test-utils";
describe("<AccessibleButton />", () => {
const defaultProps = {
onClick: jest.fn(),
children: "i am a button",
};
const getComponent = (props = {}) => render(<AccessibleButton {...defaultProps} {...props} />);
beforeEach(() => {
mockPlatformPeg();
});
afterAll(() => {
unmockPlatformPeg();
});
it("renders div with role button by default", () => {
const { asFragment } = getComponent();
expect(asFragment()).toMatchSnapshot();
});
it("renders a button element", () => {
const { asFragment } = getComponent({ element: "button" });
expect(asFragment()).toMatchSnapshot();
});
it("renders with correct classes when button has kind", () => {
const { asFragment } = getComponent({
kind: "primary",
});
expect(asFragment()).toMatchSnapshot();
});
it("disables button correctly", () => {
const onClick = jest.fn();
const { container } = getComponent({
onClick,
disabled: true,
});
const btn = getByText(container, "i am a button");
expect(btn.hasAttribute("disabled")).toBeTruthy();
expect(btn.hasAttribute("aria-disabled")).toBeTruthy();
fireEvent.click(btn);
expect(onClick).not.toHaveBeenCalled();
fireEvent.keyPress(btn, { key: Key.ENTER, code: Key.ENTER });
expect(onClick).not.toHaveBeenCalled();
});
it("calls onClick handler on button click", () => {
const onClick = jest.fn();
const { container } = getComponent({
onClick,
});
const btn = getByText(container, "i am a button");
fireEvent.click(btn);
expect(onClick).toHaveBeenCalled();
});
it("calls onClick handler on button mousedown when triggerOnMousedown is passed", () => {
const onClick = jest.fn();
const { container } = getComponent({
onClick,
triggerOnMouseDown: true,
});
const btn = getByText(container, "i am a button");
fireEvent.mouseDown(btn);
expect(onClick).toHaveBeenCalled();
});
describe("handling keyboard events", () => {
it("calls onClick handler on enter keydown", () => {
const onClick = jest.fn();
const { container } = getComponent({
onClick,
});
const btn = getByText(container, "i am a button");
fireEvent.keyDown(btn, { key: Key.ENTER, code: Key.ENTER });
expect(onClick).toHaveBeenCalled();
fireEvent.keyUp(btn, { key: Key.ENTER, code: Key.ENTER });
// handler only called once on keydown
expect(onClick).toHaveBeenCalledTimes(1);
});
it("calls onClick handler on space keyup", () => {
const onClick = jest.fn();
const { container } = getComponent({
onClick,
});
const btn = getByText(container, "i am a button");
fireEvent.keyDown(btn, { key: Key.SPACE, code: Key.SPACE });
expect(onClick).not.toHaveBeenCalled();
fireEvent.keyUp(btn, { key: Key.SPACE, code: Key.SPACE });
// handler only called once on keyup
expect(onClick).toHaveBeenCalledTimes(1);
});
it("calls onKeydown/onKeyUp handlers for keys other than space and enter", () => {
const onClick = jest.fn();
const onKeyDown = jest.fn();
const onKeyUp = jest.fn();
const { container } = getComponent({
onClick,
onKeyDown,
onKeyUp,
});
const btn = getByText(container, "i am a button");
fireEvent.keyDown(btn, { key: Key.K, code: Key.K });
fireEvent.keyUp(btn, { key: Key.K, code: Key.K });
expect(onClick).not.toHaveBeenCalled();
expect(onKeyDown).toHaveBeenCalled();
expect(onKeyUp).toHaveBeenCalled();
});
it("does nothing on non space/enter key presses when no onKeydown/onKeyUp handlers provided", () => {
const onClick = jest.fn();
const { container } = getComponent({
onClick,
});
const btn = getByText(container, "i am a button");
fireEvent.keyDown(btn, { key: Key.K, code: Key.K });
fireEvent.keyUp(btn, { key: Key.K, code: Key.K });
expect(onClick).not.toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,479 @@
/*
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 { jest } from "@jest/globals";
import { Room, MatrixClient } from "matrix-js-sdk/src/matrix";
import { ClientWidgetApi, IWidget, MatrixWidgetType } from "matrix-widget-api";
import { Optional } from "matrix-events-sdk";
import { act, render, RenderResult } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { SpiedFunction } from "jest-mock";
import {
ApprovalOpts,
WidgetInfo,
WidgetLifecycle,
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
import RightPanel from "../../../../../src/components/structures/RightPanel";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import ResizeNotifier from "../../../../../src/utils/ResizeNotifier";
import { stubClient } from "../../../../test-utils";
import { Action } from "../../../../../src/dispatcher/actions";
import dis from "../../../../../src/dispatcher/dispatcher";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases";
import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelStore";
import { UPDATE_EVENT } from "../../../../../src/stores/AsyncStore";
import WidgetStore, { IApp } from "../../../../../src/stores/WidgetStore";
import ActiveWidgetStore from "../../../../../src/stores/ActiveWidgetStore";
import AppTile from "../../../../../src/components/views/elements/AppTile";
import { Container, WidgetLayoutStore } from "../../../../../src/stores/widgets/WidgetLayoutStore";
import AppsDrawer from "../../../../../src/components/views/rooms/AppsDrawer";
import { ElementWidgetCapabilities } from "../../../../../src/stores/widgets/ElementWidgetCapabilities";
import { ElementWidget } from "../../../../../src/stores/widgets/StopGapWidget";
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
import { ModuleRunner } from "../../../../../src/modules/ModuleRunner";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
jest.mock("../../../../../src/stores/OwnProfileStore", () => ({
OwnProfileStore: {
instance: {
isProfileInfoFetched: true,
removeListener: jest.fn(),
getHttpAvatarUrl: jest.fn().mockReturnValue("http://avatar_url"),
},
},
}));
describe("AppTile", () => {
let cli: MatrixClient;
let r1: Room;
let r2: Room;
const resizeNotifier = new ResizeNotifier();
let app1: IApp;
let app2: IApp;
const waitForRps = (roomId: string) =>
new Promise<void>((resolve) => {
const update = () => {
if (RightPanelStore.instance.currentCardForRoom(roomId).phase !== RightPanelPhases.Widget) return;
RightPanelStore.instance.off(UPDATE_EVENT, update);
resolve();
};
RightPanelStore.instance.on(UPDATE_EVENT, update);
});
beforeAll(async () => {
stubClient();
cli = MatrixClientPeg.safeGet();
cli.hasLazyLoadMembersEnabled = () => false;
// Init misc. startup deps
DMRoomMap.makeShared(cli);
r1 = new Room("r1", cli, "@name:example.com");
r2 = new Room("r2", cli, "@name:example.com");
jest.spyOn(cli, "getRoom").mockImplementation((roomId) => {
if (roomId === "r1") return r1;
if (roomId === "r2") return r2;
return null;
});
jest.spyOn(cli, "getVisibleRooms").mockImplementation(() => {
return [r1, r2];
});
// Adjust various widget stores to add mock apps
app1 = {
id: "1",
eventId: "1",
roomId: "r1",
type: MatrixWidgetType.Custom,
url: "https://example.com",
name: "Example 1",
creatorUserId: cli.getSafeUserId(),
avatar_url: undefined,
};
app2 = {
id: "1",
eventId: "2",
roomId: "r2",
type: MatrixWidgetType.Custom,
url: "https://example.com",
name: "Example 2",
creatorUserId: cli.getSafeUserId(),
avatar_url: undefined,
};
jest.spyOn(WidgetStore.instance, "getApps").mockImplementation((roomId: string): Array<IApp> => {
if (roomId === "r1") return [app1];
if (roomId === "r2") return [app2];
return [];
});
// Wake up various stores we rely on
WidgetLayoutStore.instance.useUnitTestClient(cli);
// @ts-ignore
await WidgetLayoutStore.instance.onReady();
RightPanelStore.instance.useUnitTestClient(cli);
// @ts-ignore
await RightPanelStore.instance.onReady();
});
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockRestore();
});
it("destroys non-persisted right panel widget on room change", async () => {
// Set up right panel state
const realGetValue = SettingsStore.getValue;
const mockSettings = jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
if (name !== "RightPanel.phases") return realGetValue(name, roomId);
if (roomId === "r1") {
return {
history: [
{
phase: RightPanelPhases.Widget,
state: {
widgetId: "1",
},
},
],
isOpen: true,
};
}
return null;
});
// Run initial render with room 1, and also running lifecycle methods
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<RightPanel
room={r1}
resizeNotifier={resizeNotifier}
permalinkCreator={new RoomPermalinkCreator(r1, r1.roomId)}
/>
</MatrixClientContext.Provider>,
);
// Wait for RPS room 1 updates to fire
const rpsUpdated = waitForRps("r1");
dis.dispatch({
action: Action.ViewRoom,
room_id: "r1",
});
await rpsUpdated;
expect(renderResult.getByText("Example 1")).toBeInTheDocument();
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
const { container, asFragment } = renderResult;
expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy();
expect(asFragment()).toMatchSnapshot();
// We want to verify that as we change to room 2, we should close the
// right panel and destroy the widget.
// Switch to room 2
dis.dispatch({
action: Action.ViewRoom,
room_id: "r2",
});
renderResult.rerender(
<MatrixClientContext.Provider value={cli}>
<RightPanel
room={r2}
resizeNotifier={resizeNotifier}
permalinkCreator={new RoomPermalinkCreator(r2, r2.roomId)}
/>
</MatrixClientContext.Provider>,
);
expect(renderResult.queryByText("Example 1")).not.toBeInTheDocument();
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(false);
mockSettings.mockRestore();
});
it("distinguishes widgets with the same ID in different rooms", async () => {
// Set up right panel state
const realGetValue = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
if (name === "RightPanel.phases") {
if (roomId === "r1") {
return {
history: [
{
phase: RightPanelPhases.Widget,
state: {
widgetId: "1",
},
},
],
isOpen: true,
};
}
return null;
}
return realGetValue(name, roomId);
});
// Run initial render with room 1, and also running lifecycle methods
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<RightPanel
room={r1}
resizeNotifier={resizeNotifier}
permalinkCreator={new RoomPermalinkCreator(r1, r1.roomId)}
/>
</MatrixClientContext.Provider>,
);
// Wait for RPS room 1 updates to fire
const rpsUpdated1 = waitForRps("r1");
dis.dispatch({
action: Action.ViewRoom,
room_id: "r1",
});
await rpsUpdated1;
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
expect(ActiveWidgetStore.instance.isLive("1", "r2")).toBe(false);
jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
if (name === "RightPanel.phases") {
if (roomId === "r2") {
return {
history: [
{
phase: RightPanelPhases.Widget,
state: {
widgetId: "1",
},
},
],
isOpen: true,
};
}
return null;
}
return realGetValue(name, roomId);
});
// Wait for RPS room 2 updates to fire
const rpsUpdated2 = waitForRps("r2");
// Switch to room 2
dis.dispatch({
action: Action.ViewRoom,
room_id: "r2",
});
renderResult.rerender(
<MatrixClientContext.Provider value={cli}>
<RightPanel
room={r2}
resizeNotifier={resizeNotifier}
permalinkCreator={new RoomPermalinkCreator(r2, r2.roomId)}
/>
</MatrixClientContext.Provider>,
);
await rpsUpdated2;
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(false);
expect(ActiveWidgetStore.instance.isLive("1", "r2")).toBe(true);
});
it("preserves non-persisted widget on container move", async () => {
// Set up widget in top container
const realGetValue = SettingsStore.getValue;
const mockSettings = jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
if (name !== "Widgets.layout") return realGetValue(name, roomId);
if (roomId === "r1") {
return {
widgets: {
1: {
container: Container.Top,
},
},
};
}
return null;
});
act(() => {
WidgetLayoutStore.instance.recalculateRoom(r1);
});
// Run initial render with room 1, and also running lifecycle methods
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppsDrawer userId={cli.getSafeUserId()} room={r1} resizeNotifier={resizeNotifier} />
</MatrixClientContext.Provider>,
);
expect(renderResult.getByText("Example 1")).toBeInTheDocument();
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
const { asFragment } = renderResult;
expect(asFragment()).toMatchSnapshot(); // Take snapshot of AppsDrawer with AppTile
// We want to verify that as we move the widget to the center container,
// the widget frame remains running.
// Stop mocking settings so that the widget move can take effect
mockSettings.mockRestore();
act(() => {
// Move widget to center
WidgetLayoutStore.instance.moveToContainer(r1, app1, Container.Center);
});
expect(renderResult.getByText("Example 1")).toBeInTheDocument();
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
});
afterAll(async () => {
// @ts-ignore
await WidgetLayoutStore.instance.onNotReady();
// @ts-ignore
await RightPanelStore.instance.onNotReady();
jest.restoreAllMocks();
});
describe("for a pinned widget", () => {
let renderResult: RenderResult;
let moveToContainerSpy: SpiedFunction<typeof WidgetLayoutStore.instance.moveToContainer>;
beforeEach(() => {
renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
);
moveToContainerSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
});
it("should render", () => {
const { container, asFragment } = renderResult;
expect(container.querySelector(".mx_Spinner")).toBeFalsy(); // Assert that the spinner is gone
expect(asFragment()).toMatchSnapshot(); // Take a snapshot of the pinned widget
});
it("should not display the »Popout widget« button", () => {
expect(renderResult.queryByLabelText("Popout widget")).not.toBeInTheDocument();
});
it("clicking 'minimise' should send the widget to the right", async () => {
await userEvent.click(renderResult.getByLabelText("Minimise"));
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Right);
});
it("clicking 'maximise' should send the widget to the center", async () => {
await userEvent.click(renderResult.getByLabelText("Maximise"));
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Center);
});
it("should render permission request", () => {
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === app1.id) {
(opts as ApprovalOpts).approved = false;
}
});
// userId and creatorUserId are different
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
</MatrixClientContext.Provider>,
);
const { container, asFragment } = renderResult;
expect(container.querySelector(".mx_Spinner")).toBeFalsy();
expect(asFragment()).toMatchSnapshot();
expect(renderResult.queryByRole("button", { name: "Continue" })).toBeInTheDocument();
});
it("should not display 'Continue' button on permission load", () => {
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === app1.id) {
(opts as ApprovalOpts).approved = true;
}
});
// userId and creatorUserId are different
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
</MatrixClientContext.Provider>,
);
expect(renderResult.queryByRole("button", { name: "Continue" })).not.toBeInTheDocument();
});
describe("for a maximised (centered) widget", () => {
beforeEach(() => {
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockImplementation(
(room: Optional<Room>, widget: IWidget, container: Container) => {
return room === r1 && widget === app1 && container === Container.Center;
},
);
});
it("clicking 'un-maximise' should send the widget to the top", async () => {
await userEvent.click(renderResult.getByLabelText("Un-maximise"));
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Top);
});
});
describe("with an existing widgetApi with requiresClient = false", () => {
beforeEach(() => {
const api = {
hasCapability: (capability: ElementWidgetCapabilities): boolean => {
return !(capability === ElementWidgetCapabilities.RequiresClient);
},
once: () => {},
stop: () => {},
} as unknown as ClientWidgetApi;
const mockWidget = new ElementWidget(app1);
WidgetMessagingStore.instance.storeMessaging(mockWidget, r1.roomId, api);
renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
);
});
it("should display the »Popout widget« button", () => {
expect(renderResult.getByLabelText("Popout widget")).toBeInTheDocument();
});
});
});
describe("for a persistent app", () => {
let renderResult: RenderResult;
beforeEach(() => {
renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} fullWidth={true} room={r1} miniMode={true} showMenubar={false} />
</MatrixClientContext.Provider>,
);
});
it("should render", () => {
const { container, asFragment } = renderResult;
expect(container.querySelector(".mx_Spinner")).toBeFalsy();
expect(asFragment()).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,92 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import DesktopCapturerSourcePicker from "../../../../../src/components/views/elements/DesktopCapturerSourcePicker";
import PlatformPeg from "../../../../../src/PlatformPeg";
import BasePlatform from "../../../../../src/BasePlatform";
const SOURCES = [
{
id: "screen1",
name: "Screen 1",
thumbnailURL: "data:image/png;base64,",
},
{
id: "window1",
name: "Window 1",
thumbnailURL: "data:image/png;base64,",
},
];
describe("DesktopCapturerSourcePicker", () => {
beforeEach(() => {
const plaf = {
getDesktopCapturerSources: jest.fn().mockResolvedValue(SOURCES),
supportsSetting: jest.fn().mockReturnValue(false),
};
jest.spyOn(PlatformPeg, "get").mockReturnValue(plaf as unknown as BasePlatform);
});
afterEach(() => {
jest.restoreAllMocks();
});
it("should render the component", () => {
render(<DesktopCapturerSourcePicker onFinished={() => {}} />);
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Share" })).toBeInTheDocument();
});
it("should disable share button until a source is selected", () => {
render(<DesktopCapturerSourcePicker onFinished={() => {}} />);
expect(screen.getByRole("button", { name: "Share" })).toBeDisabled();
});
it("should contain a screen source in the default tab", async () => {
render(<DesktopCapturerSourcePicker onFinished={() => {}} />);
const screen1Button = await screen.findByRole("button", { name: "Screen 1" });
expect(screen1Button).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Window 1" })).not.toBeInTheDocument();
});
it("should contain a window source in the window tab", async () => {
render(<DesktopCapturerSourcePicker onFinished={() => {}} />);
await userEvent.click(screen.getByRole("tab", { name: "Application window" }));
const window1Button = await screen.findByRole("button", { name: "Window 1" });
expect(window1Button).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Screen 1" })).not.toBeInTheDocument();
});
it("should call onFinished with no arguments if cancelled", async () => {
const onFinished = jest.fn();
render(<DesktopCapturerSourcePicker onFinished={onFinished} />);
await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(onFinished).toHaveBeenCalledWith();
});
it("should call onFinished with the selected source when share clicked", async () => {
const onFinished = jest.fn();
render(<DesktopCapturerSourcePicker onFinished={onFinished} />);
const screen1Button = await screen.findByRole("button", { name: "Screen 1" });
await userEvent.click(screen1Button);
await userEvent.click(screen.getByRole("button", { name: "Share" }));
expect(onFinished).toHaveBeenCalledWith(SOURCES[0]);
});
});

View file

@ -0,0 +1,51 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*
*/
import React from "react";
import { render, waitFor } from "jest-matrix-react";
import dis from "../../../../../src/dispatcher/dispatcher";
import EffectsOverlay from "../../../../../src/components/views/elements/EffectsOverlay.tsx";
describe("<EffectsOverlay/>", () => {
let isStarted: boolean;
beforeEach(() => {
isStarted = false;
jest.mock("../../../../../src/effects/confetti/index.ts", () => {
return class Confetti {
start = () => {
isStarted = true;
};
stop = jest.fn();
};
});
});
afterEach(() => jest.useRealTimers());
it("should render", () => {
const { asFragment } = render(<EffectsOverlay roomWidth={100} />);
expect(asFragment()).toMatchSnapshot();
});
it("should start the confetti effect", async () => {
render(<EffectsOverlay roomWidth={100} />);
dis.dispatch({ action: "effects.confetti" });
await waitFor(() => expect(isStarted).toBe(true));
});
it("should start the confetti effect when the event is not outdated", async () => {
const eventDate = new Date("2024-09-01");
const date = new Date("2024-09-02");
jest.useFakeTimers().setSystemTime(date);
render(<EffectsOverlay roomWidth={100} />);
dis.dispatch({ action: "effects.confetti", event: { getTs: () => eventDate.getTime() } });
await waitFor(() => expect(isStarted).toBe(true));
});
});

View file

@ -0,0 +1,683 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { ComponentProps } from "react";
import { render, RenderResult } from "jest-matrix-react";
import { MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix";
import { KnownMembership, Membership } from "matrix-js-sdk/src/types";
import {
getMockClientWithEventEmitter,
mkEvent,
mkMembership,
mockClientMethodsUser,
unmockClientPeg,
} from "../../../../test-utils";
import EventListSummary from "../../../../../src/components/views/elements/EventListSummary";
import { Layout } from "../../../../../src/settings/enums/Layout";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import * as languageHandler from "../../../../../src/languageHandler";
describe("EventListSummary", function () {
const roomId = "!room:server.org";
// Generate dummy event tiles for use in simulating an expanded MELS
const generateTiles = (events: MatrixEvent[]) => {
return events.map((e) => {
return (
<div key={e.getId()} className="event_tile">
Expanded membership
</div>
);
});
};
/**
* Generates a membership event with the target of the event set as a mocked
* RoomMember based on `parameters.userId`.
* @param {string} eventId the ID of the event.
* @param {object} parameters the parameters to use to create the event.
* @param {string} parameters.membership the membership to assign to
* `content.membership`
* @param {string} parameters.userId the state key and target userId of the event. If
* `parameters.senderId` is not specified, this is also used as the event sender.
* @param {string} parameters.prevMembership the membership to assign to
* `prev_content.membership`.
* @param {string} parameters.senderId the user ID of the sender of the event.
* Optional. Defaults to `parameters.userId`.
* @returns {MatrixEvent} the event created.
*/
interface MembershipEventParams {
senderId?: string;
userId?: string;
membership: Membership;
prevMembership?: Membership;
}
const generateMembershipEvent = (
eventId: string,
{ senderId, userId, membership, prevMembership }: MembershipEventParams & { userId: string },
): MatrixEvent => {
const member = new RoomMember(roomId, userId);
// Use localpart as display name;
member.name = userId.match(/@([^:]*):/)![1];
jest.spyOn(member, "getAvatarUrl").mockReturnValue("avatar.jpeg");
jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("mxc://avatar.url/image.png");
const e = mkMembership({
event: true,
room: roomId,
user: senderId || userId,
skey: userId,
mship: membership,
prevMship: prevMembership,
target: member,
});
// Override random event ID to allow for equality tests against tiles from
// generateTiles
e.event.event_id = eventId;
return e;
};
// Generate mock MatrixEvents from the array of parameters
const generateEvents = (parameters: Array<MembershipEventParams & { userId: string }>) => {
const res: MatrixEvent[] = [];
for (let i = 0; i < parameters.length; i++) {
res.push(generateMembershipEvent(`event${i}`, parameters[i]));
}
return res;
};
// Generate the same sequence of `events` for `n` users, where each user ID
// is created by replacing the first "$" in userIdTemplate with `i` for
// `i = 0 .. n`.
const generateEventsForUsers = (userIdTemplate: string, n: number, events: MembershipEventParams[]) => {
let eventsForUsers: MatrixEvent[] = [];
let userId = "";
for (let i = 0; i < n; i++) {
userId = userIdTemplate.replace("$", String(i));
events.forEach((e) => {
e.userId = userId;
});
eventsForUsers = eventsForUsers.concat(
generateEvents(events as Array<MembershipEventParams & { userId: string }>),
);
}
return eventsForUsers;
};
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
});
const defaultProps: Omit<
ComponentProps<typeof EventListSummary>,
"summaryLength" | "threshold" | "avatarsMaxLength"
> = {
layout: Layout.Bubble,
events: [],
children: [],
};
const renderComponent = (props = {}): RenderResult => {
return render(
<MatrixClientContext.Provider value={mockClient}>
<EventListSummary {...defaultProps} {...props} />
</MatrixClientContext.Provider>,
);
};
beforeEach(function () {
jest.clearAllMocks();
jest.spyOn(languageHandler, "getUserLanguage").mockReturnValue("en-GB");
});
afterAll(() => {
unmockClientPeg();
});
it("renders expanded events if there are less than props.threshold", function () {
const events = generateEvents([
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
]);
const props = {
events: events,
children: generateTiles(events),
summaryLength: 1,
avatarsMaxLength: 5,
threshold: 3,
};
const { container } = renderComponent(props); // matrix cli context wrapper
const children = container.querySelector(".mx_GenericEventListSummary_unstyledList")!.children;
expect(children).toHaveLength(1);
expect(children[0]).toHaveTextContent("Expanded membership");
});
it("renders expanded events if there are less than props.threshold for join and leave", function () {
const events = generateEvents([
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
]);
const props = {
events: events,
children: generateTiles(events),
summaryLength: 1,
avatarsMaxLength: 5,
threshold: 3,
};
const { container } = renderComponent(props); // matrix cli context wrapper
const children = container.querySelector(".mx_GenericEventListSummary_unstyledList")!.children;
expect(children).toHaveLength(2);
expect(children[0]).toHaveTextContent("Expanded membership");
expect(children[1]).toHaveTextContent("Expanded membership");
});
it("renders collapsed events if events.length = props.threshold", function () {
const events = generateEvents([
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
]);
const props = {
events: events,
children: generateTiles(events),
summaryLength: 1,
avatarsMaxLength: 5,
threshold: 3,
};
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent("user_1 joined and left and joined");
});
it("truncates long join,leave repetitions", function () {
const events = generateEvents([
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
]);
const props = {
events: events,
children: generateTiles(events),
summaryLength: 1,
avatarsMaxLength: 5,
threshold: 3,
};
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent("user_1 joined and left 7 times");
});
it("truncates long join,leave repetitions between other events", function () {
const events = generateEvents([
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Ban,
membership: KnownMembership.Leave,
senderId: "@some_other_user:some.domain",
},
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Leave,
membership: KnownMembership.Invite,
senderId: "@some_other_user:some.domain",
},
]);
const props = {
events: events,
children: generateTiles(events),
summaryLength: 1,
avatarsMaxLength: 5,
threshold: 3,
};
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent("user_1 was unbanned, joined and left 7 times and was invited");
});
it("truncates multiple sequences of repetitions with other events between", function () {
const events = generateEvents([
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Ban,
membership: KnownMembership.Leave,
senderId: "@some_other_user:some.domain",
},
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Leave,
membership: KnownMembership.Ban,
senderId: "@some_other_user:some.domain",
},
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Ban, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Leave,
membership: KnownMembership.Invite,
senderId: "@some_other_user:some.domain",
},
]);
const props = {
events: events,
children: generateTiles(events),
summaryLength: 1,
avatarsMaxLength: 5,
threshold: 3,
};
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent(
"user_1 was unbanned, joined and left 2 times, was banned, " + "joined and left 3 times and was invited",
);
});
it("handles multiple users following the same sequence of memberships", function () {
const events = generateEvents([
// user_1
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Ban,
membership: KnownMembership.Leave,
senderId: "@some_other_user:some.domain",
},
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Leave,
membership: KnownMembership.Ban,
senderId: "@some_other_user:some.domain",
},
// user_2
{
userId: "@user_2:some.domain",
prevMembership: KnownMembership.Ban,
membership: KnownMembership.Leave,
senderId: "@some_other_user:some.domain",
},
{ userId: "@user_2:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_2:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_2:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_2:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{
userId: "@user_2:some.domain",
prevMembership: KnownMembership.Leave,
membership: KnownMembership.Ban,
senderId: "@some_other_user:some.domain",
},
]);
const props = {
events: events,
children: generateTiles(events),
summaryLength: 1,
avatarsMaxLength: 5,
threshold: 3,
};
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent(
"user_1 and one other were unbanned, joined and left 2 times and were banned",
);
});
it("handles many users following the same sequence of memberships", function () {
const events = generateEventsForUsers("@user_$:some.domain", 20, [
{
prevMembership: KnownMembership.Ban,
membership: KnownMembership.Leave,
senderId: "@some_other_user:some.domain",
},
{ prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{
prevMembership: KnownMembership.Leave,
membership: KnownMembership.Ban,
senderId: "@some_other_user:some.domain",
},
]);
const props = {
events: events,
children: generateTiles(events),
summaryLength: 1,
avatarsMaxLength: 5,
threshold: 3,
};
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent(
"user_0 and 19 others were unbanned, joined and left 2 times and were banned",
);
});
it("correctly orders sequences of transitions by the order of their first event", function () {
const events = generateEvents([
{
userId: "@user_2:some.domain",
prevMembership: KnownMembership.Ban,
membership: KnownMembership.Leave,
senderId: "@some_other_user:some.domain",
},
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Ban,
membership: KnownMembership.Leave,
senderId: "@some_other_user:some.domain",
},
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Leave,
membership: KnownMembership.Ban,
senderId: "@some_other_user:some.domain",
},
{ userId: "@user_2:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_2:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
{ userId: "@user_2:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
{ userId: "@user_2:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
]);
const props = {
events: events,
children: generateTiles(events),
summaryLength: 1,
avatarsMaxLength: 5,
threshold: 3,
};
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent(
"user_2 was unbanned and joined and left 2 times, user_1 was unbanned, " +
"joined and left 2 times and was banned",
);
});
it("correctly identifies transitions", function () {
const events = generateEvents([
// invited
{ userId: "@user_1:some.domain", membership: KnownMembership.Invite },
// banned
{ userId: "@user_1:some.domain", membership: KnownMembership.Ban },
// joined
{ userId: "@user_1:some.domain", membership: KnownMembership.Join },
// invite_reject
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Invite,
membership: KnownMembership.Leave,
},
// left
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
// invite_withdrawal
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Invite,
membership: KnownMembership.Leave,
senderId: "@some_other_user:some.domain",
},
// unbanned
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Ban,
membership: KnownMembership.Leave,
senderId: "@some_other_user:some.domain",
},
// kicked
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Join,
membership: KnownMembership.Leave,
senderId: "@some_other_user:some.domain",
},
// default for sender=target (leave)
{
userId: "@user_1:some.domain",
prevMembership: "????" as Membership,
membership: KnownMembership.Leave,
senderId: "@user_1:some.domain",
},
// default for sender<>target (kicked)
{
userId: "@user_1:some.domain",
prevMembership: "????" as Membership,
membership: KnownMembership.Leave,
senderId: "@some_other_user:some.domain",
},
]);
const props = {
events: events,
children: generateTiles(events),
summaryLength: 1,
avatarsMaxLength: 5,
threshold: 3,
};
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent(
"user_1 was invited, was banned, joined, rejected their invitation, left, " +
"had their invitation withdrawn, was unbanned, was removed, left and was removed",
);
});
it("handles invitation plurals correctly when there are multiple users", function () {
const events = generateEvents([
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Invite,
membership: KnownMembership.Leave,
},
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Invite,
membership: KnownMembership.Leave,
senderId: "@some_other_user:some.domain",
},
{
userId: "@user_2:some.domain",
prevMembership: KnownMembership.Invite,
membership: KnownMembership.Leave,
},
{
userId: "@user_2:some.domain",
prevMembership: KnownMembership.Invite,
membership: KnownMembership.Leave,
senderId: "@some_other_user:some.domain",
},
]);
const props = {
events: events,
children: generateTiles(events),
summaryLength: 1,
avatarsMaxLength: 5,
threshold: 3,
};
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent(
"user_1 and one other rejected their invitations and had their invitations withdrawn",
);
});
it("handles invitation plurals correctly when there are multiple invites", function () {
const events = generateEvents([
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Invite,
membership: KnownMembership.Leave,
},
{
userId: "@user_1:some.domain",
prevMembership: KnownMembership.Invite,
membership: KnownMembership.Leave,
},
]);
const props = {
events: events,
children: generateTiles(events),
summaryLength: 1,
avatarsMaxLength: 5,
threshold: 1, // threshold = 1 to force collapse
};
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent("user_1 rejected their invitation 2 times");
});
it('handles a summary length = 2, with no "others"', function () {
const events = generateEvents([
{ userId: "@user_1:some.domain", membership: KnownMembership.Join },
{ userId: "@user_1:some.domain", membership: KnownMembership.Join },
{ userId: "@user_2:some.domain", membership: KnownMembership.Join },
{ userId: "@user_2:some.domain", membership: KnownMembership.Join },
]);
const props = {
events: events,
children: generateTiles(events),
summaryLength: 2,
avatarsMaxLength: 5,
threshold: 3,
};
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent("user_1 and user_2 joined 2 times");
});
it('handles a summary length = 2, with 1 "other"', function () {
const events = generateEvents([
{ userId: "@user_1:some.domain", membership: KnownMembership.Join },
{ userId: "@user_2:some.domain", membership: KnownMembership.Join },
{ userId: "@user_3:some.domain", membership: KnownMembership.Join },
]);
const props = {
events: events,
children: generateTiles(events),
summaryLength: 2,
avatarsMaxLength: 5,
threshold: 3,
};
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent("user_1, user_2 and one other joined");
});
it('handles a summary length = 2, with many "others"', function () {
const events = generateEventsForUsers("@user_$:some.domain", 20, [{ membership: KnownMembership.Join }]);
const props = {
events: events,
children: generateTiles(events),
summaryLength: 2,
avatarsMaxLength: 5,
threshold: 3,
};
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent("user_0, user_1 and 18 others joined");
});
it("should not blindly group 3pid invites and treat them as distinct users instead", () => {
const events = [
mkEvent({
event: true,
skey: "randomstring1",
user: "@user1:server",
type: "m.room.third_party_invite",
content: {
display_name: "n...@d...",
key_validity_url: "https://blah",
public_key: "public_key",
},
}),
mkEvent({
event: true,
skey: "randomstring2",
user: "@user1:server",
type: "m.room.third_party_invite",
content: {
display_name: "n...@d...",
key_validity_url: "https://blah",
public_key: "public_key",
},
}),
mkEvent({
event: true,
skey: "randomstring3",
user: "@user1:server",
type: "m.room.third_party_invite",
content: {
display_name: "d...@w...",
key_validity_url: "https://blah",
public_key: "public_key",
},
}),
];
const props = {
events: events,
children: generateTiles(events),
summaryLength: 2,
avatarsMaxLength: 5,
threshold: 3,
};
const { container } = renderComponent(props);
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
expect(summary).toHaveTextContent("n...@d... was invited 2 times, d...@w... was invited");
});
});

View file

@ -0,0 +1,46 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { render } from "jest-matrix-react";
import React from "react";
import ExternalLink from "../../../../../src/components/views/elements/ExternalLink";
describe("<ExternalLink />", () => {
const defaultProps = {
"href": "test.com",
"onClick": jest.fn(),
"className": "myCustomClass",
"data-testid": "test",
};
const getComponent = (props = {}) => {
return render(<ExternalLink {...defaultProps} {...props} />);
};
it("renders link correctly", () => {
const children = (
<span>
react element <b>children</b>
</span>
);
expect(getComponent({ children, target: "_self", rel: "noopener" }).asFragment()).toMatchSnapshot();
});
it("defaults target and rel", () => {
const children = "test";
const { getByTestId } = getComponent({ children });
const container = getByTestId("test");
expect(container.getAttribute("rel")).toEqual("noreferrer noopener");
expect(container.getAttribute("target")).toEqual("_blank");
});
it("renders plain text link correctly", () => {
const children = "test";
expect(getComponent({ children }).asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,26 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { render } from "jest-matrix-react";
import React from "react";
import { KnownMembership } from "matrix-js-sdk/src/types";
import FacePile from "../../../../../src/components/views/elements/FacePile";
import { mkRoomMember } from "../../../../test-utils";
describe("<FacePile />", () => {
it("renders with a tooltip", () => {
const member = mkRoomMember("123", "456", KnownMembership.Join);
const { asFragment } = render(
<FacePile members={[member]} size="36px" overflow={false} tooltipLabel="tooltip" />,
);
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,109 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, screen } from "jest-matrix-react";
import Field from "../../../../../src/components/views/elements/Field";
describe("Field", () => {
describe("Placeholder", () => {
it("Should display a placeholder", async () => {
// When
const { rerender } = render(<Field value="" placeholder="my placeholder" />);
// Then
expect(screen.getByRole("textbox")).toHaveAttribute("placeholder", "my placeholder");
// When
rerender(<Field value="" placeholder="" />);
// Then
expect(screen.getByRole("textbox")).toHaveAttribute("placeholder", "");
});
it("Should display label as placeholder", async () => {
// When
render(<Field value="" label="my label" />);
// Then
expect(screen.getByRole("textbox")).toHaveAttribute("placeholder", "my label");
});
it("Should not display a placeholder", async () => {
// When
render(<Field value="" />);
// Then
expect(screen.getByRole("textbox")).not.toHaveAttribute("placeholder", "my placeholder");
});
});
describe("Feedback", () => {
it("Should mark the feedback as alert if invalid", async () => {
render(
<Field
value=""
validateOnFocus
onValidate={() => Promise.resolve({ valid: false, feedback: "Invalid" })}
/>,
);
// When invalid
fireEvent.focus(screen.getByRole("textbox"));
// Expect 'alert' role
await expect(screen.findByRole("alert")).resolves.toBeInTheDocument();
// Close the feedback is Escape is pressed
fireEvent.keyDown(screen.getByRole("textbox"), { key: "Escape" });
expect(screen.queryByRole("alert")).toBeNull();
});
it("Should mark the feedback as status if valid", async () => {
render(
<Field
value=""
validateOnFocus
onValidate={() => Promise.resolve({ valid: true, feedback: "Valid" })}
/>,
);
// When valid
fireEvent.focus(screen.getByRole("textbox"));
// Expect 'status' role
await expect(screen.findByRole("status")).resolves.toBeInTheDocument();
// Close the feedback is Escape is pressed
fireEvent.keyDown(screen.getByRole("textbox"), { key: "Escape" });
expect(screen.queryByRole("status")).toBeNull();
});
it("Should mark the feedback as tooltip if custom tooltip set", async () => {
render(
<Field
value=""
validateOnFocus
onValidate={() => Promise.resolve({ valid: true, feedback: "Valid" })}
tooltipContent="Tooltip"
/>,
);
// When valid or invalid and 'tooltipContent' set
fireEvent.focus(screen.getByRole("textbox"));
// Expect 'tooltip' role
await expect(screen.findByRole("tooltip")).resolves.toBeInTheDocument();
// Close the feedback is Escape is pressed
fireEvent.keyDown(screen.getByRole("textbox"), { key: "Escape" });
expect(screen.queryByRole("tooltip")).toBeNull();
});
});
});

View file

@ -0,0 +1,60 @@
/*
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 { act, fireEvent, render } from "jest-matrix-react";
import React from "react";
import { FilterDropdown } from "../../../../../src/components/views/elements/FilterDropdown";
import { flushPromises, mockPlatformPeg } from "../../../../test-utils";
mockPlatformPeg();
describe("<FilterDropdown />", () => {
const options = [
{ id: "one", label: "Option one" },
{ id: "two", label: "Option two", description: "with description" },
];
const defaultProps = {
className: "test",
value: "one",
options,
id: "test",
label: "test label",
onOptionChange: jest.fn(),
};
const getComponent = (props = {}): JSX.Element => <FilterDropdown {...defaultProps} {...props} />;
const openDropdown = async (container: HTMLElement): Promise<void> =>
await act(async () => {
const button = container.querySelector('[role="button"]');
expect(button).toBeTruthy();
fireEvent.click(button as Element);
await flushPromises();
});
it("renders selected option", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it("renders when selected option is not in options", () => {
const { container } = render(getComponent({ value: "oops" }));
expect(container).toMatchSnapshot();
});
it("renders selected option with selectedLabel", () => {
const { container } = render(getComponent({ selectedLabel: "Show" }));
expect(container).toMatchSnapshot();
});
it("renders dropdown options in menu", async () => {
const { container } = render(getComponent());
await openDropdown(container);
expect(container.querySelector(".mx_Dropdown_menu")).toMatchSnapshot();
});
});

View file

@ -0,0 +1,46 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render } from "jest-matrix-react";
import { FilterTabGroup } from "../../../../../src/components/views/elements/FilterTabGroup";
describe("<FilterTabGroup />", () => {
enum TestOption {
Apple = "Apple",
Banana = "Banana",
Orange = "Orange",
}
const defaultProps = {
"name": "test",
"value": TestOption.Apple,
"onFilterChange": jest.fn(),
"tabs": [
{ id: TestOption.Apple, label: `Label for ${TestOption.Apple}` },
{ id: TestOption.Banana, label: `Label for ${TestOption.Banana}` },
{ id: TestOption.Orange, label: `Label for ${TestOption.Orange}` },
],
"data-testid": "test",
};
const getComponent = (props = {}) => <FilterTabGroup<TestOption> {...defaultProps} {...props} />;
it("renders options", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it("calls onChange handler on selection", () => {
const onFilterChange = jest.fn();
const { getByText } = render(getComponent({ onFilterChange }));
fireEvent.click(getByText("Label for Banana"));
expect(onFilterChange).toHaveBeenCalledWith(TestOption.Banana);
});
});

View file

@ -0,0 +1,19 @@
/*
* Copyright 2024 New Vector Ltd.
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import ImageView from "../../../../../src/components/views/elements/ImageView";
describe("<ImageView />", () => {
it("renders correctly", () => {
const { container } = render(<ImageView src="https://example.com/image.png" onFinished={jest.fn()} />);
expect(container).toMatchSnapshot();
});
});

View file

@ -0,0 +1,33 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import userEvent from "@testing-library/user-event";
import { render, waitFor } from "jest-matrix-react";
import InfoTooltip from "../../../../../src/components/views/elements/InfoTooltip";
describe("InfoTooltip", () => {
it("should show tooltip on hover", async () => {
const { getByText, asFragment } = render(<InfoTooltip tooltip="Tooltip text">Trigger text</InfoTooltip>);
const trigger = getByText("Trigger text");
expect(trigger).toBeVisible();
await userEvent.hover(trigger!);
// wait for the tooltip to open
const tooltip = await waitFor(() => {
const tooltip = document.getElementById(trigger.getAttribute("aria-describedby")!);
expect(tooltip).toBeVisible();
return tooltip;
});
expect(tooltip).toHaveTextContent("Tooltip text");
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,96 @@
/*
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 { fireEvent, render, screen } from "jest-matrix-react";
import React from "react";
import LabelledCheckbox from "../../../../../src/components/views/elements/LabelledCheckbox";
describe("<LabelledCheckbox />", () => {
type CompProps = React.ComponentProps<typeof LabelledCheckbox>;
const getComponent = (props: CompProps) => <LabelledCheckbox {...props} />;
const getCheckbox = (): HTMLInputElement => screen.getByRole("checkbox");
it.each([undefined, "this is a byline"])("should render with byline of %p", (byline) => {
const props: CompProps = {
label: "Hello world",
value: true,
byline: byline,
onChange: jest.fn(),
};
const renderResult = render(getComponent(props));
expect(renderResult.asFragment()).toMatchSnapshot();
});
it("should support unchecked by default", () => {
const props: CompProps = {
label: "Hello world",
value: false,
onChange: jest.fn(),
};
render(getComponent(props));
expect(getCheckbox()).not.toBeChecked();
});
it("should be possible to disable the checkbox", () => {
const props: CompProps = {
label: "Hello world",
value: false,
disabled: true,
onChange: jest.fn(),
};
render(getComponent(props));
expect(getCheckbox()).toBeDisabled();
});
it("should emit onChange calls", () => {
const props: CompProps = {
label: "Hello world",
value: false,
onChange: jest.fn(),
};
render(getComponent(props));
expect(props.onChange).not.toHaveBeenCalled();
fireEvent.click(getCheckbox());
expect(props.onChange).toHaveBeenCalledWith(true);
});
it("should react to value and disabled prop changes", () => {
const props: CompProps = {
label: "Hello world",
value: false,
onChange: jest.fn(),
};
const { rerender } = render(getComponent(props));
let checkbox = getCheckbox();
expect(checkbox).not.toBeChecked();
expect(checkbox).not.toBeDisabled();
props.disabled = true;
props.value = true;
rerender(getComponent(props));
checkbox = getCheckbox();
expect(checkbox).toBeChecked();
expect(checkbox).toBeDisabled();
});
it("should render with a custom class name", () => {
const className = "some class name";
const props: CompProps = {
label: "Hello world",
value: false,
onChange: jest.fn(),
className,
};
const { container } = render(getComponent(props));
expect(container.firstElementChild?.className).toContain(className);
});
});

View file

@ -0,0 +1,49 @@
/*
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 } from "jest-matrix-react";
import LearnMore from "../../../../../src/components/views/elements/LearnMore";
import Modal from "../../../../../src/Modal";
import InfoDialog from "../../../../../src/components/views/dialogs/InfoDialog";
describe("<LearnMore />", () => {
const defaultProps = {
title: "Test",
description: "test test test",
["data-testid"]: "testid",
};
const getComponent = (props = {}) => <LearnMore {...defaultProps} {...props} />;
const modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({
finished: new Promise(() => {}),
close: jest.fn(),
});
beforeEach(() => {
jest.clearAllMocks();
});
it("renders button", () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it("opens modal on click", async () => {
const { getByTestId } = render(getComponent());
fireEvent.click(getByTestId("testid"));
expect(modalSpy).toHaveBeenCalledWith(InfoDialog, {
button: "Got it",
description: defaultProps.description,
hasCloseButton: true,
title: defaultProps.title,
});
});
});

View file

@ -0,0 +1,282 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { act, render, RenderResult, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { mocked, Mocked } from "jest-mock";
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import dis from "../../../../../src/dispatcher/dispatcher";
import { Pill, PillProps, PillType } from "../../../../../src/components/views/elements/Pill";
import {
filterConsole,
flushPromises,
mkMessage,
mkRoomCanonicalAliasEvent,
mkRoomMemberJoinEvent,
stubClient,
} from "../../../../test-utils";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { Action } from "../../../../../src/dispatcher/actions";
import { ButtonEvent } from "../../../../../src/components/views/elements/AccessibleButton";
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
describe("<Pill>", () => {
let client: Mocked<MatrixClient>;
const permalinkPrefix = "https://matrix.to/#/";
const room1Alias = "#room1:example.com";
const room1Id = "!room1:example.com";
let room1: Room;
let room1Message: MatrixEvent;
const room2Id = "!room2:example.com";
let room2: Room;
const space1Id = "!space1:example.com";
let space1: Room;
const user1Id = "@user1:example.com";
const user2Id = "@user2:example.com";
const user3Id = "@user3:example.com";
let renderResult: RenderResult;
let pillParentClickHandler: (e: ButtonEvent) => void;
const renderPill = (props: PillProps): void => {
const withDefault = {
inMessage: true,
shouldShowPillAvatar: true,
...props,
} as PillProps;
// wrap Pill with a div to allow testing of event bubbling
renderResult = render(
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<div onClick={pillParentClickHandler}>
<Pill {...withDefault} />
</div>,
);
};
filterConsole(
"Failed to parse permalink Error: Unknown entity type in permalink",
"Room !room1:example.com does not have an m.room.create event",
"Room !space1:example.com does not have an m.room.create event",
);
beforeEach(() => {
client = mocked(stubClient());
SdkContextClass.instance.client = client;
DMRoomMap.makeShared(client);
room1 = new Room(room1Id, client, user1Id);
room1.name = "Room 1";
const user1JoinRoom1Event = mkRoomMemberJoinEvent(user1Id, room1Id, {
displayname: "User 1",
});
room1.currentState.setStateEvents([
mkRoomCanonicalAliasEvent(user1Id, room1Id, room1Alias),
user1JoinRoom1Event,
]);
room1.getMember(user1Id)!.setMembershipEvent(user1JoinRoom1Event);
room1Message = mkMessage({
id: "$123-456",
event: true,
user: user1Id,
room: room1Id,
msg: "Room 1 Message",
});
room1.addLiveEvents([room1Message]);
room2 = new Room(room2Id, client, user1Id);
room2.currentState.setStateEvents([mkRoomMemberJoinEvent(user2Id, room2Id)]);
room2.name = "Room 2";
space1 = new Room(space1Id, client, client.getSafeUserId());
space1.name = "Space 1";
client.getRooms.mockReturnValue([room1, room2, space1]);
client.getRoom.mockImplementation((roomId: string) => {
if (roomId === room1.roomId) return room1;
if (roomId === room2.roomId) return room2;
if (roomId === space1.roomId) return space1;
return null;
});
client.getProfileInfo.mockImplementation(async (userId: string) => {
if (userId === user2Id) return { displayname: "User 2" };
throw new Error(`Unknown user ${userId}`);
});
jest.spyOn(dis, "dispatch");
pillParentClickHandler = jest.fn();
jest.spyOn(global.Math, "random").mockReturnValue(0.123456);
});
afterEach(() => {
jest.spyOn(global.Math, "random").mockRestore();
});
describe("when rendering a pill for a room", () => {
beforeEach(() => {
renderPill({
url: permalinkPrefix + room1Id,
});
});
it("should render the expected pill", () => {
expect(renderResult.asFragment()).toMatchSnapshot();
});
describe("when hovering the pill", () => {
beforeEach(async () => {
await userEvent.hover(screen.getByText("Room 1"));
});
it("should show a tooltip with the room Id", async () => {
expect(await screen.findByRole("tooltip", { name: room1Id })).toBeInTheDocument();
});
describe("when not hovering the pill any more", () => {
beforeEach(async () => {
await userEvent.unhover(screen.getByText("Room 1"));
});
it("should dimiss a tooltip with the room Id", () => {
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
});
});
});
});
it("should not render a non-permalink", () => {
renderPill({
url: "https://example.com/hello",
});
expect(renderResult.asFragment()).toMatchSnapshot();
});
it("should render the expected pill for a space", () => {
renderPill({
url: permalinkPrefix + space1Id,
});
expect(renderResult.asFragment()).toMatchSnapshot();
});
it("should render the expected pill for a room alias", () => {
renderPill({
url: permalinkPrefix + room1Alias,
});
expect(renderResult.asFragment()).toMatchSnapshot();
});
it("should render the expected pill for @room", () => {
renderPill({
room: room1,
type: PillType.AtRoomMention,
});
expect(renderResult.asFragment()).toMatchSnapshot();
});
describe("when rendering a pill for a user in the room", () => {
beforeEach(() => {
renderPill({
room: room1,
url: permalinkPrefix + user1Id,
});
});
it("should render as expected", () => {
expect(renderResult.asFragment()).toMatchSnapshot();
});
describe("when clicking the pill", () => {
beforeEach(async () => {
await userEvent.click(screen.getByText("User 1"));
});
it("should dipsatch a view user action and prevent event bubbling", () => {
expect(dis.dispatch).toHaveBeenCalledWith({
action: Action.ViewUser,
member: room1.getMember(user1Id),
});
expect(pillParentClickHandler).not.toHaveBeenCalled();
});
});
});
it("should render the expected pill for a known user not in the room", async () => {
renderPill({
room: room1,
url: permalinkPrefix + user2Id,
});
// wait for profile query via API
await act(async () => {
await flushPromises();
});
expect(renderResult.asFragment()).toMatchSnapshot();
});
it("should render the expected pill for an uknown user not in the room", async () => {
renderPill({
room: room1,
url: permalinkPrefix + user3Id,
});
// wait for profile query via API
await act(async () => {
await flushPromises();
});
expect(renderResult.asFragment()).toMatchSnapshot();
});
it("should not render anything if the type cannot be detected", () => {
renderPill({
url: permalinkPrefix,
});
expect(renderResult.asFragment()).toMatchInlineSnapshot(`
<DocumentFragment>
<div />
</DocumentFragment>
`);
});
it("should not render an avatar or link when called with inMessage = false and shouldShowPillAvatar = false", () => {
renderPill({
inMessage: false,
shouldShowPillAvatar: false,
url: permalinkPrefix + room1Id,
});
expect(renderResult.asFragment()).toMatchSnapshot();
});
it("should render the expected pill for a message in the same room", () => {
renderPill({
room: room1,
url: `${permalinkPrefix}${room1Id}/${room1Message.getId()}`,
});
expect(renderResult.asFragment()).toMatchSnapshot();
});
it("should render the expected pill for a message in another room", () => {
renderPill({
room: room2,
url: `${permalinkPrefix}${room1Id}/${room1Message.getId()}`,
});
expect(renderResult.asFragment()).toMatchSnapshot();
});
it("should not render a pill with an unknown type", () => {
// @ts-ignore
renderPill({ type: "unknown" });
expect(renderResult.asFragment()).toMatchInlineSnapshot(`
<DocumentFragment>
<div />
</DocumentFragment>
`);
});
});

View file

@ -0,0 +1,295 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, RenderResult } from "jest-matrix-react";
import {
Room,
MatrixEvent,
M_POLL_KIND_DISCLOSED,
M_POLL_KIND_UNDISCLOSED,
M_POLL_START,
M_TEXT,
} from "matrix-js-sdk/src/matrix";
import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
import { ReplacementEvent } from "matrix-js-sdk/src/types";
import { getMockClientWithEventEmitter } from "../../../../test-utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import PollCreateDialog from "../../../../../src/components/views/elements/PollCreateDialog";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
// Fake date to give a predictable snapshot
const realDateNow = Date.now;
const realDateToISOString = Date.prototype.toISOString;
Date.now = jest.fn(() => 2345678901234);
// eslint-disable-next-line no-extend-native
Date.prototype.toISOString = jest.fn(() => "2021-11-23T14:35:14.240Z");
afterAll(() => {
Date.now = realDateNow;
// eslint-disable-next-line no-extend-native
Date.prototype.toISOString = realDateToISOString;
});
describe("PollCreateDialog", () => {
const mockClient = getMockClientWithEventEmitter({
sendEvent: jest.fn().mockResolvedValue({ event_id: "1" }),
});
beforeEach(() => {
mockClient.sendEvent.mockClear();
});
it("renders a blank poll", () => {
const dialog = render(
<MatrixClientContext.Provider value={mockClient}>
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />
</MatrixClientContext.Provider>,
);
expect(dialog.asFragment()).toMatchSnapshot();
});
it("autofocuses the poll topic on mount", () => {
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expect(dialog.container.querySelector("#poll-topic-input")).toHaveFocus();
});
it("autofocuses the new poll option field after clicking add option button", () => {
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expect(dialog.container.querySelector("#poll-topic-input")).toHaveFocus();
fireEvent.click(dialog.container.querySelector("div.mx_PollCreateDialog_addOption")!);
expect(dialog.container.querySelector("#poll-topic-input")).not.toHaveFocus();
expect(dialog.container.querySelector("#pollcreate_option_1")).not.toHaveFocus();
expect(dialog.container.querySelector("#pollcreate_option_2")).toHaveFocus();
});
it("renders a question and some options", () => {
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expectSubmitToBeDisabled(dialog, true);
// When I set some values in the boxes
changeValue(dialog, "Question or topic", "How many turnips is the optimal number?");
changeValue(dialog, "Option 1", "As many as my neighbour");
changeValue(dialog, "Option 2", "The question is meaningless");
fireEvent.click(dialog.container.querySelector("div.mx_PollCreateDialog_addOption")!);
changeValue(dialog, "Option 3", "Mu");
expect(dialog.asFragment()).toMatchSnapshot();
});
it("renders info from a previous event", () => {
const previousEvent: MatrixEvent = new MatrixEvent(
PollStartEvent.from("Poll Q", ["Answer 1", "Answer 2"], M_POLL_KIND_DISCLOSED).serialize(),
);
const dialog = render(
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} editingMxEvent={previousEvent} />,
);
expectSubmitToBeDisabled(dialog, false);
expect(dialog.asFragment()).toMatchSnapshot();
});
it("doesn't allow submitting until there are options", () => {
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expectSubmitToBeDisabled(dialog, true);
});
it("does allow submitting when there are options and a question", () => {
// Given a dialog with no info in (which I am unable to submit)
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expectSubmitToBeDisabled(dialog, true);
// When I set some values in the boxes
changeValue(dialog, "Question or topic", "Q");
changeValue(dialog, "Option 1", "A1");
changeValue(dialog, "Option 2", "A2");
// Then I am able to submit
expectSubmitToBeDisabled(dialog, false);
});
it("shows the open poll description at first", () => {
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
expect(dialog.container.querySelector("select")).toHaveValue(M_POLL_KIND_DISCLOSED.name);
expect(dialog.container.querySelector("p")).toHaveTextContent("Voters see results as soon as they have voted");
});
it("shows the closed poll description if we choose it", () => {
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
changeKind(dialog, M_POLL_KIND_UNDISCLOSED.name);
expect(dialog.container.querySelector("select")).toHaveValue(M_POLL_KIND_UNDISCLOSED.name);
expect(dialog.container.querySelector("p")).toHaveTextContent(
"Results are only revealed when you end the poll",
);
});
it("shows the open poll description if we choose it", () => {
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
changeKind(dialog, M_POLL_KIND_UNDISCLOSED.name);
changeKind(dialog, M_POLL_KIND_DISCLOSED.name);
expect(dialog.container.querySelector("select")).toHaveValue(M_POLL_KIND_DISCLOSED.name);
expect(dialog.container.querySelector("p")).toHaveTextContent("Voters see results as soon as they have voted");
});
it("shows the closed poll description when editing a closed poll", () => {
const previousEvent: MatrixEvent = new MatrixEvent(
PollStartEvent.from("Poll Q", ["Answer 1", "Answer 2"], M_POLL_KIND_UNDISCLOSED).serialize(),
);
previousEvent.event.event_id = "$prevEventId";
const dialog = render(
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} editingMxEvent={previousEvent} />,
);
expect(dialog.container.querySelector("select")).toHaveValue(M_POLL_KIND_UNDISCLOSED.name);
expect(dialog.container.querySelector("p")).toHaveTextContent(
"Results are only revealed when you end the poll",
);
});
it("displays a spinner after submitting", () => {
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
changeValue(dialog, "Question or topic", "Q");
changeValue(dialog, "Option 1", "A1");
changeValue(dialog, "Option 2", "A2");
expect(dialog.container.querySelector(".mx_Spinner")).toBeFalsy();
fireEvent.click(dialog.container.querySelector("button")!);
expect(dialog.container.querySelector(".mx_Spinner")).toBeDefined();
});
it("sends a poll create event when submitted", () => {
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
changeValue(dialog, "Question or topic", "Q");
changeValue(dialog, "Option 1", "A1");
changeValue(dialog, "Option 2", "A2");
fireEvent.click(dialog.container.querySelector("button")!);
const [, , eventType, sentEventContent] = mockClient.sendEvent.mock.calls[0];
expect(M_POLL_START.matches(eventType)).toBeTruthy();
expect(sentEventContent).toEqual({
[M_TEXT.name]: "Q\n1. A1\n2. A2",
[M_POLL_START.name]: {
answers: [
{
id: expect.any(String),
[M_TEXT.name]: "A1",
},
{
id: expect.any(String),
[M_TEXT.name]: "A2",
},
],
kind: M_POLL_KIND_DISCLOSED.name,
max_selections: 1,
question: {
body: "Q",
format: undefined,
formatted_body: undefined,
msgtype: "m.text",
[M_TEXT.name]: "Q",
},
},
});
});
it("sends a poll edit event when editing", () => {
const previousEvent: MatrixEvent = new MatrixEvent(
PollStartEvent.from("Poll Q", ["Answer 1", "Answer 2"], M_POLL_KIND_DISCLOSED).serialize(),
);
previousEvent.event.event_id = "$prevEventId";
const dialog = render(
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} editingMxEvent={previousEvent} />,
);
changeValue(dialog, "Question or topic", "Poll Q updated");
changeValue(dialog, "Option 2", "Answer 2 updated");
changeKind(dialog, M_POLL_KIND_UNDISCLOSED.name);
fireEvent.click(dialog.container.querySelector("button")!);
const [, , eventType, sentEventContent] = mockClient.sendEvent.mock.calls[0];
expect(M_POLL_START.matches(eventType)).toBeTruthy();
expect(sentEventContent).toEqual({
"m.new_content": {
[M_TEXT.name]: "Poll Q updated\n1. Answer 1\n2. Answer 2 updated",
[M_POLL_START.name]: {
answers: [
{
id: expect.any(String),
[M_TEXT.name]: "Answer 1",
},
{
id: expect.any(String),
[M_TEXT.name]: "Answer 2 updated",
},
],
kind: M_POLL_KIND_UNDISCLOSED.name,
max_selections: 1,
question: {
body: "Poll Q updated",
format: undefined,
formatted_body: undefined,
msgtype: "m.text",
[M_TEXT.name]: "Poll Q updated",
},
},
},
"m.relates_to": {
event_id: previousEvent.getId(),
rel_type: "m.replace",
},
});
});
it("retains poll disclosure type when editing", () => {
const previousEvent: MatrixEvent = new MatrixEvent(
PollStartEvent.from("Poll Q", ["Answer 1", "Answer 2"], M_POLL_KIND_DISCLOSED).serialize(),
);
previousEvent.event.event_id = "$prevEventId";
const dialog = render(
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} editingMxEvent={previousEvent} />,
);
changeValue(dialog, "Question or topic", "Poll Q updated");
fireEvent.click(dialog.container.querySelector("button")!);
const [, , eventType, sentEventContent] = mockClient.sendEvent.mock.calls[0];
expect(M_POLL_START.matches(eventType)).toBeTruthy();
// didnt change
expect((sentEventContent as ReplacementEvent<any>)["m.new_content"][M_POLL_START.name].kind).toEqual(
M_POLL_KIND_DISCLOSED.name,
);
});
});
function createRoom(): Room {
return new Room("roomid", MatrixClientPeg.safeGet(), "@name:example.com", {});
}
function changeValue(wrapper: RenderResult, labelText: string, value: string) {
fireEvent.change(wrapper.container.querySelector(`input[label="${labelText}"]`)!, {
target: { value: value },
});
}
function changeKind(wrapper: RenderResult, value: string) {
fireEvent.change(wrapper.container.querySelector("select")!, { target: { value: value } });
}
function expectSubmitToBeDisabled(wrapper: RenderResult, disabled: boolean) {
if (disabled) {
expect(wrapper.container.querySelector('button[type="submit"]')).toHaveAttribute("aria-disabled", "true");
} else {
expect(wrapper.container.querySelector('button[type="submit"]')).not.toHaveAttribute("aria-disabled", "true");
}
}

View file

@ -0,0 +1,92 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, screen } from "jest-matrix-react";
import { defer } from "matrix-js-sdk/src/utils";
import PowerSelector from "../../../../../src/components/views/elements/PowerSelector";
describe("<PowerSelector />", () => {
it("should reset back to custom value when custom input is blurred blank", async () => {
const fn = jest.fn();
render(<PowerSelector value={25} maxValue={100} usersDefault={0} onChange={fn} />);
const input = screen.getByLabelText("Power level");
fireEvent.change(input, { target: { value: "" } });
fireEvent.blur(input);
await screen.findByDisplayValue(25);
expect(fn).not.toHaveBeenCalled();
});
it("should reset back to preset value when custom input is blurred blank", async () => {
const fn = jest.fn();
render(<PowerSelector value={50} maxValue={100} usersDefault={0} onChange={fn} />);
const select = screen.getByLabelText("Power level");
fireEvent.change(select, { target: { value: "SELECT_VALUE_CUSTOM" } });
const input = screen.getByLabelText("Power level");
fireEvent.change(input, { target: { value: "" } });
fireEvent.blur(input);
const option = await screen.findByText<HTMLOptionElement>("Moderator");
expect(option.selected).toBeTruthy();
expect(fn).not.toHaveBeenCalled();
});
it("should call onChange when custom input is blurred with a number in it", async () => {
const fn = jest.fn();
render(<PowerSelector value={25} maxValue={100} usersDefault={0} onChange={fn} powerLevelKey="key" />);
const input = screen.getByLabelText("Power level");
fireEvent.change(input, { target: { value: 40 } });
fireEvent.blur(input);
await screen.findByDisplayValue(40);
expect(fn).toHaveBeenCalledWith(40, "key");
});
it("should reset when props get changed", async () => {
const fn = jest.fn();
const { rerender } = render(<PowerSelector value={50} maxValue={100} usersDefault={0} onChange={fn} />);
const select = screen.getByLabelText("Power level");
fireEvent.change(select, { target: { value: "SELECT_VALUE_CUSTOM" } });
rerender(<PowerSelector value={51} maxValue={100} usersDefault={0} onChange={fn} />);
await screen.findByDisplayValue(51);
rerender(<PowerSelector value={50} maxValue={100} usersDefault={0} onChange={fn} />);
const option = await screen.findByText<HTMLOptionElement>("Moderator");
expect(option.selected).toBeTruthy();
expect(fn).not.toHaveBeenCalled();
});
it("should reset when onChange promise rejects", async () => {
const deferred = defer<void>();
render(
<PowerSelector
value={25}
maxValue={100}
usersDefault={0}
onChange={() => deferred.promise}
powerLevelKey="key"
/>,
);
const input = screen.getByLabelText("Power level");
fireEvent.change(input, { target: { value: 40 } });
fireEvent.blur(input);
await screen.findByDisplayValue(40);
deferred.reject("Some error");
await screen.findByDisplayValue(25);
});
});

View file

@ -0,0 +1,51 @@
/*
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 { act, render } from "jest-matrix-react";
import ProgressBar from "../../../../../src/components/views/elements/ProgressBar";
jest.useFakeTimers();
describe("<ProgressBar/>", () => {
it("works when animated", () => {
const { container, rerender } = render(<ProgressBar max={100} value={50} animated={true} />);
const progress = container.querySelector<HTMLProgressElement>("progress")!;
// The animation always starts from 0
expect(progress.value).toBe(0);
// Await the animation to conclude to our initial value of 50
act(() => {
jest.runAllTimers();
});
expect(progress.position).toBe(0.5);
// Move the needle to 80%
rerender(<ProgressBar max={100} value={80} animated={true} />);
expect(progress.position).toBe(0.5);
// Let the animaiton run a tiny bit, assert it has moved from where it was to where it needs to go
act(() => {
jest.advanceTimersByTime(150);
});
expect(progress.position).toBeGreaterThan(0.5);
expect(progress.position).toBeLessThan(0.8);
});
it("works when not animated", () => {
const { container, rerender } = render(<ProgressBar max={100} value={50} animated={false} />);
const progress = container.querySelector<HTMLProgressElement>("progress")!;
// Without animation all positional updates are immediate, not requiring timers to run
expect(progress.position).toBe(0.5);
rerender(<ProgressBar max={100} value={80} animated={false} />);
expect(progress.position).toBe(0.8);
});
});

View file

@ -0,0 +1,35 @@
/*
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 { render, waitFor, cleanup } from "jest-matrix-react";
import React from "react";
import QRCode from "../../../../../src/components/views/elements/QRCode";
describe("<QRCode />", () => {
afterEach(() => {
cleanup();
});
it("shows a spinner when data is null", async () => {
const { container } = render(<QRCode data={null} />);
expect(container.querySelector(".mx_Spinner")).toBeDefined();
});
it("renders a QR with defaults", async () => {
const { container, getAllByAltText } = render(<QRCode data="asd" />);
await waitFor(() => getAllByAltText("QR Code").length === 1);
expect(container).toMatchSnapshot();
});
it("renders a QR with high error correction level", async () => {
const { container, getAllByAltText } = render(<QRCode data="asd" errorCorrectionLevel="high" />);
await waitFor(() => getAllByAltText("QR Code").length === 1);
expect(container).toMatchSnapshot();
});
});

View file

@ -0,0 +1,81 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import * as testUtils from "../../../../test-utils";
import { getParentEventId } from "../../../../../src/utils/Reply";
describe("ReplyChain", () => {
describe("getParentEventId", () => {
it("retrieves relation reply from unedited event", () => {
const originalEventWithRelation = testUtils.mkEvent({
event: true,
type: "m.room.message",
content: {
"msgtype": "m.text",
"body": "> Reply to this message\n\n foo",
"m.relates_to": {
"m.in_reply_to": {
event_id: "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og",
},
},
},
user: "some_other_user",
room: "room_id",
});
expect(getParentEventId(originalEventWithRelation)).toStrictEqual(
"$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og",
);
});
it("retrieves relation reply from original event when edited", () => {
const originalEventWithRelation = testUtils.mkEvent({
event: true,
type: "m.room.message",
content: {
"msgtype": "m.text",
"body": "> Reply to this message\n\n foo",
"m.relates_to": {
"m.in_reply_to": {
event_id: "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og",
},
},
},
user: "some_other_user",
room: "room_id",
});
const editEvent = testUtils.mkEvent({
event: true,
type: "m.room.message",
content: {
"msgtype": "m.text",
"body": "> Reply to this message\n\n * foo bar",
"m.new_content": {
msgtype: "m.text",
body: "foo bar",
},
"m.relates_to": {
rel_type: "m.replace",
event_id: originalEventWithRelation.getId(),
},
},
user: "some_other_user",
room: "room_id",
});
// The edit replaces the original event
originalEventWithRelation.makeReplaced(editEvent);
// The relation should be pulled from the original event
expect(getParentEventId(originalEventWithRelation)).toStrictEqual(
"$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og",
);
});
});
});

View file

@ -0,0 +1,35 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { render } from "jest-matrix-react";
import React from "react";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { mkRoom, mkRoomMember, stubClient, withClientContextRenderOptions } from "../../../../test-utils";
import RoomFacePile from "../../../../../src/components/views/elements/RoomFacePile";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
describe("<RoomFacePile />", () => {
it("renders", () => {
const cli = stubClient();
DMRoomMap.makeShared(cli);
const room = mkRoom(cli, "!123");
jest.spyOn(room, "getJoinedMembers").mockReturnValue([
mkRoomMember(room.roomId, "@bob:example.org", KnownMembership.Join),
]);
const { asFragment } = render(
<RoomFacePile onlyKnownUsers={false} room={room} />,
withClientContextRenderOptions(MatrixClientPeg.get()!),
);
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,107 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { Room } from "matrix-js-sdk/src/matrix";
import { fireEvent, render, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { mkEvent, stubClient } from "../../../../test-utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import RoomTopic from "../../../../../src/components/views/elements/RoomTopic";
import dis from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
jest.mock("../../../../../src/dispatcher/dispatcher");
describe("<RoomTopic/>", () => {
const originalHref = window.location.href;
afterEach(() => {
window.location.href = originalHref;
});
/**
* Create a room with the given topic
* @param topic
*/
function createRoom(topic: string) {
stubClient();
const room = new Room("!pMBteVpcoJRdCJxDmn:matrix.org", MatrixClientPeg.safeGet(), "@alice:example.org");
const topicEvent = mkEvent({
type: "m.room.topic",
room: "!pMBteVpcoJRdCJxDmn:matrix.org",
user: "@alice:example.org",
content: { topic },
ts: 123,
event: true,
});
room.addLiveEvents([topicEvent]);
return room;
}
/**
* Create a room and render it
* @param topic
*/
const renderRoom = (topic: string) => {
const room = createRoom(topic);
render(<RoomTopic room={room} />);
};
/**
* Create a room and click on the given text
* @param topic
* @param clickText
*/
function runClickTest(topic: string, clickText: string) {
renderRoom(topic);
fireEvent.click(screen.getByText(clickText));
}
it("should capture permalink clicks", () => {
const permalink =
"https://matrix.to/#/!pMBteVpcoJRdCJxDmn:matrix.org/$K4Kg0fL-GKpW1EQ6lS36bP4eUXadWJFkdK_FH73Df8A?via=matrix.org";
const expectedHref =
"http://localhost/#/room/!pMBteVpcoJRdCJxDmn:matrix.org/$K4Kg0fL-GKpW1EQ6lS36bP4eUXadWJFkdK_FH73Df8A?via=matrix.org";
runClickTest(`... ${permalink} ...`, permalink);
expect(window.location.href).toEqual(expectedHref);
expect(dis.fire).toHaveBeenCalledTimes(0);
});
it("should not capture non-permalink clicks", () => {
const link = "https://matrix.org";
const expectedHref = originalHref;
runClickTest(`... ${link} ...`, link);
expect(window.location.href).toEqual(expectedHref);
expect(dis.fire).toHaveBeenCalledTimes(0);
});
it("should open topic dialog when not clicking a link", () => {
const topic = "foobar";
const expectedHref = originalHref;
runClickTest(topic, topic);
expect(window.location.href).toEqual(expectedHref);
expect(dis.fire).toHaveBeenCalledWith(Action.ShowRoomTopic);
});
it("should open the tooltip when hovering a text", async () => {
const topic = "room topic";
renderRoom(topic);
await userEvent.hover(screen.getByText(topic));
await waitFor(() => expect(screen.getByRole("tooltip", { name: "Click to read topic" })).toBeInTheDocument());
});
it("should not open the tooltip when hovering a link", async () => {
const topic = "https://matrix.org";
renderRoom(topic);
await userEvent.hover(screen.getByText(topic));
await waitFor(() => expect(screen.queryByRole("tooltip", { name: "Click to read topic" })).toBeNull());
});
});

View file

@ -0,0 +1,45 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { render } from "jest-matrix-react";
import React from "react";
import SdkConfig from "../../../../../src/SdkConfig";
import SearchWarning, { WarningKind } from "../../../../../src/components/views/elements/SearchWarning";
describe("<SearchWarning />", () => {
describe("with desktop builds available", () => {
beforeEach(() => {
SdkConfig.put({
brand: "Element",
desktop_builds: {
available: true,
logo: "https://logo",
url: "https://url",
},
});
});
it("renders with a logo by default", () => {
const { asFragment, getByRole } = render(
<SearchWarning isRoomEncrypted={true} kind={WarningKind.Search} />,
);
expect(getByRole("img")).toHaveAttribute("src", "https://logo");
expect(asFragment()).toMatchSnapshot();
});
it("renders without a logo when showLogo=false", () => {
const { asFragment, queryByRole } = render(
<SearchWarning isRoomEncrypted={true} kind={WarningKind.Search} showLogo={false} />,
);
expect(queryByRole("img")).not.toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,33 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen, waitForElementToBeRemoved } from "jest-matrix-react";
import SpellCheckLanguagesDropdown from "../../../../../src/components/views/elements/SpellCheckLanguagesDropdown";
import PlatformPeg from "../../../../../src/PlatformPeg";
describe("<SpellCheckLanguagesDropdown />", () => {
it("renders as expected", async () => {
const platform: any = {
getAvailableSpellCheckLanguages: jest.fn().mockResolvedValue(["en", "de", "qq"]),
supportsSetting: jest.fn(),
};
PlatformPeg.set(platform);
const { asFragment } = render(
<SpellCheckLanguagesDropdown
className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
value="en"
onOptionChange={jest.fn()}
/>,
);
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,98 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, RenderResult } from "jest-matrix-react";
import StyledRadioGroup from "../../../../../src/components/views/elements/StyledRadioGroup";
describe("<StyledRadioGroup />", () => {
const optionA = {
value: "Anteater",
label: <span>Anteater label</span>,
description: "anteater description",
className: "a-class",
};
const optionB = {
value: "Badger",
label: <span>Badger label</span>,
};
const optionC = {
value: "Canary",
label: <span>Canary label</span>,
description: <span>Canary description</span>,
};
const defaultDefinitions = [optionA, optionB, optionC];
const defaultProps = {
name: "test",
className: "test-class",
definitions: defaultDefinitions,
onChange: jest.fn(),
};
const getComponent = (props = {}) => render(<StyledRadioGroup {...defaultProps} {...props} />);
const getInputByValue = (component: RenderResult, value: string) =>
component.container.querySelector<HTMLInputElement>(`input[value="${value}"]`);
const getCheckedInput = (component: RenderResult) =>
component.container.querySelector<HTMLInputElement>("input[checked]");
it("renders radios correctly when no value is provided", () => {
const component = getComponent();
expect(component.asFragment()).toMatchSnapshot();
expect(getCheckedInput(component)).toBeFalsy();
});
it("selects correct button when value is provided", () => {
const component = getComponent({
value: optionC.value,
});
expect(getCheckedInput(component)?.value).toEqual(optionC.value);
});
it("selects correct buttons when definitions have checked prop", () => {
const definitions = [{ ...optionA, checked: true }, optionB, { ...optionC, checked: false }];
const component = getComponent({
value: optionC.value,
definitions,
});
expect(getInputByValue(component, optionA.value)).toBeChecked();
expect(getInputByValue(component, optionB.value)).not.toBeChecked();
// optionC.checked = false overrides value matching
expect(getInputByValue(component, optionC.value)).not.toBeChecked();
});
it("disables individual buttons based on definition.disabled", () => {
const definitions = [optionA, { ...optionB, disabled: true }, { ...optionC, disabled: true }];
const component = getComponent({ definitions });
expect(getInputByValue(component, optionA.value)).not.toBeDisabled();
expect(getInputByValue(component, optionB.value)).toBeDisabled();
expect(getInputByValue(component, optionC.value)).toBeDisabled();
});
it("disables all buttons with disabled prop", () => {
const component = getComponent({ disabled: true });
expect(getInputByValue(component, optionA.value)).toBeDisabled();
expect(getInputByValue(component, optionB.value)).toBeDisabled();
expect(getInputByValue(component, optionC.value)).toBeDisabled();
});
it("calls onChange on click", () => {
const onChange = jest.fn();
const component = getComponent({
value: optionC.value,
onChange,
});
fireEvent.click(getInputByValue(component, optionB.value)!);
expect(onChange).toHaveBeenCalledWith(optionB.value);
});
});

View file

@ -0,0 +1,30 @@
/* eslint @typescript-eslint/no-unused-vars: ["error", { "varsIgnorePattern": "^_" }] */
// Copyright 2024 New Vector Ltd.
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
// Please see LICENSE files in the repository root for full details.
import { render, waitFor } from "jest-matrix-react";
import hljs, { type HighlightOptions } from "highlight.js";
import React from "react";
import SyntaxHighlight from "../../../../../src/components/views/elements/SyntaxHighlight";
describe("<SyntaxHighlight />", () => {
it("renders", async () => {
const { container } = render(<SyntaxHighlight>console.log("Hello, World!");</SyntaxHighlight>);
await waitFor(() => expect(container.querySelector(".language-arcade")).toBeTruthy());
expect(container).toMatchSnapshot();
});
it.each(["json", "javascript", "css"])("uses the provided language", async (lang) => {
const mock = jest.spyOn(hljs, "highlight");
const { container } = render(<SyntaxHighlight language={lang}>// Hello, World</SyntaxHighlight>);
await waitFor(() => expect(container.querySelector(`.language-${lang}`)).toBeTruthy());
const [_lang, opts] = mock.mock.lastCall!;
expect((opts as unknown as HighlightOptions)["language"]).toBe(lang);
});
});

View file

@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AccessibleButton /> renders a button element 1`] = `
<DocumentFragment>
<button
class="mx_AccessibleButton"
role="button"
tabindex="0"
>
i am a button
</button>
</DocumentFragment>
`;
exports[`<AccessibleButton /> renders div with role button by default 1`] = `
<DocumentFragment>
<div
class="mx_AccessibleButton"
role="button"
tabindex="0"
>
i am a button
</div>
</DocumentFragment>
`;
exports[`<AccessibleButton /> renders with correct classes when button has kind 1`] = `
<DocumentFragment>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
i am a button
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,471 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AppTile destroys non-persisted right panel widget on room change 1`] = `
<DocumentFragment>
<aside
class="mx_RightPanel"
id="mx_RightPanel"
>
<div
class="mx_BaseCard mx_WidgetCard"
>
<div
class="mx_BaseCard_header"
>
<div
class="mx_BaseCard_header_title"
>
<h4
class="mx_Heading_h4 mx_BaseCard_header_title_heading"
>
Example 1
</h4>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_BaseCard_header_title_button--option"
role="button"
tabindex="0"
/>
</div>
<button
aria-labelledby="floating-ui-1"
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
data-testid="base-card-close-button"
role="button"
style="--cpd-icon-button-size: 28px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
/>
</svg>
</div>
</button>
</div>
<div
class="mx_AppTileFullWidth"
id="1"
>
<div
class="mx_AppTileBody mx_AppTileBody--large"
>
<div
class="mx_AppTileBody_fadeInSpinner"
>
<div
class="mx_Spinner"
>
<div
class="mx_Spinner_Msg"
>
Loading…
</div>
 
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
</div>
</div>
</div>
</aside>
</DocumentFragment>
`;
exports[`AppTile for a persistent app should render 1`] = `
<DocumentFragment>
<div
class="mx_AppTile_mini"
id="1"
>
<div
class="mx_AppTile_persistedWrapper"
>
<div />
</div>
</div>
</DocumentFragment>
`;
exports[`AppTile for a pinned widget should render 1`] = `
<DocumentFragment>
<div
class="mx_AppTile"
id="1"
>
<div
class="mx_AppTileMenuBar"
>
<span
class="mx_AppTileMenuBar_title"
style="pointer-events: none;"
>
<span>
<span
aria-label="Avatar"
class="_avatar_mcap2_17 mx_BaseAvatar mx_WidgetAvatar"
data-color="1"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 20px;"
>
<img
alt=""
class="_image_mcap2_50"
data-type="round"
height="20px"
loading="lazy"
referrerpolicy="no-referrer"
src="image-file-stub"
width="20px"
/>
</span>
<h3>
Example 1
</h3>
<span />
</span>
</span>
<span
class="mx_AppTileMenuBar_widgets"
>
<div
aria-label="Un-maximise"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_12"
/>
</div>
<div
aria-label="Minimise"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_12"
/>
</div>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_12"
/>
</div>
</span>
</div>
<div
class="mx_AppTile_persistedWrapper"
>
<div />
</div>
</div>
</DocumentFragment>
`;
exports[`AppTile for a pinned widget should render permission request 1`] = `
<DocumentFragment>
<div
class="mx_AppTile"
id="1"
>
<div
class="mx_AppTileMenuBar"
>
<span
class="mx_AppTileMenuBar_title"
style="pointer-events: none;"
>
<span>
<span
aria-label="Avatar"
class="_avatar_mcap2_17 mx_BaseAvatar mx_WidgetAvatar"
data-color="1"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 20px;"
>
<img
alt=""
class="_image_mcap2_50"
data-type="round"
height="20px"
loading="lazy"
referrerpolicy="no-referrer"
src="image-file-stub"
width="20px"
/>
</span>
<h3>
Example 1
</h3>
<span />
</span>
</span>
<span
class="mx_AppTileMenuBar_widgets"
>
<div
aria-label="Un-maximise"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_12"
/>
</div>
<div
aria-label="Minimise"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_12"
/>
</div>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_12"
/>
</div>
</span>
</div>
<div
class="mx_AppTileBody mx_AppTileBody--large"
>
<div
class="mx_AppPermission"
>
<div
class="mx_AppPermission_content"
>
<div
class="mx_AppPermission_content_bolder"
>
Widget added by
</div>
<div>
<span
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="1"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 38px;"
>
u
</span>
<h4
class="mx_Heading_h4"
>
@userAnother
</h4>
<div />
</div>
<div>
<span>
Using this widget may share data
<div
aria-describedby="floating-ui-87"
aria-labelledby="floating-ui-86"
class="mx_TextWithTooltip_target mx_TextWithTooltip_target--helpIcon"
>
<svg
class="mx_Icon mx_Icon_12"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 8a1.5 1.5 0 0 0-1.5 1.5 1 1 0 1 1-2 0 3.5 3.5 0 1 1 6.01 2.439c-.122.126-.24.243-.352.355-.287.288-.54.54-.76.824-.293.375-.398.651-.398.882a1 1 0 1 1-2 0c0-.874.407-1.58.819-2.11.305-.392.688-.775 1-1.085l.257-.26A1.5 1.5 0 0 0 12 8Zm1 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
/>
<path
d="M8.1 21.212A9.738 9.738 0 0 0 12 22a9.738 9.738 0 0 0 3.9-.788 10.098 10.098 0 0 0 3.175-2.137c.9-.9 1.613-1.958 2.137-3.175A9.738 9.738 0 0 0 22 12a9.738 9.738 0 0 0-.788-3.9 10.099 10.099 0 0 0-2.137-3.175c-.9-.9-1.958-1.612-3.175-2.137A9.738 9.738 0 0 0 12 2a9.738 9.738 0 0 0-3.9.788 10.099 10.099 0 0 0-3.175 2.137c-.9.9-1.612 1.958-2.137 3.175A9.738 9.738 0 0 0 2 12a9.74 9.74 0 0 0 .788 3.9 10.098 10.098 0 0 0 2.137 3.175c.9.9 1.958 1.613 3.175 2.137Zm9.575-3.537C16.125 19.225 14.233 20 12 20c-2.233 0-4.125-.775-5.675-2.325C4.775 16.125 4 14.233 4 12c0-2.233.775-4.125 2.325-5.675C7.875 4.775 9.767 4 12 4c2.233 0 4.125.775 5.675 2.325C19.225 7.875 20 9.767 20 12c0 2.233-.775 4.125-2.325 5.675Z"
/>
</svg>
</div>
with example.com.
</span>
</div>
<div>
This widget may use cookies. 
</div>
<div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_sm"
role="button"
tabindex="0"
>
Continue
</div>
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;
exports[`AppTile preserves non-persisted widget on container move 1`] = `
<DocumentFragment>
<div
class="mx_AppsDrawer"
>
<div
class="mx_AppsDrawer_resizer"
style="position: relative; user-select: auto; width: auto; height: 280px; max-height: 576px; min-height: 100px; box-sizing: border-box; flex-shrink: 0;"
>
<div
class="mx_AppsContainer"
>
<div
class="mx_AppTileFullWidth"
id="1"
>
<div
class="mx_AppTileMenuBar"
>
<span
class="mx_AppTileMenuBar_title"
style="pointer-events: none;"
>
<span>
<span
aria-label="Avatar"
class="_avatar_mcap2_17 mx_BaseAvatar mx_WidgetAvatar"
data-color="1"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 20px;"
>
<img
alt=""
class="_image_mcap2_50"
data-type="round"
height="20px"
loading="lazy"
referrerpolicy="no-referrer"
src="image-file-stub"
width="20px"
/>
</span>
<h3>
Example 1
</h3>
<span />
</span>
</span>
<span
class="mx_AppTileMenuBar_widgets"
>
<div
aria-label="Maximise"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_12"
/>
</div>
<div
aria-label="Minimise"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_12"
/>
</div>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_12"
/>
</div>
</span>
</div>
<div
class="mx_AppTileBody mx_AppTileBody--large"
>
<div
class="mx_AppTileBody_fadeInSpinner"
>
<div
class="mx_Spinner"
>
<div
class="mx_Spinner_Msg"
>
Loading…
</div>
 
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
</div>
</div>
</div>
<div
class="mx_AppsDrawer_resizer_container"
>
<div
class="mx_AppsDrawer_resizer_container_handle"
style="position: absolute; user-select: none; width: 100%; height: 10px; left: 0px; cursor: row-resize; bottom: -5px;"
/>
</div>
</div>
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<EffectsOverlay/> should render 1`] = `
<DocumentFragment>
<canvas
aria-hidden="true"
height="768"
style="display: block; z-index: 999999; pointer-events: none; position: fixed; top: 0px; right: 0px;"
width="100"
/>
</DocumentFragment>
`;

View file

@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ExternalLink /> renders link correctly 1`] = `
<DocumentFragment>
<a
class="mx_ExternalLink myCustomClass"
data-testid="test"
href="test.com"
rel="noopener"
target="_self"
>
<span>
react element
<b>
children
</b>
</span>
<i
class="mx_ExternalLink_icon"
/>
</a>
</DocumentFragment>
`;
exports[`<ExternalLink /> renders plain text link correctly 1`] = `
<DocumentFragment>
<a
class="mx_ExternalLink myCustomClass"
data-testid="test"
href="test.com"
rel="noreferrer noopener"
target="_blank"
>
test
<i
class="mx_ExternalLink_icon"
/>
</a>
</DocumentFragment>
`;

View file

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<FacePile /> renders with a tooltip 1`] = `
<DocumentFragment>
<div
aria-labelledby="floating-ui-1"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
>
<div
class="_stacked-avatars_mcap2_111"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="4"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 36px;"
>
4
</span>
</div>
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,137 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<FilterDropdown /> renders dropdown options in menu 1`] = `
<ul
class="mx_Dropdown_menu"
id="test_listbox"
role="listbox"
>
<li
aria-selected="true"
class="mx_Dropdown_option mx_Dropdown_option_highlight"
id="test__one"
role="option"
>
<div
class="mx_FilterDropdown_option"
data-testid="filter-option-one"
>
<div
class="mx_FilterDropdown_optionSelectedIcon"
/>
<span
class="mx_FilterDropdown_optionLabel"
>
Option one
</span>
</div>
</li>
<li
aria-selected="false"
class="mx_Dropdown_option"
id="test__two"
role="option"
>
<div
class="mx_FilterDropdown_option"
data-testid="filter-option-two"
>
<span
class="mx_FilterDropdown_optionLabel"
>
Option two
</span>
<span
class="mx_FilterDropdown_optionDescription"
>
with description
</span>
</div>
</li>
</ul>
`;
exports[`<FilterDropdown /> renders selected option 1`] = `
<div>
<div
class="mx_Dropdown mx_FilterDropdown test"
>
<div
aria-describedby="test_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="test label"
aria-owns="test_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
>
<div
class="mx_Dropdown_option"
id="test_value"
>
Option one
</div>
<span
class="mx_Dropdown_arrow"
/>
</div>
</div>
</div>
`;
exports[`<FilterDropdown /> renders selected option with selectedLabel 1`] = `
<div>
<div
class="mx_Dropdown mx_FilterDropdown test"
>
<div
aria-describedby="test_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="test label"
aria-owns="test_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
>
<div
class="mx_Dropdown_option"
id="test_value"
>
Show: Option one
</div>
<span
class="mx_Dropdown_arrow"
/>
</div>
</div>
</div>
`;
exports[`<FilterDropdown /> renders when selected option is not in options 1`] = `
<div>
<div
class="mx_Dropdown mx_FilterDropdown test"
>
<div
aria-describedby="test_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="test label"
aria-owns="test_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
>
<div
class="mx_Dropdown_option"
id="test_value"
/>
<span
class="mx_Dropdown_arrow"
/>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<FilterTabGroup /> renders options 1`] = `
<div>
<fieldset
class="mx_FilterTabGroup"
data-testid="test"
>
<label
data-testid="filter-tab-test-Apple"
>
<input
checked=""
name="test"
type="radio"
value="Apple"
/>
<span>
Label for Apple
</span>
</label>
<label
data-testid="filter-tab-test-Banana"
>
<input
name="test"
type="radio"
value="Banana"
/>
<span>
Label for Banana
</span>
</label>
<label
data-testid="filter-tab-test-Orange"
>
<input
name="test"
type="radio"
value="Orange"
/>
<span>
Label for Orange
</span>
</label>
</fieldset>
</div>
`;

View file

@ -0,0 +1,82 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ImageView /> renders correctly 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-label="Image view"
class="mx_ImageView"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_ImageView_panel"
>
<div />
<div
class="mx_ImageView_toolbar"
>
<div
aria-describedby="floating-ui-2"
aria-label="Zoom out"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_zoomOut"
role="button"
tabindex="0"
/>
<div
aria-label="Zoom in"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_zoomIn"
role="button"
tabindex="0"
/>
<div
aria-label="Rotate Left"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_rotateCCW"
role="button"
tabindex="0"
/>
<div
aria-label="Rotate Right"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_rotateCW"
role="button"
tabindex="0"
/>
<div
aria-label="Download"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_download"
role="button"
tabindex="0"
/>
<div
aria-label="Close"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_close"
role="button"
tabindex="0"
/>
</div>
</div>
<div
class="mx_ImageView_image_wrapper"
>
<img
class="mx_ImageView_image "
draggable="true"
src="https://example.com/image.png"
style="transform: translateX(0px)
translateY(0px)
scale(0)
rotate(0deg); cursor: zoom-out;"
/>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;

View file

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InfoTooltip should show tooltip on hover 1`] = `
<DocumentFragment>
<div
aria-describedby="floating-ui-2"
class="mx_InfoTooltip"
tabindex="0"
>
<span
aria-label="Information"
class="mx_InfoTooltip_icon mx_InfoTooltip_icon_info"
/>
Trigger text
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,82 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<LabelledCheckbox /> should render with byline of "this is a byline" 1`] = `
<DocumentFragment>
<label
class="mx_LabelledCheckbox"
>
<span
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
checked=""
id="checkbox_vY7Q4uEh9K"
type="checkbox"
/>
<label
for="checkbox_vY7Q4uEh9K"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
</label>
</span>
<div
class="mx_LabelledCheckbox_labels"
>
<span
class="mx_LabelledCheckbox_label"
>
Hello world
</span>
<span
class="mx_LabelledCheckbox_byline"
>
this is a byline
</span>
</div>
</label>
</DocumentFragment>
`;
exports[`<LabelledCheckbox /> should render with byline of undefined 1`] = `
<DocumentFragment>
<label
class="mx_LabelledCheckbox"
>
<span
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
>
<input
checked=""
id="checkbox_vY7Q4uEh9K"
type="checkbox"
/>
<label
for="checkbox_vY7Q4uEh9K"
>
<div
class="mx_Checkbox_background"
>
<div
class="mx_Checkbox_checkmark"
/>
</div>
</label>
</span>
<div
class="mx_LabelledCheckbox_labels"
>
<span
class="mx_LabelledCheckbox_label"
>
Hello world
</span>
</div>
</label>
</DocumentFragment>
`;

View file

@ -0,0 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<LearnMore /> renders button 1`] = `
<div>
<div
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="testid"
role="button"
tabindex="0"
>
Learn more
</div>
</div>
`;

View file

@ -0,0 +1,294 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Pill> should not render a non-permalink 1`] = `
<DocumentFragment>
<div />
</DocumentFragment>
`;
exports[`<Pill> should not render an avatar or link when called with inMessage = false and shouldShowPillAvatar = false 1`] = `
<DocumentFragment>
<div>
<bdi>
<span
tabindex="0"
>
<span
class="mx_Pill mx_RoomPill"
>
<span
class="mx_Pill_text"
>
Room 1
</span>
</span>
</span>
</bdi>
</div>
</DocumentFragment>
`;
exports[`<Pill> should render the expected pill for @room 1`] = `
<DocumentFragment>
<div>
<bdi>
<span
tabindex="0"
>
<span
class="mx_Pill mx_AtRoomPill"
>
<span
aria-hidden="true"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="1"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 16px;"
>
R
</span>
<span
class="mx_Pill_text"
>
@room
</span>
</span>
</span>
</bdi>
</div>
</DocumentFragment>
`;
exports[`<Pill> should render the expected pill for a known user not in the room 1`] = `
<DocumentFragment>
<div>
<bdi>
<a
class="mx_Pill mx_UserPill"
href="https://matrix.to/#/@user2:example.com"
>
<span
aria-hidden="true"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="5"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 16px;"
>
U
</span>
<span
class="mx_Pill_text"
>
User 2
</span>
</a>
</bdi>
</div>
</DocumentFragment>
`;
exports[`<Pill> should render the expected pill for a message in another room 1`] = `
<DocumentFragment>
<div>
<bdi>
<a
class="mx_Pill mx_EventPill"
href="https://matrix.to/#/!room1:example.com/$123-456"
>
<span
aria-hidden="true"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="1"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 16px;"
>
R
</span>
<span
class="mx_Pill_text"
>
Message in Room 1
</span>
</a>
</bdi>
</div>
</DocumentFragment>
`;
exports[`<Pill> should render the expected pill for a message in the same room 1`] = `
<DocumentFragment>
<div>
<bdi>
<a
class="mx_Pill mx_EventPill"
href="https://matrix.to/#/!room1:example.com/$123-456"
>
<span
aria-hidden="true"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="4"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 16px;"
>
U
</span>
<span
class="mx_Pill_text"
>
Message from User 1
</span>
</a>
</bdi>
</div>
</DocumentFragment>
`;
exports[`<Pill> should render the expected pill for a room alias 1`] = `
<DocumentFragment>
<div>
<bdi>
<a
class="mx_Pill mx_RoomPill"
href="https://matrix.to/#/#room1:example.com"
>
<span
aria-hidden="true"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="1"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 16px;"
>
R
</span>
<span
class="mx_Pill_text"
>
Room 1
</span>
</a>
</bdi>
</div>
</DocumentFragment>
`;
exports[`<Pill> should render the expected pill for a space 1`] = `
<DocumentFragment>
<div>
<bdi>
<a
class="mx_Pill mx_RoomPill"
href="https://matrix.to/#/!space1:example.com"
>
<span
aria-hidden="true"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="2"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 16px;"
>
S
</span>
<span
class="mx_Pill_text"
>
Space 1
</span>
</a>
</bdi>
</div>
</DocumentFragment>
`;
exports[`<Pill> should render the expected pill for an uknown user not in the room 1`] = `
<DocumentFragment>
<div>
<bdi>
<a
class="mx_Pill mx_UserPill"
href="https://matrix.to/#/@user3:example.com"
>
<div
class="mx_Pill_UserIcon mx_BaseAvatar"
/>
<span
class="mx_Pill_text"
>
@user3:example.com
</span>
</a>
</bdi>
</div>
</DocumentFragment>
`;
exports[`<Pill> when rendering a pill for a room should render the expected pill 1`] = `
<DocumentFragment>
<div>
<bdi>
<a
class="mx_Pill mx_RoomPill"
href="https://matrix.to/#/!room1:example.com"
>
<span
aria-hidden="true"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="1"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 16px;"
>
R
</span>
<span
class="mx_Pill_text"
>
Room 1
</span>
</a>
</bdi>
</div>
</DocumentFragment>
`;
exports[`<Pill> when rendering a pill for a user in the room should render as expected 1`] = `
<DocumentFragment>
<div>
<bdi>
<a
class="mx_Pill mx_UserPill"
href="https://matrix.to/#/@user1:example.com"
>
<span
aria-hidden="true"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="4"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 16px;"
>
U
</span>
<span
class="mx_Pill_text"
>
User 1
</span>
</a>
</bdi>
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,560 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PollCreateDialog renders a blank poll 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_CompoundDialog_content"
aria-labelledby="mx_CompoundDialog_title"
class="mx_CompoundDialog mx_ScrollableBaseDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_CompoundDialog_header"
>
<h1>
Create poll
</h1>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_CompoundDialog_cancelButton"
role="button"
tabindex="0"
/>
<form
class="mx_CompoundDialog_form"
>
<div
class="mx_CompoundDialog_content"
>
<div
class="mx_PollCreateDialog"
>
<h2>
Poll type
</h2>
<div
class="mx_Field mx_Field_select"
>
<select
id="mx_Field_1"
type="text"
>
<option
value="org.matrix.msc3381.poll.disclosed"
>
Open poll
</option>
<option
value="org.matrix.msc3381.poll.undisclosed"
>
Closed poll
</option>
</select>
<label
for="mx_Field_1"
/>
</div>
<p>
Voters see results as soon as they have voted
</p>
<h2>
What is your poll question or topic?
</h2>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="poll-topic-input"
label="Question or topic"
maxlength="340"
placeholder="Write something…"
type="text"
value=""
/>
<label
for="poll-topic-input"
>
Question or topic
</label>
</div>
<h2>
Create options
</h2>
<div
class="mx_PollCreateDialog_option"
>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="pollcreate_option_0"
label="Option 1"
maxlength="340"
placeholder="Write an option"
type="text"
value=""
/>
<label
for="pollcreate_option_0"
>
Option 1
</label>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_PollCreateDialog_option"
>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="pollcreate_option_1"
label="Option 2"
maxlength="340"
placeholder="Write an option"
type="text"
value=""
/>
<label
for="pollcreate_option_1"
>
Option 2
</label>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
role="button"
tabindex="0"
>
Add option
</div>
</div>
</div>
<div
class="mx_CompoundDialog_footer"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Cancel
</div>
<button
aria-disabled="true"
class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled"
disabled=""
role="button"
tabindex="0"
type="submit"
>
Create Poll
</button>
</div>
</form>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`PollCreateDialog renders a question and some options 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_CompoundDialog_content"
aria-labelledby="mx_CompoundDialog_title"
class="mx_CompoundDialog mx_ScrollableBaseDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_CompoundDialog_header"
>
<h1>
Create poll
</h1>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_CompoundDialog_cancelButton"
role="button"
tabindex="0"
/>
<form
class="mx_CompoundDialog_form"
>
<div
class="mx_CompoundDialog_content"
>
<div
class="mx_PollCreateDialog"
>
<h2>
Poll type
</h2>
<div
class="mx_Field mx_Field_select"
>
<select
id="mx_Field_4"
type="text"
>
<option
value="org.matrix.msc3381.poll.disclosed"
>
Open poll
</option>
<option
value="org.matrix.msc3381.poll.undisclosed"
>
Closed poll
</option>
</select>
<label
for="mx_Field_4"
/>
</div>
<p>
Voters see results as soon as they have voted
</p>
<h2>
What is your poll question or topic?
</h2>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="poll-topic-input"
label="Question or topic"
maxlength="340"
placeholder="Write something…"
type="text"
value="How many turnips is the optimal number?"
/>
<label
for="poll-topic-input"
>
Question or topic
</label>
</div>
<h2>
Create options
</h2>
<div
class="mx_PollCreateDialog_option"
>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="pollcreate_option_0"
label="Option 1"
maxlength="340"
placeholder="Write an option"
type="text"
value="As many as my neighbour"
/>
<label
for="pollcreate_option_0"
>
Option 1
</label>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_PollCreateDialog_option"
>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="pollcreate_option_1"
label="Option 2"
maxlength="340"
placeholder="Write an option"
type="text"
value="The question is meaningless"
/>
<label
for="pollcreate_option_1"
>
Option 2
</label>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_PollCreateDialog_option"
>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="pollcreate_option_2"
label="Option 3"
maxlength="340"
placeholder="Write an option"
type="text"
value="Mu"
/>
<label
for="pollcreate_option_2"
>
Option 3
</label>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
role="button"
tabindex="0"
>
Add option
</div>
</div>
</div>
<div
class="mx_CompoundDialog_footer"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Cancel
</div>
<button
class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
type="submit"
>
Create Poll
</button>
</div>
</form>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`PollCreateDialog renders info from a previous event 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_CompoundDialog_content"
aria-labelledby="mx_CompoundDialog_title"
class="mx_CompoundDialog mx_ScrollableBaseDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_CompoundDialog_header"
>
<h1>
Edit poll
</h1>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_CompoundDialog_cancelButton"
role="button"
tabindex="0"
/>
<form
class="mx_CompoundDialog_form"
>
<div
class="mx_CompoundDialog_content"
>
<div
class="mx_PollCreateDialog"
>
<h2>
Poll type
</h2>
<div
class="mx_Field mx_Field_select"
>
<select
id="mx_Field_5"
type="text"
>
<option
value="org.matrix.msc3381.poll.disclosed"
>
Open poll
</option>
<option
value="org.matrix.msc3381.poll.undisclosed"
>
Closed poll
</option>
</select>
<label
for="mx_Field_5"
/>
</div>
<p>
Voters see results as soon as they have voted
</p>
<h2>
What is your poll question or topic?
</h2>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="poll-topic-input"
label="Question or topic"
maxlength="340"
placeholder="Write something…"
type="text"
value="Poll Q"
/>
<label
for="poll-topic-input"
>
Question or topic
</label>
</div>
<h2>
Create options
</h2>
<div
class="mx_PollCreateDialog_option"
>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="pollcreate_option_0"
label="Option 1"
maxlength="340"
placeholder="Write an option"
type="text"
value="Answer 1"
/>
<label
for="pollcreate_option_0"
>
Option 1
</label>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_PollCreateDialog_option"
>
<div
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
>
<input
id="pollcreate_option_1"
label="Option 2"
maxlength="340"
placeholder="Write an option"
type="text"
value="Answer 2"
/>
<label
for="pollcreate_option_1"
>
Option 2
</label>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
role="button"
tabindex="0"
/>
</div>
<div
class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
role="button"
tabindex="0"
>
Add option
</div>
</div>
</div>
<div
class="mx_CompoundDialog_footer"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Cancel
</div>
<button
class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
type="submit"
>
Done
</button>
</div>
</form>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View file

@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<QRCode /> renders a QR with defaults 1`] = `
<div>
<div
class="mx_QRCode"
>
<img
alt="QR Code"
class="mx_VerificationQRCode"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHQAAAB0CAYAAABUmhYnAAAAAklEQVR4AewaftIAAALASURBVO3BQW7kQAwEwSxC//9yro88NSBIM2sTjIg/WGMUa5RijVKsUYo1SrFGKdYoxRqlWKMUa5RijVKsUYo1SrFGKdYoxRrl4qEkfJPKSRI6lS4JJypdEr5J5YlijVKsUYo1ysXLVN6UhCeScKJyh8qbkvCmYo1SrFGKNcrFhyXhDpU7knCi0iWhS0KnckcS7lD5pGKNUqxRijXKxR+n0iXhDpVJijVKsUYp1igXw6h0SehUuiR0Kn9ZsUYp1ijFGuXiw1T+J5VPUvlNijVKsUYp1igXL0vCNyWhU+mS0Kk8kYTfrFijFGuUYo1y8ZDKb5KETqVLwh0qf0mxRinWKMUa5eKhJNyh0iXhk5JwotIl4UTljiR0Kp9UrFGKNUqxRrl4SKVLQqfSJaFT6ZLQqXyTSpeEO5LQqZwkoVN5olijFGuUYo1y8VASOpUTlTuScKLSJaFTOUlCp9KpnCThjiR8UrFGKdYoxRrl4j9LQqfypiR0Kp3KSRI6lU7lDpVPKtYoxRqlWKNcfFgSnkhCp3KHykkSOpU7ktCp3JGETuWJYo1SrFGKNUr8wR+WhCdUuiR0Kl0SOpU7knCi8kSxRinWKMUa5eKhJHyTyolKl4QnkvBEEr6pWKMUa5RijXLxMpU3JeEJlS4JXRJOVLokdEk4UTlJwpuKNUqxRinWKBcfloQ7VL5JpUtCl4QTlZMkdCqfVKxRijVKsUa5+ONUuiR0Kp3KiUqXhE7liSR0Km8q1ijFGqVYo1z8cUk4SUKn0iWhU+lUTpLQqXQqJ0noVJ4o1ijFGqVYo1x8mMo3qXRJOFE5SUKn0qmcJOFE5U3FGqVYoxRrlIuXJeE3UTlJQqdyRxI6lTuS0Kk8UaxRijVKsUaJP1hjFGuUYo1SrFGKNUqxRinWKMUapVijFGuUYo1SrFGKNUqxRinWKMUa5R93wRT2jE+pTAAAAABJRU5ErkJggg=="
/>
</div>
</div>
`;
exports[`<QRCode /> renders a QR with high error correction level 1`] = `
<div>
<div
class="mx_QRCode"
>
<img
alt="QR Code"
class="mx_VerificationQRCode"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHQAAAB0CAYAAABUmhYnAAAAAklEQVR4AewaftIAAAKwSURBVO3BQY6YUAwFwW6L+1/5ZZZefQkBk8RylfnBGqNYoxRrlGKNUqxRijVKsUYp1ijFGqVYoxRrlGKNUqxRijVKsUYp1igXD6n8piQ8oXKShE7lNyXhiWKNUqxRijXKxcuS8CaVE5UuCZ3KSRLuSMKbVN5UrFGKNUqxRrn4mModSXhC5SQJnUqXhDtU7kjCl4o1SrFGKdYoF8MkoVM5ScIkxRqlWKMUa5SLYVS6JHQqJ0n4nxVrlGKNUqxRLj6WhN+UhE6lS0Kn8kQS/iXFGqVYoxRrlIuXqUym8i8r1ijFGqVYo1w8lIS/KQmdypuS8D8p1ijFGqVYo1x8TKVLwonKE0noVDqVLgmdSpeETuVNSXhTsUYp1ijFGsX84EUqXRJOVLokvEnlS0m4Q+UkCU8Ua5RijVKsUcwPPqTypST8JpWTJNyh0iXhiWKNUqxRijXKxUMqdyShUzlJQqfSqXRJ6FS6JLwpCZ1Kl4ROpUvCm4o1SrFGKdYo5gcfUumScKJyRxI6lTuS0Kl0SehU7khCp3KShCeKNUqxRinWKBcfS8IdSbhD5Y4kdCpdEjqVLgknKidJ6FTeVKxRijVKsUa5eEjlNyXhjiR0Kr9JpUvCl4o1SrFGKdYoFy9LwptU7kjCSRI6lU6lS8KJSpeEE5UvFWuUYo1SrFEuPqZyRxLuSEKn0iWhU+mS8EQSOpW/qVijFGuUYo1yMZzKHSpdEjqVLgl3JOFLxRqlWKMUa5SL/5zKl1ROVE6S0KmcJOGJYo1SrFGKNcrFx5LwpSR0Kl0STlS6JJyodEnoVDqVLglfKtYoxRqlWKNcvEzlX6JykoQTlROVJ1S6JDxRrFGKNUqxRjE/WGMUa5RijVKsUYo1SrFGKdYoxRqlWKMUa5RijVKsUYo1SrFGKdYoxRrlD3iZAgP0EjHgAAAAAElFTkSuQmCC"
/>
</div>
</div>
`;

View file

@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RoomFacePile /> renders 1`] = `
<DocumentFragment>
<div
aria-describedby="floating-ui-2"
aria-labelledby="floating-ui-1"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
>
<div
class="_stacked-avatars_mcap2_111"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="4"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 28px;"
>
b
</span>
</div>
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SearchWarning /> with desktop builds available renders with a logo by default 1`] = `
<DocumentFragment>
<div
class="mx_SearchWarning"
>
<img
alt=""
src="https://logo"
width="32px"
/>
<span>
<span>
Use the
<a
href="https://url"
rel="noreferrer noopener"
target="_blank"
>
Desktop app
</a>
to search encrypted messages
</span>
</span>
</div>
</DocumentFragment>
`;
exports[`<SearchWarning /> with desktop builds available renders without a logo when showLogo=false 1`] = `
<DocumentFragment>
<div
class="mx_SearchWarning"
>
<span>
<span>
Use the
<a
href="https://url"
rel="noreferrer noopener"
target="_blank"
>
Desktop app
</a>
to search encrypted messages
</span>
</span>
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SpellCheckLanguagesDropdown /> renders as expected 1`] = `
<DocumentFragment>
<div
class="mx_Dropdown mx_GeneralUserSettingsTab_spellCheckLanguageInput"
>
<div
aria-describedby="mx_LanguageDropdown_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Language Dropdown"
aria-owns="mx_LanguageDropdown_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
>
<div
class="mx_Dropdown_option"
id="mx_LanguageDropdown_value"
>
<div>
English
</div>
</div>
<span
class="mx_Dropdown_arrow"
/>
</div>
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,89 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<StyledRadioGroup /> renders radios correctly when no value is provided 1`] = `
<DocumentFragment>
<label
class="mx_StyledRadioButton test-class a-class mx_StyledRadioButton_enabled"
>
<input
aria-describedby="test-Anteater-description"
id="test-Anteater"
name="test"
type="radio"
value="Anteater"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
<span>
Anteater label
</span>
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<span
id="test-Anteater-description"
>
anteater description
</span>
<label
class="mx_StyledRadioButton test-class mx_StyledRadioButton_enabled"
>
<input
id="test-Badger"
name="test"
type="radio"
value="Badger"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
<span>
Badger label
</span>
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<label
class="mx_StyledRadioButton test-class mx_StyledRadioButton_enabled"
>
<input
aria-describedby="test-Canary-description"
id="test-Canary"
name="test"
type="radio"
value="Canary"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
<span>
Canary label
</span>
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<span
id="test-Canary-description"
>
<span>
Canary description
</span>
</span>
</DocumentFragment>
`;

View file

@ -0,0 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SyntaxHighlight /> renders 1`] = `
<div>
<pre
class="mx_SyntaxHighlight hljs language-arcade"
>
<code>
<span
class="hljs-built_in"
>
console
</span>
.
<span
class="hljs-built_in"
>
log
</span>
(
<span
class="hljs-string"
>
"Hello, World!"
</span>
);
</code>
</pre>
</div>
`;

View file

@ -0,0 +1,25 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { cleanup, render, waitFor } from "jest-matrix-react";
import React from "react";
import VerificationQRCode from "../../../../../../src/components/views/elements/crypto/VerificationQRCode";
describe("<VerificationQRCode />", () => {
afterEach(() => {
cleanup();
});
it("renders a QR code", async () => {
const { container, getAllByAltText } = render(<VerificationQRCode qrCodeBytes={Buffer.from("asd")} />);
// wait for the spinner to go away
await waitFor(() => getAllByAltText("QR Code").length === 1);
expect(container).toMatchSnapshot();
});
});

View file

@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<VerificationQRCode /> renders a QR code 1`] = `
<div>
<div
class="mx_QRCode mx_VerificationQRCode"
>
<img
alt="QR Code"
class="mx_VerificationQRCode"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMQAAADECAYAAADApo5rAAAAAklEQVR4AewaftIAAAPVSURBVO3BQQ4bRxAEwawG///lsiADfdqBQWpFkVZGpD8g6adB0hokrUHSGiStQdIaJK1B0hokrUHSGiStQdIaJK1B0hokrUHSGiStQdIaJK1B0hokrUHSGiStQdIaJK1B0hokrUHSevBmSfhWbXlWEk7acpKEZ7XlJAnfqi3vMkhag6Q1SFqDpDVIWg8+RFs+QRLeJQmvaMud2vIJkvCnDZLWIGkNktYgaQ2S1iBpPfgCSbhTW+6WhGe15SQJJ0m40pa7JeFObflkg6Q1SFqDpDVIWoOkNUhaD3SLtlxJwt3aot9jkLQGSWuQtAZJa5C0BknrgX6rtpwk4aQtJ0m40hb9mkHSGiStQdIaJK1B0nrwBdryf9SWT9eWv8kgaQ2S1iBpDZLWIGkNktaDD5GEb5aEK205ScJJW94lCfrXIGkNktYgaQ2S1iBpDZLWgzdry98kCSdtOUnCndqi/zZIWoOkNUhag6Q1SFqDpPXgzZJwp7acJOGTJeEVbbmShFe05U5JOGnLJxskrUHSGiStQdIaJK0Hb9aWK0k4actJEk7aciUJJ235Vm05ScKdknDSllck4Upb3mWQtAZJa5C0BklrkLQGSevBmyXhSlte0ZY7JeEVbbmShJO2vCIJV9py0pZXJOFOSfhkg6Q1SFqDpDVIWoOkNUhaD/7HknClLZ8gCSdtOWnLs5Jw0paTttypLZ9skLQGSWuQtAZJa5C0BknrwRdIwrsk4aQtd2rLK5JwpS13S8KVttwtCVfa8i6DpDVIWoOkNUhag6SV/oB+WRLepS1XknDSlpMknLTlTkl4VlveZZC0BklrkLQGSWuQtAZJ68GbJeFbteVZbTlJwrsk4V2S8K0GSWuQtAZJa5C0BklrkLQefIi2fIIkvEtbTpJwkoRnteUkCSdJeFZbXpGEP22QtAZJa5C0BklrkLQGSevBF0jCndryzdpyJQknSXhFW56VhJO2fLJB0hokrUHSGiStQdJ6oFu05UoSTtpy0pZnteUkCSdteZcknLTlTxskrUHSGiStQdIaJK1B0nqgWyThWUk4actJEq605aQtr0jClbactOUVSbjSlncZJK1B0hokrUHSGiStQdJ68AXa8q3acpKEV7TlWUk4actJW56VhFe05U8bJK1B0hokrUHSGiStQdJ68CGS8LdpyyuScKUtd0vClbbcLQlX2vIug6Q1SFqDpDVIWoOklf6ApJ8GSWuQtAZJa5C0BklrkLQGSWuQtAZJa5C0BklrkLQGSWuQtAZJa5C0BklrkLQGSWuQtAZJa5C0BklrkLQGSWuQtP4BohkVlic75PUAAAAASUVORK5CYII="
/>
</div>
</div>
`;