feat: show call participants in room list (Discord-style)
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled

This commit is contained in:
sorB
2026-05-10 14:25:35 +02:00
parent b797925316
commit 3da363517f
4610 changed files with 827237 additions and 1 deletions
@@ -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 OR LicenseRef-Element-Commercial
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();
});
});
});
@@ -0,0 +1,520 @@
/*
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 OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { Room, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { type IWidget, MatrixWidgetType } from "matrix-widget-api";
import { act, render, waitForElementToBeRemoved, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import {
type ApprovalOpts,
type 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 { clientAndSDKContextRenderOptions, 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 WidgetStore, { type IApp } from "../../../../../src/stores/WidgetStore";
import ActiveWidgetStore from "../../../../../src/stores/ActiveWidgetStore";
import AppTile from "../../../../../src/components/views/elements/AppTile";
import { type Container, WidgetLayoutStore } from "../../../../../src/stores/widgets/WidgetLayoutStore";
import AppsDrawer from "../../../../../src/components/views/rooms/AppsDrawer";
import { ElementWidgetCapabilities } from "../../../../../src/stores/widgets/ElementWidgetCapabilities";
import { ElementWidget, type WidgetMessaging } from "../../../../../src/stores/widgets/WidgetMessaging";
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
import { ModuleRunner } from "../../../../../src/modules/ModuleRunner";
import { ModuleApi } from "../../../../../src/modules/Api";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
jest.mock("../../../../../src/stores/OwnProfileStore", () => ({
OwnProfileStore: {
instance: {
on: jest.fn(),
isProfileInfoFetched: true,
removeListener: jest.fn(),
getHttpAvatarUrl: jest.fn().mockReturnValue("http://avatar_url"),
},
},
}));
const realGetValue = SettingsStore.getValue;
describe("AppTile", () => {
let cli: MatrixClient;
let sdkContext: SdkContextClass;
let r1: Room;
let r2: Room;
const resizeNotifier = new ResizeNotifier();
let app1: IApp;
let app2: IApp;
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 [];
});
});
afterAll(async () => {
jest.restoreAllMocks();
});
beforeEach(async () => {
// Do not carry across settings from previous tests
SettingsStore.reset();
sdkContext = new SdkContextClass();
// @ts-ignore
await WidgetMessagingStore.instance.onReady();
// 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();
});
afterEach(async () => {
jest.spyOn(SettingsStore, "getValue").mockRestore();
// @ts-ignore
await WidgetLayoutStore.instance.onNotReady();
// @ts-ignore
await RightPanelStore.instance.onNotReady();
});
it("destroys non-persisted right panel widget on room change", async () => {
// Set up right panel state
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>,
);
act(() =>
dis.dispatch({
action: Action.ViewRoom,
room_id: "r1",
}),
);
await expect(renderResult.findByText("Example 1")).resolves.toBeInTheDocument();
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
const { asFragment } = renderResult;
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
act(() =>
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);
});
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>,
);
act(() =>
dis.dispatch({
action: Action.ViewRoom,
room_id: "r1",
}),
);
await waitFor(() => {
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);
});
// Switch to room 2
act(() =>
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 waitFor(() => {
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: "top",
},
},
};
}
return null;
});
act(() => {
WidgetLayoutStore.instance.recalculateRoom(r1);
});
// Run initial render with room 1, and also running lifecycle methods
const renderResult = render(
<AppsDrawer userId={cli.getSafeUserId()} room={r1} />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
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, "center");
});
expect(renderResult.getByText("Example 1")).toBeInTheDocument();
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
});
describe("for a pinned widget", () => {
let moveToContainerSpy: jest.SpyInstance<void, [room: Room, widget: IWidget, toContainer: Container]>;
beforeEach(async () => {
moveToContainerSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
});
it("should render", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
const { asFragment } = renderResult;
expect(asFragment()).toMatchSnapshot(); // Take a snapshot of the pinned widget
});
it("should not display the »Popout widget« button", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
expect(renderResult.queryByLabelText("Popout widget")).not.toBeInTheDocument();
});
it("clicking 'minimise' should send the widget to the right", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
await userEvent.click(renderResult.getByLabelText("Minimise"));
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, "right");
});
it("clicking 'maximise' should send the widget to the center", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
await userEvent.click(renderResult.getByLabelText("Maximise"));
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, "center");
});
it("should render permission request", async () => {
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 { container, asFragment, queryByRole } = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
</MatrixClientContext.Provider>,
);
expect(container.querySelector(".mx_Spinner")).toBeFalsy();
expect(queryByRole("button", { name: "Continue" })).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("should not display 'Continue' button on permission load", async () => {
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>,
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
expect(renderResult.queryByRole("button", { name: "Continue" })).not.toBeInTheDocument();
});
it("should auto-approve preload via new widget lifecycle API", async () => {
// Legacy module API denies preload
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === app1.id) {
(opts as ApprovalOpts).approved = false;
}
});
// New API approves preload
jest.spyOn(ModuleApi.instance.widgetLifecycle, "preapprovePreload").mockResolvedValue(true);
// userId and creatorUserId are different so legacy path would show "Continue"
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
</MatrixClientContext.Provider>,
);
// The new API runs async in componentDidMount, so wait for it to take effect
await waitFor(() => {
expect(renderResult.queryByRole("button", { name: "Continue" })).not.toBeInTheDocument();
});
});
describe("for a maximised (centered) widget", () => {
beforeEach(() => {
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockImplementation(
(room: Room | null, widget: IWidget, container: Container) => {
return room === r1 && widget === app1 && container === "center";
},
);
});
afterEach(() => {
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockRestore();
});
it("clicking 'un-maximise' should send the widget to the top", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
await userEvent.click(renderResult.getByLabelText("Un-maximise"));
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, "top");
});
});
describe("with an existing widgetApi with requiresClient = false", () => {
beforeEach(() => {
const messaging = {
on: () => {},
off: () => {},
prepare: async () => {},
stop: () => {},
widgetApi: {
hasCapability: (capability: ElementWidgetCapabilities): boolean => {
return !(capability === ElementWidgetCapabilities.RequiresClient);
},
},
} as unknown as WidgetMessaging;
const mockWidget = new ElementWidget(app1);
WidgetMessagingStore.instance.storeMessaging(mockWidget, r1.roomId, messaging);
});
it("should display the »Popout widget« button", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
expect(renderResult.getByLabelText("Popout widget")).toBeInTheDocument();
});
});
});
describe("for a persistent app", () => {
it("should render", async () => {
const { asFragment, queryByRole } = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} fullWidth={true} miniMode={true} showMenubar={false} />
</MatrixClientContext.Provider>,
);
await waitForElementToBeRemoved(() => queryByRole("progressbar"));
expect(asFragment()).toMatchSnapshot();
});
});
});
@@ -0,0 +1,53 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { render } from "jest-matrix-react";
import React, { type ComponentProps } from "react";
import { afterEach } from "node:test";
import userEvent from "@testing-library/user-event";
import { BugReportDialogButton } from "../../../../../src/components/views/elements/BugReportDialogButton";
import SdkConfig from "../../../../../src/SdkConfig";
import Modal from "../../../../../src/Modal";
import BugReportDialog from "../../../../../src/components/views/dialogs/BugReportDialog";
import { BugReportEndpointURLLocal } from "../../../../../src/IConfigOptions";
describe("<BugReportDialogButton />", () => {
const getComponent = (props: ComponentProps<typeof BugReportDialogButton> = {}) =>
render(<BugReportDialogButton {...props} />);
afterEach(() => {
SdkConfig.reset();
jest.restoreAllMocks();
});
it("renders nothing if the bug reporter is disabled", () => {
SdkConfig.put({ bug_report_endpoint_url: undefined });
const { container } = getComponent({});
expect(container).toBeEmptyDOMElement();
});
it("renders 'submit' label if a URL is configured", () => {
SdkConfig.put({ bug_report_endpoint_url: "https://example.org" });
const { container } = getComponent({});
expect(container).toMatchSnapshot();
});
it("renders 'download' label if 'loca' is configured", () => {
SdkConfig.put({ bug_report_endpoint_url: BugReportEndpointURLLocal });
const { container } = getComponent({});
expect(container).toMatchSnapshot();
});
it("passes through props to dialog", async () => {
SdkConfig.put({ bug_report_endpoint_url: BugReportEndpointURLLocal });
const spy = jest.spyOn(Modal, "createDialog");
const { getByRole } = getComponent({ label: "a label", error: "an error" });
await userEvent.click(getByRole("button"));
expect(spy).toHaveBeenCalledWith(BugReportDialog, { error: "an error", label: "a label" });
});
});
@@ -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 OR LicenseRef-Element-Commercial
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 type 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]);
});
});
@@ -0,0 +1,92 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React, { type ReactElement } from "react";
import { render, screen, fireEvent } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import Dropdown from "../../../../../src/components/views/elements/Dropdown";
import type { NonEmptyArray } from "../../../../../src/@types/common";
describe("<Dropdown />", () => {
const placeholder = "Select an option";
const onOptionChange = jest.fn();
function renderDropdown(props?: Partial<React.ComponentProps<typeof Dropdown>>) {
return render(
<Dropdown
id="id"
label="Test Dropdown"
placeholder={placeholder}
onOptionChange={onOptionChange}
{...props}
>
{
[<div key="one">one</div>, <div key="two">two</div>, <div key="three">three</div>] as NonEmptyArray<
ReactElement & { key: string }
>
}
</Dropdown>,
);
}
afterEach(() => {
jest.clearAllMocks();
});
it("renders with placeholder", () => {
const { asFragment } = renderDropdown();
expect(screen.getByText(placeholder)).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
it("expands and collapses on click", async () => {
const user = userEvent.setup();
renderDropdown();
const button = screen.getByRole("button");
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
await user.click(button);
expect(screen.getByRole("listbox")).toBeInTheDocument();
// Collapse by clicking outside
await user.click(document.body);
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
});
it("calls onOptionChange when an option is selected", async () => {
renderDropdown();
await userEvent.click(screen.getByRole("button"));
const option = screen.getByRole("option", { name: "two" });
await userEvent.click(option);
expect(onOptionChange).toHaveBeenCalledWith("two");
});
it("handles keyboard navigation and selection", async () => {
renderDropdown();
const button = screen.getByRole("button");
await userEvent.click(button);
// Arrow down to "two"
fireEvent.keyDown(button, { key: "ArrowDown" });
expect(screen.getByRole("option", { name: "two" })).toHaveFocus();
// Arrow up to "one"
fireEvent.keyDown(button, { key: "ArrowUp" });
expect(screen.getByRole("option", { name: "one" })).toHaveFocus();
// Enter to select
fireEvent.keyDown(button, { key: "Enter" });
expect(onOptionChange).toHaveBeenCalledWith("one");
});
it("does not open when disabled", async () => {
renderDropdown({ disabled: true });
const button = screen.getByRole("button");
await userEvent.click(button);
expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
});
});
@@ -0,0 +1,50 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* 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));
});
});
@@ -0,0 +1,719 @@
/*
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 OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { type ComponentProps } from "react";
import { render, type RenderResult } from "jest-matrix-react";
import { type MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix";
import { KnownMembership, type 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");
expect(children).toMatchSnapshot();
});
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");
expect(children).toMatchSnapshot();
});
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");
expect(summary).toMatchSnapshot();
});
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");
expect(summary).toMatchSnapshot();
});
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");
// The sequence was summarised correctly
expect(summary).toHaveTextContent("user_1 was unbanned, joined and left 7 times and was invited");
// And there is no spoiler on the user's name since they were not banned
expect(summary).not.toContainHTML("mx_EventTile_spoiler_content");
expect(summary).toMatchSnapshot();
});
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");
// The sequence was summarised correctly
expect(summary).toHaveTextContent(
"user_1 was unbanned, joined and left 2 times, was banned, joined and left 3 times and was invited",
);
// And the banned user's name is hidden within a spoiler
expect(summary).toContainHTML('<span class="mx_EventTile_spoiler_content">user_1</span>');
expect(summary).toMatchSnapshot();
});
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");
// The sequence was summarised correctly
expect(summary).toHaveTextContent(
"user_1 and one other were unbanned, joined and left 2 times and were banned",
);
// And the banned user's name is hidden within a spoiler
expect(summary).toContainHTML('<span class="mx_EventTile_spoiler_content">user_1</span>');
expect(summary).toMatchSnapshot();
});
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");
// The sequence was summarised correctly
expect(summary).toHaveTextContent(
"user_0 and 19 others were unbanned, joined and left 2 times and were banned",
);
// And the banned user's name is hidden within a spoiler
expect(summary).toContainHTML('<span class="mx_EventTile_spoiler_content">user_0</span>');
expect(summary).toMatchSnapshot();
});
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",
);
expect(summary).toMatchSnapshot();
});
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",
);
expect(summary).toMatchSnapshot();
});
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",
);
expect(summary).toMatchSnapshot();
});
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");
expect(summary).toMatchSnapshot();
});
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");
expect(summary).toMatchSnapshot();
});
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");
expect(summary).toMatchSnapshot();
});
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");
expect(summary).toMatchSnapshot();
});
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");
expect(summary).toMatchSnapshot();
});
});
@@ -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 OR LicenseRef-Element-Commercial
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();
});
});
@@ -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 OR LicenseRef-Element-Commercial
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();
});
});
@@ -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 OR LicenseRef-Element-Commercial
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 'aria-live=assertive'
await expect(screen.findByRole("tooltip")).resolves.toHaveAttribute("aria-live", "assertive");
// Close the feedback is Escape is pressed
fireEvent.keyDown(screen.getByRole("textbox"), { key: "Escape" });
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
});
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 'aria-live=polite' role
await expect(screen.findByRole("tooltip")).resolves.toHaveAttribute("aria-live", "polite");
// Close the feedback is Escape is pressed
fireEvent.keyDown(screen.getByRole("textbox"), { key: "Escape" });
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
});
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();
});
});
});
@@ -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 OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { act, fireEvent, render } from "jest-matrix-react";
import React, { type JSX } 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();
});
});
@@ -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 OR LicenseRef-Element-Commercial
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);
});
});
@@ -0,0 +1,119 @@
/*
* 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 OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { mocked } from "jest-mock";
import { render, fireEvent, waitFor } from "jest-matrix-react";
import fetchMock from "@fetch-mock/jest";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import ImageView from "../../../../../src/components/views/elements/ImageView";
import { FileDownloader } from "../../../../../src/utils/FileDownloader";
import Modal from "../../../../../src/Modal";
import ErrorDialog from "../../../../../src/components/views/dialogs/ErrorDialog";
import { stubClient } from "../../../../test-utils";
jest.mock("../../../../../src/utils/FileDownloader");
describe("<ImageView />", () => {
beforeEach(() => {
jest.resetAllMocks();
});
it("renders correctly", () => {
const { container } = render(<ImageView src="https://example.com/image.png" onFinished={jest.fn()} />);
expect(container).toMatchSnapshot();
});
it("should download on click", async () => {
fetchMock.get("https://example.com/image.png", "TESTFILE");
const { getByRole } = render(
<ImageView src="https://example.com/image.png" name="filename.png" onFinished={jest.fn()} />,
);
fireEvent.click(getByRole("button", { name: "Download" }));
await waitFor(() =>
expect(mocked(FileDownloader).mock.instances[0].download).toHaveBeenCalledWith({
blob: expect.anything(),
name: "filename.png",
}),
);
expect(fetchMock).toHaveFetched("https://example.com/image.png");
});
it("should use event as download source if given", async () => {
stubClient();
const event = new MatrixEvent({
event_id: "$eventId",
type: "m.image",
content: {
body: "fromEvent.png",
url: "mxc://test.dummy/fromEvent.png",
file_name: "filename.png",
},
origin_server_ts: new Date(2000, 0, 1, 0, 0, 0, 0).getTime(),
});
fetchMock.get("http://this.is.a.url/test.dummy/fromEvent.png", "TESTFILE");
const { getByRole } = render(
<ImageView
src="https://test.dummy/fromSrc.png"
name="fromName.png"
onFinished={jest.fn()}
mxEvent={event}
/>,
);
fireEvent.click(getByRole("button", { name: "Download" }));
await waitFor(() =>
expect(mocked(FileDownloader).mock.instances[0].download).toHaveBeenCalledWith({
blob: expect.anything(),
name: "fromEvent.png",
}),
);
expect(fetchMock).toHaveFetched("http://this.is.a.url/test.dummy/fromEvent.png");
});
it("should start download on Ctrl+S", async () => {
fetchMock.get("https://example.com/image.png", "TESTFILE");
const { container } = render(
<ImageView src="https://example.com/image.png" name="filename.png" onFinished={jest.fn()} />,
);
const dialog = container.querySelector('[role="dialog"]') as HTMLElement;
dialog?.focus();
fireEvent.keyDown(dialog!, { key: "s", code: "KeyS", ctrlKey: true });
await waitFor(() => {
expect(mocked(FileDownloader).mock.instances[0].download).toHaveBeenCalledWith({
blob: expect.anything(),
name: "filename.png",
});
});
expect(fetchMock).toHaveFetched("https://example.com/image.png");
});
it("should handle download errors", async () => {
const modalSpy = jest.spyOn(Modal, "createDialog");
fetchMock.get("https://example.com/image.png", { status: 500 });
const { getByRole } = render(
<ImageView src="https://example.com/image.png" name="filename.png" onFinished={jest.fn()} />,
);
fireEvent.click(getByRole("button", { name: "Download" }));
await waitFor(() =>
expect(modalSpy).toHaveBeenCalledWith(
ErrorDialog,
expect.objectContaining({
title: "Download failed",
}),
),
);
});
});
@@ -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 OR LicenseRef-Element-Commercial
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();
});
});
@@ -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 OR LicenseRef-Element-Commercial
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);
});
});
@@ -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 OR LicenseRef-Element-Commercial
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,
});
});
});
@@ -0,0 +1,40 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
import MiniAvatarUploader from "../../../../../src/components/views/elements/MiniAvatarUploader.tsx";
import { stubClient, withClientContextRenderOptions } from "../../../../test-utils";
const BASE64_GIF = "R0lGODlhAQABAAAAACw=";
const AVATAR_FILE = new File([Uint8Array.from(atob(BASE64_GIF), (c) => c.charCodeAt(0))], "avatar.gif", {
type: "image/gif",
});
describe("<MiniAvatarUploader />", () => {
it("calls setAvatarUrl when a file is uploaded", async () => {
const cli = stubClient();
mocked(cli.uploadContent).mockResolvedValue({ content_uri: "mxc://example.com/1234" });
const setAvatarUrl = jest.fn();
const user = userEvent.setup();
const { container, findByLabelText } = render(
<MiniAvatarUploader hasAvatar={false} noAvatarLabel="Upload" setAvatarUrl={setAvatarUrl} isUserAvatar />,
withClientContextRenderOptions(cli),
);
await findByLabelText("Upload");
await user.upload(container.querySelector("input")!, AVATAR_FILE);
expect(cli.uploadContent).toHaveBeenCalledWith(AVATAR_FILE);
expect(setAvatarUrl).toHaveBeenCalledWith("mxc://example.com/1234");
});
});
@@ -0,0 +1,288 @@
/*
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 OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, type RenderResult, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { mocked, type Mocked } from "jest-mock";
import { type MatrixClient, type MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import dis from "../../../../../src/dispatcher/dispatcher";
import { Pill, type PillProps, PillType } from "../../../../../src/components/views/elements/Pill";
import {
filterConsole,
flushPromises,
mkMessage,
mkRoomCanonicalAliasEvent,
mkRoomMemberJoinEvent,
stubClient,
withClientContextRenderOptions,
} from "../../../../test-utils";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { Action } from "../../../../../src/dispatcher/actions";
import { type ButtonEvent } from "../../../../../src/components/views/elements/AccessibleButton";
import { SDKContext, SdkContextClass } from "../../../../../src/contexts/SDKContext";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg.ts";
import { TestSdkContext } from "../../../TestSdkContext.ts";
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 cli = MatrixClientPeg.safeGet();
const mockSdkContext = new TestSdkContext();
mockSdkContext.client = cli;
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
<SDKContext.Provider value={mockSdkContext}>
<div onClick={pillParentClickHandler}>
<Pill {...withDefault} />
</div>
</SDKContext.Provider>,
withClientContextRenderOptions(cli),
);
};
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], { addToState: true });
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 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 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>
`);
});
});
@@ -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 OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, type 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 { type 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");
}
}
@@ -0,0 +1,91 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, screen } from "jest-matrix-react";
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 = Promise.withResolvers<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);
});
});
@@ -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 OR LicenseRef-Element-Commercial
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);
});
});
@@ -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 OR LicenseRef-Element-Commercial
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();
});
});
@@ -0,0 +1,62 @@
/*
Copyright 2025 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, waitFor } from "jest-matrix-react";
import ReplyChain from "../../../../../src/components/views/elements/ReplyChain.tsx";
import { mkEvent, stubClient, withClientContextRenderOptions } from "../../../../test-utils";
describe("ReplyChain", () => {
it("should call setQuoteExpanded if chain is longer than 2 lines", async () => {
// Jest/JSDOM won't set clientHeight/scrollHeight for us so we have to synthesise it
jest.spyOn(Element.prototype, "clientHeight", "get").mockReturnValue(100);
jest.spyOn(Element.prototype, "scrollHeight", "get").mockReturnValue(150);
const cli = stubClient();
const { room_id: roomId } = await cli.createRoom({});
const room = cli.getRoom(roomId)!;
const targetEv = mkEvent({
event: true,
type: "m.room.message",
user: cli.getUserId()!,
room: roomId,
id: "$event1",
content: {
body: "A\nB\nC",
msgtype: "m.text",
},
});
jest.spyOn(room, "findEventById").mockReturnValue(targetEv);
const parentEv = mkEvent({
event: true,
type: "m.room.message",
user: cli.getUserId()!,
room: roomId,
id: "$event2",
content: {
"body": "Reply",
"msgtype": "m.text",
"m.relates_to": {
"m.in_reply_to": {
event_id: "$event1",
},
},
},
});
const setQuoteExpanded = jest.fn();
const { asFragment } = render(
<ReplyChain parentEv={parentEv} setQuoteExpanded={setQuoteExpanded} />,
withClientContextRenderOptions(cli),
);
await waitFor(() => expect(setQuoteExpanded).toHaveBeenCalledWith(false));
expect(asFragment()).toMatchSnapshot();
});
});
@@ -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 OR LicenseRef-Element-Commercial
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();
});
});
@@ -0,0 +1,110 @@
/*
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 OR LicenseRef-Element-Commercial
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 { LinkedTextContext } from "@element-hq/web-shared-components";
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], { addToState: true });
return room;
}
/**
* Create a room and render it
* @param topic
*/
const renderRoom = (topic: string) => {
const room = createRoom(topic);
render(<RoomTopic room={room} />, {
wrapper: ({ children }) => <LinkedTextContext.Provider value={{}}>{children}</LinkedTextContext.Provider>,
});
};
/**
* 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());
});
});
@@ -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 OR LicenseRef-Element-Commercial
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("presentation")).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();
});
});
});
@@ -0,0 +1,37 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen } from "jest-matrix-react";
import { SettingLevel } from "../../../../../src/settings/SettingLevel.ts";
import SettingsDropdown from "../../../../../src/components/views/elements/SettingsDropdown.tsx";
describe("<SettingsDropdown />", () => {
it("should render a disabled setting", async () => {
const { asFragment } = render(
<SettingsDropdown settingKey="Electron.autoLaunch" level={SettingLevel.PLATFORM} />,
);
expect(asFragment()).toMatchSnapshot();
const trigger = screen.getByRole("button");
expect(trigger).toHaveTextContent("No");
expect(trigger).toHaveAttribute("aria-disabled", "true");
});
it("should not render a disabled setting if hideIfCannotSet=true", async () => {
const { container } = render(
<SettingsDropdown settingKey="Electron.autoLaunch" level={SettingLevel.PLATFORM} hideIfCannotSet />,
);
expect(container).toBeEmptyDOMElement();
});
it("should not render a non-options setting", async () => {
const { container } = render(<SettingsDropdown settingKey="systemFont" level={SettingLevel.DEVICE} />);
expect(container).toBeEmptyDOMElement();
});
});
@@ -0,0 +1,36 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import SettingsField from "../../../../../src/components/views/elements/SettingsField";
import { SettingLevel } from "../../../../../src/settings/SettingLevel.ts";
describe("<SettingsField />", () => {
it("should render with the default label", () => {
const component = render(<SettingsField settingKey="Developer.elementCallUrl" level={SettingLevel.DEVICE} />);
expect(screen.getByText("Element Call URL")).toBeTruthy();
expect(component.asFragment()).toMatchSnapshot();
});
it("should call onChange when saving a change", async () => {
const fn = jest.fn();
render(<SettingsField settingKey="Developer.elementCallUrl" level={SettingLevel.DEVICE} onChange={fn} />);
const input = screen.getByRole("textbox");
await userEvent.type(input, "https://call.element.dev");
expect(input).toHaveValue("https://call.element.dev");
screen.getByLabelText("Save").click();
await waitFor(() => {
expect(fn).toHaveBeenCalledWith("https://call.element.dev");
});
});
});
@@ -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 OR LicenseRef-Element-Commercial
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();
});
});
@@ -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 OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, render, type 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);
});
});
@@ -0,0 +1,31 @@
// 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 OR LicenseRef-Element-Commercial
// 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 language="javascript">console.log("Hello, World!");</SyntaxHighlight>,
);
await waitFor(() => expect(container.querySelector(".language-javascript")).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 [, opts] = mock.mock.lastCall!;
expect((opts as unknown as HighlightOptions)["language"]).toBe(lang);
});
});
@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
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>
`;
@@ -0,0 +1,560 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`AppTile destroys non-persisted right panel widget on room change 1`] = `
<DocumentFragment>
<aside
class="mx_RightPanel"
data-testid="right-panel"
id="mx_RightPanel"
>
<div
class="mx_BaseCard mx_WidgetCard"
>
<div
class="mx_BaseCard_header"
>
<div
class="mx_BaseCard_header_title"
>
<h1
class="mx_Heading_h4 mx_BaseCard_header_title_heading"
>
Example 1
</h1>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_BaseCard_header_title_button--option"
data-state="closed"
id="radix-_r_0_"
role="button"
tabindex="0"
type="button"
/>
</div>
<button
aria-labelledby="_r_2_"
class="_icon-button_1215g_8"
data-kind="secondary"
data-testid="base-card-close-button"
role="button"
style="--cpd-icon-button-size: 28px;"
tabindex="0"
>
<div
class="_indicator-icon_147l5_17"
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.414"
/>
</svg>
</div>
</button>
</div>
<div
class="mx_AppTileFullWidth"
id="1"
>
<div
class="mx_AppTile_persistedWrapper"
>
<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_va14e_8 mx_BaseAvatar mx_WidgetAvatar"
data-color="1"
data-testid="avatar-img"
data-type="round"
role="img"
style="--cpd-avatar-size: 20px;"
>
<img
alt=""
class="_image_va14e_43"
data-type="round"
height="20px"
loading="lazy"
referrerpolicy="no-referrer"
src="image-file-stub"
width="20px"
/>
</span>
<h1>
Example 1
</h1>
<span />
</span>
</span>
<span
class="mx_AppTileMenuBar_widgets"
>
<div
aria-label="Maximise"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
>
<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="M21 3.997a1 1 0 0 0-.29-.702l-.005-.004A1 1 0 0 0 20 3h-8a1 1 0 1 0 0 2h5.586L5 17.586V12a1 1 0 1 0-2 0v8.003a1 1 0 0 0 .29.702l.005.004c.18.18.43.291.705.291h8a1 1 0 1 0 0-2H6.414L19 6.414V12a1 1 0 1 0 2 0z"
/>
</svg>
</div>
<div
aria-label="Minimise"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
>
<svg
class="mx_Icon mx_Icon_16"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 13a.97.97 0 0 1-.713-.287A.97.97 0 0 1 5 12q0-.424.287-.713A.97.97 0 0 1 6 11h12q.424 0 .712.287.288.288.288.713 0 .424-.288.713A.97.97 0 0 1 18 13z"
/>
</svg>
</div>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
data-state="closed"
id="radix-_r_1k_"
role="button"
tabindex="0"
type="button"
>
<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="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
/>
</svg>
</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_va14e_8 mx_BaseAvatar mx_WidgetAvatar"
data-color="1"
data-testid="avatar-img"
data-type="round"
role="img"
style="--cpd-avatar-size: 20px;"
>
<img
alt=""
class="_image_va14e_43"
data-type="round"
height="20px"
loading="lazy"
referrerpolicy="no-referrer"
src="image-file-stub"
width="20px"
/>
</span>
<h1>
Example 1
</h1>
<span />
</span>
</span>
<span
class="mx_AppTileMenuBar_widgets"
>
<div
aria-label="Maximise"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
>
<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="M21 3.997a1 1 0 0 0-.29-.702l-.005-.004A1 1 0 0 0 20 3h-8a1 1 0 1 0 0 2h5.586L5 17.586V12a1 1 0 1 0-2 0v8.003a1 1 0 0 0 .29.702l.005.004c.18.18.43.291.705.291h8a1 1 0 1 0 0-2H6.414L19 6.414V12a1 1 0 1 0 2 0z"
/>
</svg>
</div>
<div
aria-label="Minimise"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
>
<svg
class="mx_Icon mx_Icon_16"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 13a.97.97 0 0 1-.713-.287A.97.97 0 0 1 5 12q0-.424.287-.713A.97.97 0 0 1 6 11h12q.424 0 .712.287.288.288.288.713 0 .424-.288.713A.97.97 0 0 1 18 13z"
/>
</svg>
</div>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
data-state="closed"
id="radix-_r_30_"
role="button"
tabindex="0"
type="button"
>
<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="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
/>
</svg>
</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_va14e_8 mx_BaseAvatar _avatar-imageless_va14e_55"
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="_r_33_"
aria-labelledby="_r_32_"
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.439q-.183.188-.352.355c-.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 8m1 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0"
/>
<path
d="M8.1 21.213A9.7 9.7 0 0 0 12 22a9.7 9.7 0 0 0 3.9-.788 10.1 10.1 0 0 0 3.175-2.137q1.35-1.35 2.137-3.175A9.7 9.7 0 0 0 22 12a9.7 9.7 0 0 0-.788-3.9 10.1 10.1 0 0 0-2.137-3.175q-1.35-1.35-3.175-2.137A9.7 9.7 0 0 0 12 2a9.7 9.7 0 0 0-3.9.788 10.1 10.1 0 0 0-3.175 2.137Q3.575 6.275 2.788 8.1A9.7 9.7 0 0 0 2 12q0 2.075.788 3.9a10.1 10.1 0 0 0 2.137 3.175q1.35 1.35 3.175 2.137m9.575-3.538Q15.35 20 12 20t-5.675-2.325T4 12t2.325-5.675T12 4t5.675 2.325T20 12t-2.325 5.675"
/>
</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_va14e_8 mx_BaseAvatar mx_WidgetAvatar"
data-color="1"
data-testid="avatar-img"
data-type="round"
role="img"
style="--cpd-avatar-size: 20px;"
>
<img
alt=""
class="_image_va14e_43"
data-type="round"
height="20px"
loading="lazy"
referrerpolicy="no-referrer"
src="image-file-stub"
width="20px"
/>
</span>
<h1>
Example 1
</h1>
<span />
</span>
</span>
<span
class="mx_AppTileMenuBar_widgets"
>
<div
aria-label="Maximise"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
>
<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="M21 3.997a1 1 0 0 0-.29-.702l-.005-.004A1 1 0 0 0 20 3h-8a1 1 0 1 0 0 2h5.586L5 17.586V12a1 1 0 1 0-2 0v8.003a1 1 0 0 0 .29.702l.005.004c.18.18.43.291.705.291h8a1 1 0 1 0 0-2H6.414L19 6.414V12a1 1 0 1 0 2 0z"
/>
</svg>
</div>
<div
aria-label="Minimise"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
role="button"
tabindex="0"
>
<svg
class="mx_Icon mx_Icon_16"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 13a.97.97 0 0 1-.713-.287A.97.97 0 0 1 5 12q0-.424.287-.713A.97.97 0 0 1 6 11h12q.424 0 .712.287.288.288.288.713 0 .424-.288.713A.97.97 0 0 1 18 13z"
/>
</svg>
</div>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
data-state="closed"
id="radix-_r_10_"
role="button"
tabindex="0"
type="button"
>
<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="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
/>
</svg>
</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>
 
<svg
aria-label="Loading…"
class="_icon_1855a_18"
data-testid="spinner"
fill="currentColor"
height="1em"
role="progressbar"
style="width: 32px; height: 32px;"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M12 4.031a8 8 0 1 0 8 8 1 1 0 0 1 2 0c0 5.523-4.477 10-10 10s-10-4.477-10-10 4.477-10 10-10a1 1 0 1 1 0 2"
fill-rule="evenodd"
/>
</svg>
</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>
`;
@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<BugReportDialogButton /> renders 'download' label if 'loca' is configured 1`] = `
<div>
<button
class="_button_1nw83_8"
data-kind="secondary"
data-size="md"
role="button"
tabindex="0"
>
Download logs
</button>
</div>
`;
exports[`<BugReportDialogButton /> renders 'submit' label if a URL is configured 1`] = `
<div>
<button
class="_button_1nw83_8"
data-kind="secondary"
data-size="md"
role="button"
tabindex="0"
>
Submit debug logs
</button>
</div>
`;
@@ -0,0 +1,39 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<Dropdown /> renders with placeholder 1`] = `
<DocumentFragment>
<div
class="mx_Dropdown"
>
<div
aria-describedby="id_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Test Dropdown"
aria-owns="id_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
>
<div
class="mx_Dropdown_option"
id="id_value"
>
Select an option
</div>
<svg
class="mx_Dropdown_arrow"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</DocumentFragment>
`;
@@ -0,0 +1,12 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
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>
`;
@@ -0,0 +1,272 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`EventListSummary correctly identifies transitions 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
<span>
<button
class="mx_EventTile_spoiler"
>
 
<span
class="mx_EventTile_spoiler_content"
>
user_1
</span>
</button>
was invited, was banned, joined, rejected their invitation, left, had their invitation withdrawn, was unbanned, was removed, left and was removed
</span>
</span>
`;
exports[`EventListSummary correctly orders sequences of transitions by the order of their first event 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
<span>
user_2
was unbanned and joined and left 2 times
</span>
,
<span>
<button
class="mx_EventTile_spoiler"
>
 
<span
class="mx_EventTile_spoiler_content"
>
user_1
</span>
</button>
was unbanned, joined and left 2 times and was banned
</span>
</span>
`;
exports[`EventListSummary handles a summary length = 2, with 1 "other" 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
<span>
<span>
user_1
,
user_2
and one other
</span>
joined
</span>
</span>
`;
exports[`EventListSummary handles a summary length = 2, with many "others" 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
<span>
<span>
user_0
,
user_1
and 18 others
</span>
joined
</span>
</span>
`;
exports[`EventListSummary handles a summary length = 2, with no "others" 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
<span>
user_1
and
user_2
joined 2 times
</span>
</span>
`;
exports[`EventListSummary handles invitation plurals correctly when there are multiple invites 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
<span>
user_1
rejected their invitation 2 times
</span>
</span>
`;
exports[`EventListSummary handles invitation plurals correctly when there are multiple users 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
<span>
<span>
user_1
and one other
</span>
rejected their invitations and had their invitations withdrawn
</span>
</span>
`;
exports[`EventListSummary handles many users following the same sequence of memberships 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
<span>
<span>
<button
class="mx_EventTile_spoiler"
>
 
<span
class="mx_EventTile_spoiler_content"
>
user_0
</span>
</button>
and 19 others
</span>
were unbanned, joined and left 2 times and were banned
</span>
</span>
`;
exports[`EventListSummary handles multiple users following the same sequence of memberships 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
<span>
<span>
<button
class="mx_EventTile_spoiler"
>
 
<span
class="mx_EventTile_spoiler_content"
>
user_1
</span>
</button>
and one other
</span>
were unbanned, joined and left 2 times and were banned
</span>
</span>
`;
exports[`EventListSummary renders collapsed events if events.length = props.threshold 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
<span>
user_1
joined and left and joined
</span>
</span>
`;
exports[`EventListSummary renders expanded events if there are less than props.threshold 1`] = `
HTMLCollection [
<div
class="event_tile"
>
Expanded membership
</div>,
]
`;
exports[`EventListSummary renders expanded events if there are less than props.threshold for join and leave 1`] = `
HTMLCollection [
<div
class="event_tile"
>
Expanded membership
</div>,
<div
class="event_tile"
>
Expanded membership
</div>,
]
`;
exports[`EventListSummary should not blindly group 3pid invites and treat them as distinct users instead 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
<span>
n...@d...
was invited 2 times
</span>
,
<span>
d...@w...
was invited
</span>
</span>
`;
exports[`EventListSummary truncates long join,leave repetitions 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
<span>
user_1
joined and left 7 times
</span>
</span>
`;
exports[`EventListSummary truncates long join,leave repetitions between other events 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
<span>
user_1
was unbanned, joined and left 7 times and was invited
</span>
</span>
`;
exports[`EventListSummary truncates multiple sequences of repetitions with other events between 1`] = `
<span
class="mx_TextualEvent mx_GenericEventListSummary_summary"
>
<span>
<button
class="mx_EventTile_spoiler"
>
 
<span
class="mx_EventTile_spoiler_content"
>
user_1
</span>
</button>
was unbanned, joined and left 2 times, was banned, joined and left 3 times and was invited
</span>
</span>
`;
@@ -0,0 +1,64 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
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>
<svg
class="mx_ExternalLink_icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
/>
<path
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
/>
</svg>
</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
<svg
class="mx_ExternalLink_icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 3h6a1 1 0 1 1 0 2H5v14h14v-6a1 1 0 1 1 2 0v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2"
/>
<path
d="M15 3h5a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0V6.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L17.586 5H15a1 1 0 1 1 0-2"
/>
</svg>
</a>
</DocumentFragment>
`;
@@ -0,0 +1,27 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<FacePile /> renders with a tooltip 1`] = `
<DocumentFragment>
<div
aria-labelledby="_r_0_"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
>
<div
class="_stacked-avatars_va14e_109"
>
<span
class="_avatar_va14e_8 mx_BaseAvatar _avatar-imageless_va14e_55"
data-color="4"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 36px;"
>
4
</span>
</div>
</div>
</DocumentFragment>
`;
@@ -0,0 +1,175 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
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"
tabindex="-1"
>
<div
class="mx_FilterDropdown_option"
data-testid="filter-option-one"
>
<svg
class="mx_FilterDropdown_optionSelectedIcon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
<span
class="mx_FilterDropdown_optionLabel"
>
Option one
</span>
</div>
</li>
<li
aria-selected="false"
class="mx_Dropdown_option"
id="test__two"
role="option"
tabindex="-1"
>
<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>
<svg
class="mx_Dropdown_arrow"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</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>
<svg
class="mx_Dropdown_arrow"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</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"
/>
<svg
class="mx_Dropdown_arrow"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</div>
`;
@@ -0,0 +1,48 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
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>
`;
@@ -0,0 +1,196 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
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="_r_2_"
aria-label="Zoom out"
class="mx_AccessibleButton mx_ImageView_button"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.5 9.5q.425 0 .713.287.288.288.287.713a.97.97 0 0 1-.287.713.97.97 0 0 1-.713.287h-6a.97.97 0 0 1-.713-.287.97.97 0 0 1-.287-.713q0-.425.287-.713A.97.97 0 0 1 7.5 9.5z"
/>
<path
clip-rule="evenodd"
d="M10.5 3a7.5 7.5 0 0 1 5.963 12.049l3.244 3.244a1 1 0 1 1-1.414 1.414l-3.244-3.244A7.5 7.5 0 1 1 10.5 3m0 2a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11"
fill-rule="evenodd"
/>
</svg>
</div>
<div
aria-label="Zoom in"
class="mx_AccessibleButton mx_ImageView_button"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#cpd_ZoomInIcon_a)"
>
<path
d="M10.5 6.5q.425 0 .713.287.288.288.287.713v2h2q.425 0 .713.287.288.288.287.713a.97.97 0 0 1-.287.713.97.97 0 0 1-.713.287h-2v2a.97.97 0 0 1-.287.713.97.97 0 0 1-.713.287.97.97 0 0 1-.713-.287.97.97 0 0 1-.287-.713v-2h-2a.97.97 0 0 1-.713-.287.97.97 0 0 1-.287-.713q0-.425.287-.713A.97.97 0 0 1 7.5 9.5h2v-2q0-.425.287-.713A.97.97 0 0 1 10.5 6.5"
/>
<path
clip-rule="evenodd"
d="M10.5 3a7.5 7.5 0 0 1 5.963 12.049l3.244 3.244a1 1 0 1 1-1.414 1.414l-3.244-3.244A7.5 7.5 0 1 1 10.5 3m0 2a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11"
fill-rule="evenodd"
/>
<path
d="M15.05 16.463a7.5 7.5 0 1 1 1.414-1.414l3.243 3.244a1 1 0 0 1-1.414 1.414zM16 10.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0"
/>
<path
d="M7.875 11.375h1.75v1.75q0 .372.252.623A.85.85 0 0 0 10.5 14a.85.85 0 0 0 .623-.252.85.85 0 0 0 .252-.623v-1.75h1.75a.85.85 0 0 0 .623-.252A.85.85 0 0 0 14 10.5a.85.85 0 0 0-.252-.623.85.85 0 0 0-.623-.252h-1.75v-1.75a.85.85 0 0 0-.252-.623A.85.85 0 0 0 10.5 7a.85.85 0 0 0-.623.252.85.85 0 0 0-.252.623v1.75h-1.75a.85.85 0 0 0-.623.252A.85.85 0 0 0 7 10.5q0 .372.252.623a.85.85 0 0 0 .623.252"
/>
</g>
<defs>
<clippath
id="cpd_ZoomInIcon_a"
>
<path
d="M0 0h24v24H0z"
/>
</clippath>
</defs>
</svg>
</div>
<div
aria-label="Rotate Left"
class="mx_AccessibleButton mx_ImageView_button"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.56 7.98C6.1 7.52 5.31 7.6 5 8.17c-.28.51-.5 1.03-.67 1.58-.19.63.31 1.25.96 1.25h.01c.43 0 .82-.28.94-.7q.18-.6.48-1.17c.22-.37.15-.84-.16-1.15M5.31 13h-.02c-.65 0-1.15.62-.96 1.25.16.54.38 1.07.66 1.58.31.57 1.11.66 1.57.2.3-.31.38-.77.17-1.15-.2-.37-.36-.76-.48-1.16a.97.97 0 0 0-.94-.72m2.85 6.02q.765.42 1.59.66c.62.18 1.24-.32 1.24-.96v-.03c0-.43-.28-.82-.7-.94-.4-.12-.78-.28-1.15-.48a.97.97 0 0 0-1.16.17l-.03.03c-.45.45-.36 1.24.21 1.55M13 4.07v-.66c0-.89-1.08-1.34-1.71-.71L9.17 4.83c-.4.4-.4 1.04 0 1.43l2.13 2.08c.63.62 1.7.17 1.7-.72V6.09c2.84.48 5 2.94 5 5.91 0 2.73-1.82 5.02-4.32 5.75a.97.97 0 0 0-.68.94v.02c0 .65.61 1.14 1.23.96A7.976 7.976 0 0 0 20 12c0-4.08-3.05-7.44-7-7.93"
/>
</svg>
</div>
<div
aria-label="Rotate Right"
class="mx_AccessibleButton mx_ImageView_button"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#cpd_RotateRightIcon_a)"
>
<path
d="M14.83 4.83 12.7 2.7c-.62-.62-1.7-.18-1.7.71v.66C7.06 4.56 4 7.92 4 12c0 3.64 2.43 6.71 5.77 7.68.62.18 1.23-.32 1.23-.96v-.03a.97.97 0 0 0-.68-.94A5.98 5.98 0 0 1 6 12c0-2.97 2.16-5.43 5-5.91v1.53c0 .89 1.07 1.33 1.7.71l2.13-2.08a.99.99 0 0 0 0-1.42m4.84 4.93q-.24-.825-.66-1.59c-.31-.57-1.1-.66-1.56-.2l-.01.01c-.31.31-.38.78-.17 1.16.2.37.36.76.48 1.16.12.42.51.7.94.7h.02c.65 0 1.15-.62.96-1.24M13 18.68v.02c0 .65.62 1.14 1.24.96q.825-.24 1.59-.66c.57-.31.66-1.1.2-1.56l-.02-.02a.97.97 0 0 0-1.16-.17c-.37.21-.76.37-1.16.49-.41.12-.69.51-.69.94m4.44-2.65c.46.46 1.25.37 1.56-.2.28-.51.5-1.04.67-1.59.18-.62-.31-1.24-.96-1.24h-.02c-.44 0-.82.28-.94.7q-.18.6-.48 1.17c-.21.38-.13.86.17 1.16"
/>
</g>
<defs>
<clippath
id="cpd_RotateRightIcon_a"
>
<path
d="M0 0h24v24H0z"
/>
</clippath>
</defs>
</svg>
</div>
<div
aria-label="Download"
class="mx_AccessibleButton mx_ImageView_button"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 15.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-3.6-3.6a.95.95 0 0 1-.275-.7q0-.425.275-.7.274-.275.712-.288t.713.263L11 12.15V5q0-.424.287-.713A.97.97 0 0 1 12 4q.424 0 .713.287Q13 4.576 13 5v7.15l1.875-1.875q.274-.274.713-.263.437.014.712.288a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-3.6 3.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063M6 20q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 18v-2q0-.424.287-.713A.97.97 0 0 1 5 15q.424 0 .713.287Q6 15.576 6 16v2h12v-2q0-.424.288-.713A.97.97 0 0 1 19 15q.424 0 .712.287.288.288.288.713v2q0 .824-.587 1.413A1.93 1.93 0 0 1 18 20z"
/>
</svg>
</div>
<div
aria-label="Close"
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_close"
role="button"
tabindex="0"
>
<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.414"
/>
</svg>
</div>
</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>
`;
@@ -0,0 +1,34 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`InfoTooltip should show tooltip on hover 1`] = `
<DocumentFragment>
<div
aria-describedby="_r_2_"
class="mx_InfoTooltip"
tabindex="0"
>
<span
aria-label="Information"
class="mx_InfoTooltip_icon"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.288 7.288A.97.97 0 0 1 12 7q.424 0 .713.287Q13 7.576 13 8t-.287.713A.97.97 0 0 1 12 9a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 8q0-.424.287-.713m.001 4.001A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713v4q0 .424-.287.712A.97.97 0 0 1 12 17a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 16v-4q0-.424.287-.713"
/>
<path
clip-rule="evenodd"
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2s10 4.477 10 10m-2 0a8 8 0 1 1-16 0 8 8 0 0 1 16 0"
fill-rule="evenodd"
/>
</svg>
</span>
Trigger text
</div>
</DocumentFragment>
`;
@@ -0,0 +1,130 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<LabelledCheckbox /> should render with byline of "this is a byline" 1`] = `
<DocumentFragment>
<div
class="mx_LabelledCheckbox"
>
<form
class="_root_19upo_16"
>
<div
class="_inline-field_19upo_32"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_153f2_10"
>
<input
aria-describedby="_r_4_"
checked=""
class="_input_153f2_18"
id="checkbox_vY7Q4uEh9K"
type="checkbox"
/>
<div
class="_ui_153f2_19"
>
<svg
aria-hidden="true"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="checkbox_vY7Q4uEh9K"
>
<span
class="mx_LabelledCheckbox_label"
>
Hello world
</span>
</label>
<span
class="_message_19upo_85 _help-message_19upo_91"
id="_r_4_"
>
this is a byline
</span>
</div>
</div>
</form>
</div>
</DocumentFragment>
`;
exports[`<LabelledCheckbox /> should render with byline of undefined 1`] = `
<DocumentFragment>
<div
class="mx_LabelledCheckbox"
>
<form
class="_root_19upo_16"
>
<div
class="_inline-field_19upo_32"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_153f2_10"
>
<input
checked=""
class="_input_153f2_18"
id="checkbox_vY7Q4uEh9K"
type="checkbox"
/>
<div
class="_ui_153f2_19"
>
<svg
aria-hidden="true"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="checkbox_vY7Q4uEh9K"
>
<span
class="mx_LabelledCheckbox_label"
>
Hello world
</span>
</label>
</div>
</div>
</form>
</div>
</DocumentFragment>
`;
@@ -0,0 +1,14 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<LearnMore /> renders button 1`] = `
<div>
<button
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
data-testid="testid"
role="button"
tabindex="0"
>
Learn more
</button>
</div>
`;
@@ -0,0 +1,303 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
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_va14e_8 mx_BaseAvatar _avatar-imageless_va14e_55"
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_va14e_8 mx_BaseAvatar _avatar-imageless_va14e_55"
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_va14e_8 mx_BaseAvatar _avatar-imageless_va14e_55"
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_va14e_8 mx_BaseAvatar _avatar-imageless_va14e_55"
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_va14e_8 mx_BaseAvatar _avatar-imageless_va14e_55"
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_va14e_8 mx_BaseAvatar _avatar-imageless_va14e_55"
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"
>
<svg
class="mx_Pill_UserIcon mx_BaseAvatar"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 12q-1.65 0-2.825-1.175T8 8t1.175-2.825T12 4t2.825 1.175T16 8t-1.175 2.825T12 12m-8 6v-.8q0-.85.438-1.562.437-.713 1.162-1.088a14.8 14.8 0 0 1 3.15-1.163A13.8 13.8 0 0 1 12 13q1.65 0 3.25.387 1.6.388 3.15 1.163.724.375 1.163 1.087Q20 16.35 20 17.2v.8q0 .824-.587 1.413A1.93 1.93 0 0 1 18 20H6q-.824 0-1.412-.587A1.93 1.93 0 0 1 4 18"
/>
</svg>
<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_va14e_8 mx_BaseAvatar _avatar-imageless_va14e_55"
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_va14e_8 mx_BaseAvatar _avatar-imageless_va14e_55"
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>
`;
@@ -0,0 +1,716 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
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"
>
<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.414"
/>
</svg>
</div>
<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"
/>
<svg
class="mx_Field_select_chevron"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</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"
>
<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.414"
/>
</svg>
</div>
</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"
>
<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.414"
/>
</svg>
</div>
</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"
>
<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.414"
/>
</svg>
</div>
<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"
/>
<svg
class="mx_Field_select_chevron"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</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"
>
<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.414"
/>
</svg>
</div>
</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"
>
<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.414"
/>
</svg>
</div>
</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"
>
<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.414"
/>
</svg>
</div>
</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"
>
<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.414"
/>
</svg>
</div>
<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"
/>
<svg
class="mx_Field_select_chevron"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</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"
>
<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.414"
/>
</svg>
</div>
</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"
>
<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.414"
/>
</svg>
</div>
</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>
`;
@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
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>
`;
@@ -0,0 +1,64 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`ReplyChain should call setQuoteExpanded if chain is longer than 2 lines 1`] = `
<DocumentFragment>
<div
class="mx_ReplyChain_wrapper"
>
<div />
<div>
<blockquote
class="mx_ReplyChain mx_ReplyChain_color2"
>
<div
class="mx_ReplyTile"
>
<a
href="#"
>
<div
class="mx_ReplyTile_sender"
>
<span
class="_avatar_va14e_8 mx_BaseAvatar _avatar-imageless_va14e_55"
data-color="2"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 16px;"
title="@userId:matrix.org"
>
u
</span>
<div
class="mx_DisambiguatedProfile _disambiguatedProfile_4jooo_8"
role="button"
tabindex="0"
>
<span
class="mx_Username_color2 _disambiguatedProfile_displayName_4jooo_14 mx_DisambiguatedProfile_displayName"
dir="auto"
>
@userId:matrix.org
</span>
</div>
</div>
<div
class="mx_MTextBody mx_EventTile_content _root_1hgc7_8 _text_1hgc7_13"
>
<div
class="mx_EventTile_body translate"
dir="auto"
>
A
B
C
</div>
</div>
</a>
</div>
</blockquote>
</div>
</div>
</DocumentFragment>
`;
@@ -0,0 +1,28 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<RoomFacePile /> renders 1`] = `
<DocumentFragment>
<div
aria-describedby="_r_1_"
aria-labelledby="_r_0_"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
>
<div
class="_stacked-avatars_va14e_109"
>
<span
class="_avatar_va14e_8 mx_BaseAvatar _avatar-imageless_va14e_55"
data-color="4"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 28px;"
>
b
</span>
</div>
</div>
</DocumentFragment>
`;
@@ -0,0 +1,50 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
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>
`;
@@ -0,0 +1,57 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<SettingsDropdown /> should render a disabled setting 1`] = `
<DocumentFragment>
<div
class="mx_SettingsDropdown"
>
<label
class="mx_SettingsDropdown_label"
for="_r_0_"
>
<span
class="mx_SettingsDropdown_labelText"
>
Open %(brand)s when you log in to your computer
</span>
</label>
<div
class="mx_Dropdown mx_Dropdown_disabled"
>
<div
aria-describedby="_r_0__value"
aria-disabled="true"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Open %(brand)s when you log in to your computer"
aria-owns="_r_0__input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput mx_AccessibleButton_disabled"
disabled=""
role="button"
tabindex="0"
>
<div
class="mx_Dropdown_option"
id="_r_0__value"
>
<div>
No
</div>
</div>
<svg
class="mx_Dropdown_arrow"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</div>
</DocumentFragment>
`;
@@ -0,0 +1,31 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<SettingsField /> should render with the default label 1`] = `
<DocumentFragment>
<form
class="_root_19upo_16"
>
<div
class="_field_19upo_26"
>
<label
class="_label_19upo_59"
for="radix-_r_0_"
>
Element Call URL
</label>
<div
class="_controls_17lij_8"
>
<input
class="_control_d83jn_10"
id="radix-_r_0_"
name="input"
title=""
value=""
/>
</div>
</div>
</form>
</DocumentFragment>
`;
@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
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>
<svg
class="mx_Dropdown_arrow"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</DocumentFragment>
`;
@@ -0,0 +1,89 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
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>
`;
@@ -0,0 +1,30 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<SyntaxHighlight /> renders 1`] = `
<div>
<pre
class="mx_SyntaxHighlight hljs language-javascript"
>
<code>
<span
class="hljs-variable language_"
>
console
</span>
.
<span
class="hljs-title function_"
>
log
</span>
(
<span
class="hljs-string"
>
"Hello, World!"
</span>
);
</code>
</pre>
</div>
`;
@@ -0,0 +1,27 @@
/*
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 OR LicenseRef-Element-Commercial
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={new Uint8ClampedArray(Buffer.from("asd"))} />,
);
// wait for the spinner to go away
await waitFor(() => getAllByAltText("QR Code").length === 1);
expect(container).toMatchSnapshot();
});
});
@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
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>
`;