Merge matrix-react-sdk into element-web
Merge remote-tracking branch 'repomerge/t3chguy/repomerge' into t3chguy/repo-merge Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
commit
f0ee7f7905
3265 changed files with 484599 additions and 699 deletions
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { fireEvent, getByText, render } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
|
||||
import AccessibleButton from "../../../../../src/components/views/elements/AccessibleButton";
|
||||
import { Key } from "../../../../../src/Keyboard";
|
||||
import { mockPlatformPeg, unmockPlatformPeg } from "../../../../test-utils";
|
||||
|
||||
describe("<AccessibleButton />", () => {
|
||||
const defaultProps = {
|
||||
onClick: jest.fn(),
|
||||
children: "i am a button",
|
||||
};
|
||||
const getComponent = (props = {}) => render(<AccessibleButton {...defaultProps} {...props} />);
|
||||
|
||||
beforeEach(() => {
|
||||
mockPlatformPeg();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
unmockPlatformPeg();
|
||||
});
|
||||
|
||||
it("renders div with role button by default", () => {
|
||||
const { asFragment } = getComponent();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders a button element", () => {
|
||||
const { asFragment } = getComponent({ element: "button" });
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with correct classes when button has kind", () => {
|
||||
const { asFragment } = getComponent({
|
||||
kind: "primary",
|
||||
});
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("disables button correctly", () => {
|
||||
const onClick = jest.fn();
|
||||
const { container } = getComponent({
|
||||
onClick,
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
const btn = getByText(container, "i am a button");
|
||||
|
||||
expect(btn.hasAttribute("disabled")).toBeTruthy();
|
||||
expect(btn.hasAttribute("aria-disabled")).toBeTruthy();
|
||||
|
||||
fireEvent.click(btn);
|
||||
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.keyPress(btn, { key: Key.ENTER, code: Key.ENTER });
|
||||
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls onClick handler on button click", () => {
|
||||
const onClick = jest.fn();
|
||||
const { container } = getComponent({
|
||||
onClick,
|
||||
});
|
||||
|
||||
const btn = getByText(container, "i am a button");
|
||||
fireEvent.click(btn);
|
||||
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls onClick handler on button mousedown when triggerOnMousedown is passed", () => {
|
||||
const onClick = jest.fn();
|
||||
const { container } = getComponent({
|
||||
onClick,
|
||||
triggerOnMouseDown: true,
|
||||
});
|
||||
|
||||
const btn = getByText(container, "i am a button");
|
||||
fireEvent.mouseDown(btn);
|
||||
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("handling keyboard events", () => {
|
||||
it("calls onClick handler on enter keydown", () => {
|
||||
const onClick = jest.fn();
|
||||
const { container } = getComponent({
|
||||
onClick,
|
||||
});
|
||||
|
||||
const btn = getByText(container, "i am a button");
|
||||
|
||||
fireEvent.keyDown(btn, { key: Key.ENTER, code: Key.ENTER });
|
||||
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
|
||||
fireEvent.keyUp(btn, { key: Key.ENTER, code: Key.ENTER });
|
||||
|
||||
// handler only called once on keydown
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onClick handler on space keyup", () => {
|
||||
const onClick = jest.fn();
|
||||
const { container } = getComponent({
|
||||
onClick,
|
||||
});
|
||||
const btn = getByText(container, "i am a button");
|
||||
|
||||
fireEvent.keyDown(btn, { key: Key.SPACE, code: Key.SPACE });
|
||||
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.keyUp(btn, { key: Key.SPACE, code: Key.SPACE });
|
||||
|
||||
// handler only called once on keyup
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onKeydown/onKeyUp handlers for keys other than space and enter", () => {
|
||||
const onClick = jest.fn();
|
||||
const onKeyDown = jest.fn();
|
||||
const onKeyUp = jest.fn();
|
||||
const { container } = getComponent({
|
||||
onClick,
|
||||
onKeyDown,
|
||||
onKeyUp,
|
||||
});
|
||||
|
||||
const btn = getByText(container, "i am a button");
|
||||
|
||||
fireEvent.keyDown(btn, { key: Key.K, code: Key.K });
|
||||
fireEvent.keyUp(btn, { key: Key.K, code: Key.K });
|
||||
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
expect(onKeyDown).toHaveBeenCalled();
|
||||
expect(onKeyUp).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing on non space/enter key presses when no onKeydown/onKeyUp handlers provided", () => {
|
||||
const onClick = jest.fn();
|
||||
const { container } = getComponent({
|
||||
onClick,
|
||||
});
|
||||
|
||||
const btn = getByText(container, "i am a button");
|
||||
|
||||
fireEvent.keyDown(btn, { key: Key.K, code: Key.K });
|
||||
fireEvent.keyUp(btn, { key: Key.K, code: Key.K });
|
||||
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
479
test/unit-tests/components/views/elements/AppTile-test.tsx
Normal file
479
test/unit-tests/components/views/elements/AppTile-test.tsx
Normal file
|
@ -0,0 +1,479 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { jest } from "@jest/globals";
|
||||
import { Room, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { ClientWidgetApi, IWidget, MatrixWidgetType } from "matrix-widget-api";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
import { act, render, RenderResult } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { SpiedFunction } from "jest-mock";
|
||||
import {
|
||||
ApprovalOpts,
|
||||
WidgetInfo,
|
||||
WidgetLifecycle,
|
||||
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/WidgetLifecycle";
|
||||
|
||||
import RightPanel from "../../../../../src/components/structures/RightPanel";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import ResizeNotifier from "../../../../../src/utils/ResizeNotifier";
|
||||
import { stubClient } from "../../../../test-utils";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import dis from "../../../../../src/dispatcher/dispatcher";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases";
|
||||
import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelStore";
|
||||
import { UPDATE_EVENT } from "../../../../../src/stores/AsyncStore";
|
||||
import WidgetStore, { IApp } from "../../../../../src/stores/WidgetStore";
|
||||
import ActiveWidgetStore from "../../../../../src/stores/ActiveWidgetStore";
|
||||
import AppTile from "../../../../../src/components/views/elements/AppTile";
|
||||
import { Container, WidgetLayoutStore } from "../../../../../src/stores/widgets/WidgetLayoutStore";
|
||||
import AppsDrawer from "../../../../../src/components/views/rooms/AppsDrawer";
|
||||
import { ElementWidgetCapabilities } from "../../../../../src/stores/widgets/ElementWidgetCapabilities";
|
||||
import { ElementWidget } from "../../../../../src/stores/widgets/StopGapWidget";
|
||||
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
|
||||
import { ModuleRunner } from "../../../../../src/modules/ModuleRunner";
|
||||
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
|
||||
|
||||
jest.mock("../../../../../src/stores/OwnProfileStore", () => ({
|
||||
OwnProfileStore: {
|
||||
instance: {
|
||||
isProfileInfoFetched: true,
|
||||
removeListener: jest.fn(),
|
||||
getHttpAvatarUrl: jest.fn().mockReturnValue("http://avatar_url"),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe("AppTile", () => {
|
||||
let cli: MatrixClient;
|
||||
let r1: Room;
|
||||
let r2: Room;
|
||||
const resizeNotifier = new ResizeNotifier();
|
||||
let app1: IApp;
|
||||
let app2: IApp;
|
||||
|
||||
const waitForRps = (roomId: string) =>
|
||||
new Promise<void>((resolve) => {
|
||||
const update = () => {
|
||||
if (RightPanelStore.instance.currentCardForRoom(roomId).phase !== RightPanelPhases.Widget) return;
|
||||
RightPanelStore.instance.off(UPDATE_EVENT, update);
|
||||
resolve();
|
||||
};
|
||||
RightPanelStore.instance.on(UPDATE_EVENT, update);
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.safeGet();
|
||||
cli.hasLazyLoadMembersEnabled = () => false;
|
||||
|
||||
// Init misc. startup deps
|
||||
DMRoomMap.makeShared(cli);
|
||||
|
||||
r1 = new Room("r1", cli, "@name:example.com");
|
||||
r2 = new Room("r2", cli, "@name:example.com");
|
||||
|
||||
jest.spyOn(cli, "getRoom").mockImplementation((roomId) => {
|
||||
if (roomId === "r1") return r1;
|
||||
if (roomId === "r2") return r2;
|
||||
return null;
|
||||
});
|
||||
jest.spyOn(cli, "getVisibleRooms").mockImplementation(() => {
|
||||
return [r1, r2];
|
||||
});
|
||||
|
||||
// Adjust various widget stores to add mock apps
|
||||
app1 = {
|
||||
id: "1",
|
||||
eventId: "1",
|
||||
roomId: "r1",
|
||||
type: MatrixWidgetType.Custom,
|
||||
url: "https://example.com",
|
||||
name: "Example 1",
|
||||
creatorUserId: cli.getSafeUserId(),
|
||||
avatar_url: undefined,
|
||||
};
|
||||
app2 = {
|
||||
id: "1",
|
||||
eventId: "2",
|
||||
roomId: "r2",
|
||||
type: MatrixWidgetType.Custom,
|
||||
url: "https://example.com",
|
||||
name: "Example 2",
|
||||
creatorUserId: cli.getSafeUserId(),
|
||||
avatar_url: undefined,
|
||||
};
|
||||
jest.spyOn(WidgetStore.instance, "getApps").mockImplementation((roomId: string): Array<IApp> => {
|
||||
if (roomId === "r1") return [app1];
|
||||
if (roomId === "r2") return [app2];
|
||||
return [];
|
||||
});
|
||||
|
||||
// Wake up various stores we rely on
|
||||
WidgetLayoutStore.instance.useUnitTestClient(cli);
|
||||
// @ts-ignore
|
||||
await WidgetLayoutStore.instance.onReady();
|
||||
RightPanelStore.instance.useUnitTestClient(cli);
|
||||
// @ts-ignore
|
||||
await RightPanelStore.instance.onReady();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockRestore();
|
||||
});
|
||||
|
||||
it("destroys non-persisted right panel widget on room change", async () => {
|
||||
// Set up right panel state
|
||||
const realGetValue = SettingsStore.getValue;
|
||||
const mockSettings = jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
|
||||
if (name !== "RightPanel.phases") return realGetValue(name, roomId);
|
||||
if (roomId === "r1") {
|
||||
return {
|
||||
history: [
|
||||
{
|
||||
phase: RightPanelPhases.Widget,
|
||||
state: {
|
||||
widgetId: "1",
|
||||
},
|
||||
},
|
||||
],
|
||||
isOpen: true,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Run initial render with room 1, and also running lifecycle methods
|
||||
const renderResult = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<RightPanel
|
||||
room={r1}
|
||||
resizeNotifier={resizeNotifier}
|
||||
permalinkCreator={new RoomPermalinkCreator(r1, r1.roomId)}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
// Wait for RPS room 1 updates to fire
|
||||
const rpsUpdated = waitForRps("r1");
|
||||
dis.dispatch({
|
||||
action: Action.ViewRoom,
|
||||
room_id: "r1",
|
||||
});
|
||||
await rpsUpdated;
|
||||
|
||||
expect(renderResult.getByText("Example 1")).toBeInTheDocument();
|
||||
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
|
||||
|
||||
const { container, asFragment } = renderResult;
|
||||
expect(container.getElementsByClassName("mx_Spinner").length).toBeTruthy();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
// We want to verify that as we change to room 2, we should close the
|
||||
// right panel and destroy the widget.
|
||||
|
||||
// Switch to room 2
|
||||
dis.dispatch({
|
||||
action: Action.ViewRoom,
|
||||
room_id: "r2",
|
||||
});
|
||||
|
||||
renderResult.rerender(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<RightPanel
|
||||
room={r2}
|
||||
resizeNotifier={resizeNotifier}
|
||||
permalinkCreator={new RoomPermalinkCreator(r2, r2.roomId)}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
expect(renderResult.queryByText("Example 1")).not.toBeInTheDocument();
|
||||
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(false);
|
||||
|
||||
mockSettings.mockRestore();
|
||||
});
|
||||
|
||||
it("distinguishes widgets with the same ID in different rooms", async () => {
|
||||
// Set up right panel state
|
||||
const realGetValue = SettingsStore.getValue;
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
|
||||
if (name === "RightPanel.phases") {
|
||||
if (roomId === "r1") {
|
||||
return {
|
||||
history: [
|
||||
{
|
||||
phase: RightPanelPhases.Widget,
|
||||
state: {
|
||||
widgetId: "1",
|
||||
},
|
||||
},
|
||||
],
|
||||
isOpen: true,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return realGetValue(name, roomId);
|
||||
});
|
||||
|
||||
// Run initial render with room 1, and also running lifecycle methods
|
||||
const renderResult = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<RightPanel
|
||||
room={r1}
|
||||
resizeNotifier={resizeNotifier}
|
||||
permalinkCreator={new RoomPermalinkCreator(r1, r1.roomId)}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
// Wait for RPS room 1 updates to fire
|
||||
const rpsUpdated1 = waitForRps("r1");
|
||||
dis.dispatch({
|
||||
action: Action.ViewRoom,
|
||||
room_id: "r1",
|
||||
});
|
||||
await rpsUpdated1;
|
||||
|
||||
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
|
||||
expect(ActiveWidgetStore.instance.isLive("1", "r2")).toBe(false);
|
||||
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
|
||||
if (name === "RightPanel.phases") {
|
||||
if (roomId === "r2") {
|
||||
return {
|
||||
history: [
|
||||
{
|
||||
phase: RightPanelPhases.Widget,
|
||||
state: {
|
||||
widgetId: "1",
|
||||
},
|
||||
},
|
||||
],
|
||||
isOpen: true,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return realGetValue(name, roomId);
|
||||
});
|
||||
// Wait for RPS room 2 updates to fire
|
||||
const rpsUpdated2 = waitForRps("r2");
|
||||
// Switch to room 2
|
||||
dis.dispatch({
|
||||
action: Action.ViewRoom,
|
||||
room_id: "r2",
|
||||
});
|
||||
renderResult.rerender(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<RightPanel
|
||||
room={r2}
|
||||
resizeNotifier={resizeNotifier}
|
||||
permalinkCreator={new RoomPermalinkCreator(r2, r2.roomId)}
|
||||
/>
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
await rpsUpdated2;
|
||||
|
||||
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(false);
|
||||
expect(ActiveWidgetStore.instance.isLive("1", "r2")).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves non-persisted widget on container move", async () => {
|
||||
// Set up widget in top container
|
||||
const realGetValue = SettingsStore.getValue;
|
||||
const mockSettings = jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
|
||||
if (name !== "Widgets.layout") return realGetValue(name, roomId);
|
||||
if (roomId === "r1") {
|
||||
return {
|
||||
widgets: {
|
||||
1: {
|
||||
container: Container.Top,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
act(() => {
|
||||
WidgetLayoutStore.instance.recalculateRoom(r1);
|
||||
});
|
||||
|
||||
// Run initial render with room 1, and also running lifecycle methods
|
||||
const renderResult = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<AppsDrawer userId={cli.getSafeUserId()} room={r1} resizeNotifier={resizeNotifier} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
expect(renderResult.getByText("Example 1")).toBeInTheDocument();
|
||||
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
|
||||
|
||||
const { asFragment } = renderResult;
|
||||
expect(asFragment()).toMatchSnapshot(); // Take snapshot of AppsDrawer with AppTile
|
||||
|
||||
// We want to verify that as we move the widget to the center container,
|
||||
// the widget frame remains running.
|
||||
|
||||
// Stop mocking settings so that the widget move can take effect
|
||||
mockSettings.mockRestore();
|
||||
act(() => {
|
||||
// Move widget to center
|
||||
WidgetLayoutStore.instance.moveToContainer(r1, app1, Container.Center);
|
||||
});
|
||||
|
||||
expect(renderResult.getByText("Example 1")).toBeInTheDocument();
|
||||
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// @ts-ignore
|
||||
await WidgetLayoutStore.instance.onNotReady();
|
||||
// @ts-ignore
|
||||
await RightPanelStore.instance.onNotReady();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("for a pinned widget", () => {
|
||||
let renderResult: RenderResult;
|
||||
let moveToContainerSpy: SpiedFunction<typeof WidgetLayoutStore.instance.moveToContainer>;
|
||||
|
||||
beforeEach(() => {
|
||||
renderResult = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<AppTile key={app1.id} app={app1} room={r1} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
moveToContainerSpy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
const { container, asFragment } = renderResult;
|
||||
|
||||
expect(container.querySelector(".mx_Spinner")).toBeFalsy(); // Assert that the spinner is gone
|
||||
expect(asFragment()).toMatchSnapshot(); // Take a snapshot of the pinned widget
|
||||
});
|
||||
|
||||
it("should not display the »Popout widget« button", () => {
|
||||
expect(renderResult.queryByLabelText("Popout widget")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("clicking 'minimise' should send the widget to the right", async () => {
|
||||
await userEvent.click(renderResult.getByLabelText("Minimise"));
|
||||
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Right);
|
||||
});
|
||||
|
||||
it("clicking 'maximise' should send the widget to the center", async () => {
|
||||
await userEvent.click(renderResult.getByLabelText("Maximise"));
|
||||
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Center);
|
||||
});
|
||||
|
||||
it("should render permission request", () => {
|
||||
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
|
||||
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === app1.id) {
|
||||
(opts as ApprovalOpts).approved = false;
|
||||
}
|
||||
});
|
||||
|
||||
// userId and creatorUserId are different
|
||||
const renderResult = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
const { container, asFragment } = renderResult;
|
||||
|
||||
expect(container.querySelector(".mx_Spinner")).toBeFalsy();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
expect(renderResult.queryByRole("button", { name: "Continue" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not display 'Continue' button on permission load", () => {
|
||||
jest.spyOn(ModuleRunner.instance, "invoke").mockImplementation((lifecycleEvent, opts, widgetInfo) => {
|
||||
if (lifecycleEvent === WidgetLifecycle.PreLoadRequest && (widgetInfo as WidgetInfo).id === app1.id) {
|
||||
(opts as ApprovalOpts).approved = true;
|
||||
}
|
||||
});
|
||||
|
||||
// userId and creatorUserId are different
|
||||
const renderResult = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
expect(renderResult.queryByRole("button", { name: "Continue" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("for a maximised (centered) widget", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockImplementation(
|
||||
(room: Optional<Room>, widget: IWidget, container: Container) => {
|
||||
return room === r1 && widget === app1 && container === Container.Center;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("clicking 'un-maximise' should send the widget to the top", async () => {
|
||||
await userEvent.click(renderResult.getByLabelText("Un-maximise"));
|
||||
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, Container.Top);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with an existing widgetApi with requiresClient = false", () => {
|
||||
beforeEach(() => {
|
||||
const api = {
|
||||
hasCapability: (capability: ElementWidgetCapabilities): boolean => {
|
||||
return !(capability === ElementWidgetCapabilities.RequiresClient);
|
||||
},
|
||||
once: () => {},
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi;
|
||||
|
||||
const mockWidget = new ElementWidget(app1);
|
||||
WidgetMessagingStore.instance.storeMessaging(mockWidget, r1.roomId, api);
|
||||
|
||||
renderResult = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<AppTile key={app1.id} app={app1} room={r1} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
it("should display the »Popout widget« button", () => {
|
||||
expect(renderResult.getByLabelText("Popout widget")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a persistent app", () => {
|
||||
let renderResult: RenderResult;
|
||||
|
||||
beforeEach(() => {
|
||||
renderResult = render(
|
||||
<MatrixClientContext.Provider value={cli}>
|
||||
<AppTile key={app1.id} app={app1} fullWidth={true} room={r1} miniMode={true} showMenubar={false} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
const { container, asFragment } = renderResult;
|
||||
|
||||
expect(container.querySelector(".mx_Spinner")).toBeFalsy();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import DesktopCapturerSourcePicker from "../../../../../src/components/views/elements/DesktopCapturerSourcePicker";
|
||||
import PlatformPeg from "../../../../../src/PlatformPeg";
|
||||
import BasePlatform from "../../../../../src/BasePlatform";
|
||||
|
||||
const SOURCES = [
|
||||
{
|
||||
id: "screen1",
|
||||
name: "Screen 1",
|
||||
thumbnailURL: "data:image/png;base64,",
|
||||
},
|
||||
{
|
||||
id: "window1",
|
||||
name: "Window 1",
|
||||
thumbnailURL: "data:image/png;base64,",
|
||||
},
|
||||
];
|
||||
|
||||
describe("DesktopCapturerSourcePicker", () => {
|
||||
beforeEach(() => {
|
||||
const plaf = {
|
||||
getDesktopCapturerSources: jest.fn().mockResolvedValue(SOURCES),
|
||||
supportsSetting: jest.fn().mockReturnValue(false),
|
||||
};
|
||||
jest.spyOn(PlatformPeg, "get").mockReturnValue(plaf as unknown as BasePlatform);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should render the component", () => {
|
||||
render(<DesktopCapturerSourcePicker onFinished={() => {}} />);
|
||||
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Share" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should disable share button until a source is selected", () => {
|
||||
render(<DesktopCapturerSourcePicker onFinished={() => {}} />);
|
||||
expect(screen.getByRole("button", { name: "Share" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should contain a screen source in the default tab", async () => {
|
||||
render(<DesktopCapturerSourcePicker onFinished={() => {}} />);
|
||||
|
||||
const screen1Button = await screen.findByRole("button", { name: "Screen 1" });
|
||||
|
||||
expect(screen1Button).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Window 1" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should contain a window source in the window tab", async () => {
|
||||
render(<DesktopCapturerSourcePicker onFinished={() => {}} />);
|
||||
|
||||
await userEvent.click(screen.getByRole("tab", { name: "Application window" }));
|
||||
|
||||
const window1Button = await screen.findByRole("button", { name: "Window 1" });
|
||||
|
||||
expect(window1Button).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Screen 1" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onFinished with no arguments if cancelled", async () => {
|
||||
const onFinished = jest.fn();
|
||||
render(<DesktopCapturerSourcePicker onFinished={onFinished} />);
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
expect(onFinished).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("should call onFinished with the selected source when share clicked", async () => {
|
||||
const onFinished = jest.fn();
|
||||
render(<DesktopCapturerSourcePicker onFinished={onFinished} />);
|
||||
|
||||
const screen1Button = await screen.findByRole("button", { name: "Screen 1" });
|
||||
|
||||
await userEvent.click(screen1Button);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Share" }));
|
||||
expect(onFinished).toHaveBeenCalledWith(SOURCES[0]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, waitFor } from "jest-matrix-react";
|
||||
|
||||
import dis from "../../../../../src/dispatcher/dispatcher";
|
||||
import EffectsOverlay from "../../../../../src/components/views/elements/EffectsOverlay.tsx";
|
||||
|
||||
describe("<EffectsOverlay/>", () => {
|
||||
let isStarted: boolean;
|
||||
beforeEach(() => {
|
||||
isStarted = false;
|
||||
jest.mock("../../../../../src/effects/confetti/index.ts", () => {
|
||||
return class Confetti {
|
||||
start = () => {
|
||||
isStarted = true;
|
||||
};
|
||||
stop = jest.fn();
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => jest.useRealTimers());
|
||||
|
||||
it("should render", () => {
|
||||
const { asFragment } = render(<EffectsOverlay roomWidth={100} />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should start the confetti effect", async () => {
|
||||
render(<EffectsOverlay roomWidth={100} />);
|
||||
dis.dispatch({ action: "effects.confetti" });
|
||||
await waitFor(() => expect(isStarted).toBe(true));
|
||||
});
|
||||
|
||||
it("should start the confetti effect when the event is not outdated", async () => {
|
||||
const eventDate = new Date("2024-09-01");
|
||||
const date = new Date("2024-09-02");
|
||||
jest.useFakeTimers().setSystemTime(date);
|
||||
|
||||
render(<EffectsOverlay roomWidth={100} />);
|
||||
dis.dispatch({ action: "effects.confetti", event: { getTs: () => eventDate.getTime() } });
|
||||
await waitFor(() => expect(isStarted).toBe(true));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,683 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps } from "react";
|
||||
import { render, RenderResult } from "jest-matrix-react";
|
||||
import { MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership, Membership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import {
|
||||
getMockClientWithEventEmitter,
|
||||
mkEvent,
|
||||
mkMembership,
|
||||
mockClientMethodsUser,
|
||||
unmockClientPeg,
|
||||
} from "../../../../test-utils";
|
||||
import EventListSummary from "../../../../../src/components/views/elements/EventListSummary";
|
||||
import { Layout } from "../../../../../src/settings/enums/Layout";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
import * as languageHandler from "../../../../../src/languageHandler";
|
||||
|
||||
describe("EventListSummary", function () {
|
||||
const roomId = "!room:server.org";
|
||||
// Generate dummy event tiles for use in simulating an expanded MELS
|
||||
const generateTiles = (events: MatrixEvent[]) => {
|
||||
return events.map((e) => {
|
||||
return (
|
||||
<div key={e.getId()} className="event_tile">
|
||||
Expanded membership
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a membership event with the target of the event set as a mocked
|
||||
* RoomMember based on `parameters.userId`.
|
||||
* @param {string} eventId the ID of the event.
|
||||
* @param {object} parameters the parameters to use to create the event.
|
||||
* @param {string} parameters.membership the membership to assign to
|
||||
* `content.membership`
|
||||
* @param {string} parameters.userId the state key and target userId of the event. If
|
||||
* `parameters.senderId` is not specified, this is also used as the event sender.
|
||||
* @param {string} parameters.prevMembership the membership to assign to
|
||||
* `prev_content.membership`.
|
||||
* @param {string} parameters.senderId the user ID of the sender of the event.
|
||||
* Optional. Defaults to `parameters.userId`.
|
||||
* @returns {MatrixEvent} the event created.
|
||||
*/
|
||||
interface MembershipEventParams {
|
||||
senderId?: string;
|
||||
userId?: string;
|
||||
membership: Membership;
|
||||
prevMembership?: Membership;
|
||||
}
|
||||
const generateMembershipEvent = (
|
||||
eventId: string,
|
||||
{ senderId, userId, membership, prevMembership }: MembershipEventParams & { userId: string },
|
||||
): MatrixEvent => {
|
||||
const member = new RoomMember(roomId, userId);
|
||||
// Use localpart as display name;
|
||||
member.name = userId.match(/@([^:]*):/)![1];
|
||||
jest.spyOn(member, "getAvatarUrl").mockReturnValue("avatar.jpeg");
|
||||
jest.spyOn(member, "getMxcAvatarUrl").mockReturnValue("mxc://avatar.url/image.png");
|
||||
const e = mkMembership({
|
||||
event: true,
|
||||
room: roomId,
|
||||
user: senderId || userId,
|
||||
skey: userId,
|
||||
mship: membership,
|
||||
prevMship: prevMembership,
|
||||
target: member,
|
||||
});
|
||||
// Override random event ID to allow for equality tests against tiles from
|
||||
// generateTiles
|
||||
e.event.event_id = eventId;
|
||||
return e;
|
||||
};
|
||||
|
||||
// Generate mock MatrixEvents from the array of parameters
|
||||
const generateEvents = (parameters: Array<MembershipEventParams & { userId: string }>) => {
|
||||
const res: MatrixEvent[] = [];
|
||||
for (let i = 0; i < parameters.length; i++) {
|
||||
res.push(generateMembershipEvent(`event${i}`, parameters[i]));
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
// Generate the same sequence of `events` for `n` users, where each user ID
|
||||
// is created by replacing the first "$" in userIdTemplate with `i` for
|
||||
// `i = 0 .. n`.
|
||||
const generateEventsForUsers = (userIdTemplate: string, n: number, events: MembershipEventParams[]) => {
|
||||
let eventsForUsers: MatrixEvent[] = [];
|
||||
let userId = "";
|
||||
for (let i = 0; i < n; i++) {
|
||||
userId = userIdTemplate.replace("$", String(i));
|
||||
events.forEach((e) => {
|
||||
e.userId = userId;
|
||||
});
|
||||
eventsForUsers = eventsForUsers.concat(
|
||||
generateEvents(events as Array<MembershipEventParams & { userId: string }>),
|
||||
);
|
||||
}
|
||||
return eventsForUsers;
|
||||
};
|
||||
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(),
|
||||
});
|
||||
|
||||
const defaultProps: Omit<
|
||||
ComponentProps<typeof EventListSummary>,
|
||||
"summaryLength" | "threshold" | "avatarsMaxLength"
|
||||
> = {
|
||||
layout: Layout.Bubble,
|
||||
events: [],
|
||||
children: [],
|
||||
};
|
||||
const renderComponent = (props = {}): RenderResult => {
|
||||
return render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<EventListSummary {...defaultProps} {...props} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(languageHandler, "getUserLanguage").mockReturnValue("en-GB");
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
unmockClientPeg();
|
||||
});
|
||||
|
||||
it("renders expanded events if there are less than props.threshold", function () {
|
||||
const events = generateEvents([
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
]);
|
||||
const props = {
|
||||
events: events,
|
||||
children: generateTiles(events),
|
||||
summaryLength: 1,
|
||||
avatarsMaxLength: 5,
|
||||
threshold: 3,
|
||||
};
|
||||
|
||||
const { container } = renderComponent(props); // matrix cli context wrapper
|
||||
|
||||
const children = container.querySelector(".mx_GenericEventListSummary_unstyledList")!.children;
|
||||
expect(children).toHaveLength(1);
|
||||
expect(children[0]).toHaveTextContent("Expanded membership");
|
||||
});
|
||||
|
||||
it("renders expanded events if there are less than props.threshold for join and leave", function () {
|
||||
const events = generateEvents([
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
]);
|
||||
const props = {
|
||||
events: events,
|
||||
children: generateTiles(events),
|
||||
summaryLength: 1,
|
||||
avatarsMaxLength: 5,
|
||||
threshold: 3,
|
||||
};
|
||||
|
||||
const { container } = renderComponent(props); // matrix cli context wrapper
|
||||
|
||||
const children = container.querySelector(".mx_GenericEventListSummary_unstyledList")!.children;
|
||||
expect(children).toHaveLength(2);
|
||||
expect(children[0]).toHaveTextContent("Expanded membership");
|
||||
expect(children[1]).toHaveTextContent("Expanded membership");
|
||||
});
|
||||
|
||||
it("renders collapsed events if events.length = props.threshold", function () {
|
||||
const events = generateEvents([
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
]);
|
||||
const props = {
|
||||
events: events,
|
||||
children: generateTiles(events),
|
||||
summaryLength: 1,
|
||||
avatarsMaxLength: 5,
|
||||
threshold: 3,
|
||||
};
|
||||
|
||||
const { container } = renderComponent(props);
|
||||
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
|
||||
expect(summary).toHaveTextContent("user_1 joined and left and joined");
|
||||
});
|
||||
|
||||
it("truncates long join,leave repetitions", function () {
|
||||
const events = generateEvents([
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
]);
|
||||
const props = {
|
||||
events: events,
|
||||
children: generateTiles(events),
|
||||
summaryLength: 1,
|
||||
avatarsMaxLength: 5,
|
||||
threshold: 3,
|
||||
};
|
||||
|
||||
const { container } = renderComponent(props);
|
||||
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
|
||||
expect(summary).toHaveTextContent("user_1 joined and left 7 times");
|
||||
});
|
||||
|
||||
it("truncates long join,leave repetitions between other events", function () {
|
||||
const events = generateEvents([
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Ban,
|
||||
membership: KnownMembership.Leave,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Leave,
|
||||
membership: KnownMembership.Invite,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
]);
|
||||
const props = {
|
||||
events: events,
|
||||
children: generateTiles(events),
|
||||
summaryLength: 1,
|
||||
avatarsMaxLength: 5,
|
||||
threshold: 3,
|
||||
};
|
||||
|
||||
const { container } = renderComponent(props);
|
||||
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
|
||||
expect(summary).toHaveTextContent("user_1 was unbanned, joined and left 7 times and was invited");
|
||||
});
|
||||
|
||||
it("truncates multiple sequences of repetitions with other events between", function () {
|
||||
const events = generateEvents([
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Ban,
|
||||
membership: KnownMembership.Leave,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Leave,
|
||||
membership: KnownMembership.Ban,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Ban, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Leave,
|
||||
membership: KnownMembership.Invite,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
]);
|
||||
const props = {
|
||||
events: events,
|
||||
children: generateTiles(events),
|
||||
summaryLength: 1,
|
||||
avatarsMaxLength: 5,
|
||||
threshold: 3,
|
||||
};
|
||||
|
||||
const { container } = renderComponent(props);
|
||||
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
|
||||
expect(summary).toHaveTextContent(
|
||||
"user_1 was unbanned, joined and left 2 times, was banned, " + "joined and left 3 times and was invited",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles multiple users following the same sequence of memberships", function () {
|
||||
const events = generateEvents([
|
||||
// user_1
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Ban,
|
||||
membership: KnownMembership.Leave,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Leave,
|
||||
membership: KnownMembership.Ban,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
// user_2
|
||||
{
|
||||
userId: "@user_2:some.domain",
|
||||
prevMembership: KnownMembership.Ban,
|
||||
membership: KnownMembership.Leave,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
{ userId: "@user_2:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_2:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_2:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_2:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{
|
||||
userId: "@user_2:some.domain",
|
||||
prevMembership: KnownMembership.Leave,
|
||||
membership: KnownMembership.Ban,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
]);
|
||||
const props = {
|
||||
events: events,
|
||||
children: generateTiles(events),
|
||||
summaryLength: 1,
|
||||
avatarsMaxLength: 5,
|
||||
threshold: 3,
|
||||
};
|
||||
|
||||
const { container } = renderComponent(props);
|
||||
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
|
||||
expect(summary).toHaveTextContent(
|
||||
"user_1 and one other were unbanned, joined and left 2 times and were banned",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles many users following the same sequence of memberships", function () {
|
||||
const events = generateEventsForUsers("@user_$:some.domain", 20, [
|
||||
{
|
||||
prevMembership: KnownMembership.Ban,
|
||||
membership: KnownMembership.Leave,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
{ prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{
|
||||
prevMembership: KnownMembership.Leave,
|
||||
membership: KnownMembership.Ban,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
]);
|
||||
const props = {
|
||||
events: events,
|
||||
children: generateTiles(events),
|
||||
summaryLength: 1,
|
||||
avatarsMaxLength: 5,
|
||||
threshold: 3,
|
||||
};
|
||||
|
||||
const { container } = renderComponent(props);
|
||||
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
|
||||
expect(summary).toHaveTextContent(
|
||||
"user_0 and 19 others were unbanned, joined and left 2 times and were banned",
|
||||
);
|
||||
});
|
||||
|
||||
it("correctly orders sequences of transitions by the order of their first event", function () {
|
||||
const events = generateEvents([
|
||||
{
|
||||
userId: "@user_2:some.domain",
|
||||
prevMembership: KnownMembership.Ban,
|
||||
membership: KnownMembership.Leave,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Ban,
|
||||
membership: KnownMembership.Leave,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Leave,
|
||||
membership: KnownMembership.Ban,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
{ userId: "@user_2:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_2:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
{ userId: "@user_2:some.domain", prevMembership: KnownMembership.Leave, membership: KnownMembership.Join },
|
||||
{ userId: "@user_2:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
]);
|
||||
const props = {
|
||||
events: events,
|
||||
children: generateTiles(events),
|
||||
summaryLength: 1,
|
||||
avatarsMaxLength: 5,
|
||||
threshold: 3,
|
||||
};
|
||||
|
||||
const { container } = renderComponent(props);
|
||||
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
|
||||
expect(summary).toHaveTextContent(
|
||||
"user_2 was unbanned and joined and left 2 times, user_1 was unbanned, " +
|
||||
"joined and left 2 times and was banned",
|
||||
);
|
||||
});
|
||||
|
||||
it("correctly identifies transitions", function () {
|
||||
const events = generateEvents([
|
||||
// invited
|
||||
{ userId: "@user_1:some.domain", membership: KnownMembership.Invite },
|
||||
// banned
|
||||
{ userId: "@user_1:some.domain", membership: KnownMembership.Ban },
|
||||
// joined
|
||||
{ userId: "@user_1:some.domain", membership: KnownMembership.Join },
|
||||
// invite_reject
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Invite,
|
||||
membership: KnownMembership.Leave,
|
||||
},
|
||||
// left
|
||||
{ userId: "@user_1:some.domain", prevMembership: KnownMembership.Join, membership: KnownMembership.Leave },
|
||||
// invite_withdrawal
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Invite,
|
||||
membership: KnownMembership.Leave,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
// unbanned
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Ban,
|
||||
membership: KnownMembership.Leave,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
// kicked
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Join,
|
||||
membership: KnownMembership.Leave,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
// default for sender=target (leave)
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: "????" as Membership,
|
||||
membership: KnownMembership.Leave,
|
||||
senderId: "@user_1:some.domain",
|
||||
},
|
||||
// default for sender<>target (kicked)
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: "????" as Membership,
|
||||
membership: KnownMembership.Leave,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
]);
|
||||
const props = {
|
||||
events: events,
|
||||
children: generateTiles(events),
|
||||
summaryLength: 1,
|
||||
avatarsMaxLength: 5,
|
||||
threshold: 3,
|
||||
};
|
||||
|
||||
const { container } = renderComponent(props);
|
||||
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
|
||||
expect(summary).toHaveTextContent(
|
||||
"user_1 was invited, was banned, joined, rejected their invitation, left, " +
|
||||
"had their invitation withdrawn, was unbanned, was removed, left and was removed",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles invitation plurals correctly when there are multiple users", function () {
|
||||
const events = generateEvents([
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Invite,
|
||||
membership: KnownMembership.Leave,
|
||||
},
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Invite,
|
||||
membership: KnownMembership.Leave,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
{
|
||||
userId: "@user_2:some.domain",
|
||||
prevMembership: KnownMembership.Invite,
|
||||
membership: KnownMembership.Leave,
|
||||
},
|
||||
{
|
||||
userId: "@user_2:some.domain",
|
||||
prevMembership: KnownMembership.Invite,
|
||||
membership: KnownMembership.Leave,
|
||||
senderId: "@some_other_user:some.domain",
|
||||
},
|
||||
]);
|
||||
const props = {
|
||||
events: events,
|
||||
children: generateTiles(events),
|
||||
summaryLength: 1,
|
||||
avatarsMaxLength: 5,
|
||||
threshold: 3,
|
||||
};
|
||||
|
||||
const { container } = renderComponent(props);
|
||||
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
|
||||
expect(summary).toHaveTextContent(
|
||||
"user_1 and one other rejected their invitations and had their invitations withdrawn",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles invitation plurals correctly when there are multiple invites", function () {
|
||||
const events = generateEvents([
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Invite,
|
||||
membership: KnownMembership.Leave,
|
||||
},
|
||||
{
|
||||
userId: "@user_1:some.domain",
|
||||
prevMembership: KnownMembership.Invite,
|
||||
membership: KnownMembership.Leave,
|
||||
},
|
||||
]);
|
||||
const props = {
|
||||
events: events,
|
||||
children: generateTiles(events),
|
||||
summaryLength: 1,
|
||||
avatarsMaxLength: 5,
|
||||
threshold: 1, // threshold = 1 to force collapse
|
||||
};
|
||||
|
||||
const { container } = renderComponent(props);
|
||||
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
|
||||
expect(summary).toHaveTextContent("user_1 rejected their invitation 2 times");
|
||||
});
|
||||
|
||||
it('handles a summary length = 2, with no "others"', function () {
|
||||
const events = generateEvents([
|
||||
{ userId: "@user_1:some.domain", membership: KnownMembership.Join },
|
||||
{ userId: "@user_1:some.domain", membership: KnownMembership.Join },
|
||||
{ userId: "@user_2:some.domain", membership: KnownMembership.Join },
|
||||
{ userId: "@user_2:some.domain", membership: KnownMembership.Join },
|
||||
]);
|
||||
const props = {
|
||||
events: events,
|
||||
children: generateTiles(events),
|
||||
summaryLength: 2,
|
||||
avatarsMaxLength: 5,
|
||||
threshold: 3,
|
||||
};
|
||||
|
||||
const { container } = renderComponent(props);
|
||||
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
|
||||
expect(summary).toHaveTextContent("user_1 and user_2 joined 2 times");
|
||||
});
|
||||
|
||||
it('handles a summary length = 2, with 1 "other"', function () {
|
||||
const events = generateEvents([
|
||||
{ userId: "@user_1:some.domain", membership: KnownMembership.Join },
|
||||
{ userId: "@user_2:some.domain", membership: KnownMembership.Join },
|
||||
{ userId: "@user_3:some.domain", membership: KnownMembership.Join },
|
||||
]);
|
||||
const props = {
|
||||
events: events,
|
||||
children: generateTiles(events),
|
||||
summaryLength: 2,
|
||||
avatarsMaxLength: 5,
|
||||
threshold: 3,
|
||||
};
|
||||
|
||||
const { container } = renderComponent(props);
|
||||
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
|
||||
expect(summary).toHaveTextContent("user_1, user_2 and one other joined");
|
||||
});
|
||||
|
||||
it('handles a summary length = 2, with many "others"', function () {
|
||||
const events = generateEventsForUsers("@user_$:some.domain", 20, [{ membership: KnownMembership.Join }]);
|
||||
const props = {
|
||||
events: events,
|
||||
children: generateTiles(events),
|
||||
summaryLength: 2,
|
||||
avatarsMaxLength: 5,
|
||||
threshold: 3,
|
||||
};
|
||||
|
||||
const { container } = renderComponent(props);
|
||||
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
|
||||
expect(summary).toHaveTextContent("user_0, user_1 and 18 others joined");
|
||||
});
|
||||
|
||||
it("should not blindly group 3pid invites and treat them as distinct users instead", () => {
|
||||
const events = [
|
||||
mkEvent({
|
||||
event: true,
|
||||
skey: "randomstring1",
|
||||
user: "@user1:server",
|
||||
type: "m.room.third_party_invite",
|
||||
content: {
|
||||
display_name: "n...@d...",
|
||||
key_validity_url: "https://blah",
|
||||
public_key: "public_key",
|
||||
},
|
||||
}),
|
||||
mkEvent({
|
||||
event: true,
|
||||
skey: "randomstring2",
|
||||
user: "@user1:server",
|
||||
type: "m.room.third_party_invite",
|
||||
content: {
|
||||
display_name: "n...@d...",
|
||||
key_validity_url: "https://blah",
|
||||
public_key: "public_key",
|
||||
},
|
||||
}),
|
||||
mkEvent({
|
||||
event: true,
|
||||
skey: "randomstring3",
|
||||
user: "@user1:server",
|
||||
type: "m.room.third_party_invite",
|
||||
content: {
|
||||
display_name: "d...@w...",
|
||||
key_validity_url: "https://blah",
|
||||
public_key: "public_key",
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const props = {
|
||||
events: events,
|
||||
children: generateTiles(events),
|
||||
summaryLength: 2,
|
||||
avatarsMaxLength: 5,
|
||||
threshold: 3,
|
||||
};
|
||||
|
||||
const { container } = renderComponent(props);
|
||||
const summary = container.querySelector(".mx_GenericEventListSummary_summary");
|
||||
expect(summary).toHaveTextContent("n...@d... was invited 2 times, d...@w... was invited");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
|
||||
import ExternalLink from "../../../../../src/components/views/elements/ExternalLink";
|
||||
|
||||
describe("<ExternalLink />", () => {
|
||||
const defaultProps = {
|
||||
"href": "test.com",
|
||||
"onClick": jest.fn(),
|
||||
"className": "myCustomClass",
|
||||
"data-testid": "test",
|
||||
};
|
||||
const getComponent = (props = {}) => {
|
||||
return render(<ExternalLink {...defaultProps} {...props} />);
|
||||
};
|
||||
|
||||
it("renders link correctly", () => {
|
||||
const children = (
|
||||
<span>
|
||||
react element <b>children</b>
|
||||
</span>
|
||||
);
|
||||
expect(getComponent({ children, target: "_self", rel: "noopener" }).asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("defaults target and rel", () => {
|
||||
const children = "test";
|
||||
const { getByTestId } = getComponent({ children });
|
||||
const container = getByTestId("test");
|
||||
expect(container.getAttribute("rel")).toEqual("noreferrer noopener");
|
||||
expect(container.getAttribute("target")).toEqual("_blank");
|
||||
});
|
||||
|
||||
it("renders plain text link correctly", () => {
|
||||
const children = "test";
|
||||
expect(getComponent({ children }).asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
26
test/unit-tests/components/views/elements/FacePile-test.tsx
Normal file
26
test/unit-tests/components/views/elements/FacePile-test.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import FacePile from "../../../../../src/components/views/elements/FacePile";
|
||||
import { mkRoomMember } from "../../../../test-utils";
|
||||
|
||||
describe("<FacePile />", () => {
|
||||
it("renders with a tooltip", () => {
|
||||
const member = mkRoomMember("123", "456", KnownMembership.Join);
|
||||
|
||||
const { asFragment } = render(
|
||||
<FacePile members={[member]} size="36px" overflow={false} tooltipLabel="tooltip" />,
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
109
test/unit-tests/components/views/elements/Field-test.tsx
Normal file
109
test/unit-tests/components/views/elements/Field-test.tsx
Normal file
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, screen } from "jest-matrix-react";
|
||||
|
||||
import Field from "../../../../../src/components/views/elements/Field";
|
||||
|
||||
describe("Field", () => {
|
||||
describe("Placeholder", () => {
|
||||
it("Should display a placeholder", async () => {
|
||||
// When
|
||||
const { rerender } = render(<Field value="" placeholder="my placeholder" />);
|
||||
|
||||
// Then
|
||||
expect(screen.getByRole("textbox")).toHaveAttribute("placeholder", "my placeholder");
|
||||
|
||||
// When
|
||||
rerender(<Field value="" placeholder="" />);
|
||||
|
||||
// Then
|
||||
expect(screen.getByRole("textbox")).toHaveAttribute("placeholder", "");
|
||||
});
|
||||
|
||||
it("Should display label as placeholder", async () => {
|
||||
// When
|
||||
render(<Field value="" label="my label" />);
|
||||
|
||||
// Then
|
||||
expect(screen.getByRole("textbox")).toHaveAttribute("placeholder", "my label");
|
||||
});
|
||||
|
||||
it("Should not display a placeholder", async () => {
|
||||
// When
|
||||
render(<Field value="" />);
|
||||
|
||||
// Then
|
||||
expect(screen.getByRole("textbox")).not.toHaveAttribute("placeholder", "my placeholder");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Feedback", () => {
|
||||
it("Should mark the feedback as alert if invalid", async () => {
|
||||
render(
|
||||
<Field
|
||||
value=""
|
||||
validateOnFocus
|
||||
onValidate={() => Promise.resolve({ valid: false, feedback: "Invalid" })}
|
||||
/>,
|
||||
);
|
||||
|
||||
// When invalid
|
||||
fireEvent.focus(screen.getByRole("textbox"));
|
||||
|
||||
// Expect 'alert' role
|
||||
await expect(screen.findByRole("alert")).resolves.toBeInTheDocument();
|
||||
|
||||
// Close the feedback is Escape is pressed
|
||||
fireEvent.keyDown(screen.getByRole("textbox"), { key: "Escape" });
|
||||
expect(screen.queryByRole("alert")).toBeNull();
|
||||
});
|
||||
|
||||
it("Should mark the feedback as status if valid", async () => {
|
||||
render(
|
||||
<Field
|
||||
value=""
|
||||
validateOnFocus
|
||||
onValidate={() => Promise.resolve({ valid: true, feedback: "Valid" })}
|
||||
/>,
|
||||
);
|
||||
|
||||
// When valid
|
||||
fireEvent.focus(screen.getByRole("textbox"));
|
||||
|
||||
// Expect 'status' role
|
||||
await expect(screen.findByRole("status")).resolves.toBeInTheDocument();
|
||||
|
||||
// Close the feedback is Escape is pressed
|
||||
fireEvent.keyDown(screen.getByRole("textbox"), { key: "Escape" });
|
||||
expect(screen.queryByRole("status")).toBeNull();
|
||||
});
|
||||
|
||||
it("Should mark the feedback as tooltip if custom tooltip set", async () => {
|
||||
render(
|
||||
<Field
|
||||
value=""
|
||||
validateOnFocus
|
||||
onValidate={() => Promise.resolve({ valid: true, feedback: "Valid" })}
|
||||
tooltipContent="Tooltip"
|
||||
/>,
|
||||
);
|
||||
|
||||
// When valid or invalid and 'tooltipContent' set
|
||||
fireEvent.focus(screen.getByRole("textbox"));
|
||||
|
||||
// Expect 'tooltip' role
|
||||
await expect(screen.findByRole("tooltip")).resolves.toBeInTheDocument();
|
||||
|
||||
// Close the feedback is Escape is pressed
|
||||
fireEvent.keyDown(screen.getByRole("textbox"), { key: "Escape" });
|
||||
expect(screen.queryByRole("tooltip")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { act, fireEvent, render } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
|
||||
import { FilterDropdown } from "../../../../../src/components/views/elements/FilterDropdown";
|
||||
import { flushPromises, mockPlatformPeg } from "../../../../test-utils";
|
||||
|
||||
mockPlatformPeg();
|
||||
|
||||
describe("<FilterDropdown />", () => {
|
||||
const options = [
|
||||
{ id: "one", label: "Option one" },
|
||||
{ id: "two", label: "Option two", description: "with description" },
|
||||
];
|
||||
const defaultProps = {
|
||||
className: "test",
|
||||
value: "one",
|
||||
options,
|
||||
id: "test",
|
||||
label: "test label",
|
||||
onOptionChange: jest.fn(),
|
||||
};
|
||||
const getComponent = (props = {}): JSX.Element => <FilterDropdown {...defaultProps} {...props} />;
|
||||
|
||||
const openDropdown = async (container: HTMLElement): Promise<void> =>
|
||||
await act(async () => {
|
||||
const button = container.querySelector('[role="button"]');
|
||||
expect(button).toBeTruthy();
|
||||
fireEvent.click(button as Element);
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("renders selected option", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders when selected option is not in options", () => {
|
||||
const { container } = render(getComponent({ value: "oops" }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders selected option with selectedLabel", () => {
|
||||
const { container } = render(getComponent({ selectedLabel: "Show" }));
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders dropdown options in menu", async () => {
|
||||
const { container } = render(getComponent());
|
||||
await openDropdown(container);
|
||||
expect(container.querySelector(".mx_Dropdown_menu")).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render } from "jest-matrix-react";
|
||||
|
||||
import { FilterTabGroup } from "../../../../../src/components/views/elements/FilterTabGroup";
|
||||
|
||||
describe("<FilterTabGroup />", () => {
|
||||
enum TestOption {
|
||||
Apple = "Apple",
|
||||
Banana = "Banana",
|
||||
Orange = "Orange",
|
||||
}
|
||||
const defaultProps = {
|
||||
"name": "test",
|
||||
"value": TestOption.Apple,
|
||||
"onFilterChange": jest.fn(),
|
||||
"tabs": [
|
||||
{ id: TestOption.Apple, label: `Label for ${TestOption.Apple}` },
|
||||
{ id: TestOption.Banana, label: `Label for ${TestOption.Banana}` },
|
||||
{ id: TestOption.Orange, label: `Label for ${TestOption.Orange}` },
|
||||
],
|
||||
"data-testid": "test",
|
||||
};
|
||||
const getComponent = (props = {}) => <FilterTabGroup<TestOption> {...defaultProps} {...props} />;
|
||||
|
||||
it("renders options", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls onChange handler on selection", () => {
|
||||
const onFilterChange = jest.fn();
|
||||
const { getByText } = render(getComponent({ onFilterChange }));
|
||||
|
||||
fireEvent.click(getByText("Label for Banana"));
|
||||
|
||||
expect(onFilterChange).toHaveBeenCalledWith(TestOption.Banana);
|
||||
});
|
||||
});
|
19
test/unit-tests/components/views/elements/ImageView-test.tsx
Normal file
19
test/unit-tests/components/views/elements/ImageView-test.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
|
||||
import ImageView from "../../../../../src/components/views/elements/ImageView";
|
||||
|
||||
describe("<ImageView />", () => {
|
||||
it("renders correctly", () => {
|
||||
const { container } = render(<ImageView src="https://example.com/image.png" onFinished={jest.fn()} />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { render, waitFor } from "jest-matrix-react";
|
||||
|
||||
import InfoTooltip from "../../../../../src/components/views/elements/InfoTooltip";
|
||||
|
||||
describe("InfoTooltip", () => {
|
||||
it("should show tooltip on hover", async () => {
|
||||
const { getByText, asFragment } = render(<InfoTooltip tooltip="Tooltip text">Trigger text</InfoTooltip>);
|
||||
|
||||
const trigger = getByText("Trigger text");
|
||||
expect(trigger).toBeVisible();
|
||||
await userEvent.hover(trigger!);
|
||||
|
||||
// wait for the tooltip to open
|
||||
const tooltip = await waitFor(() => {
|
||||
const tooltip = document.getElementById(trigger.getAttribute("aria-describedby")!);
|
||||
expect(tooltip).toBeVisible();
|
||||
return tooltip;
|
||||
});
|
||||
expect(tooltip).toHaveTextContent("Tooltip text");
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { fireEvent, render, screen } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
|
||||
import LabelledCheckbox from "../../../../../src/components/views/elements/LabelledCheckbox";
|
||||
|
||||
describe("<LabelledCheckbox />", () => {
|
||||
type CompProps = React.ComponentProps<typeof LabelledCheckbox>;
|
||||
const getComponent = (props: CompProps) => <LabelledCheckbox {...props} />;
|
||||
const getCheckbox = (): HTMLInputElement => screen.getByRole("checkbox");
|
||||
|
||||
it.each([undefined, "this is a byline"])("should render with byline of %p", (byline) => {
|
||||
const props: CompProps = {
|
||||
label: "Hello world",
|
||||
value: true,
|
||||
byline: byline,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const renderResult = render(getComponent(props));
|
||||
expect(renderResult.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should support unchecked by default", () => {
|
||||
const props: CompProps = {
|
||||
label: "Hello world",
|
||||
value: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
render(getComponent(props));
|
||||
expect(getCheckbox()).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("should be possible to disable the checkbox", () => {
|
||||
const props: CompProps = {
|
||||
label: "Hello world",
|
||||
value: false,
|
||||
disabled: true,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
render(getComponent(props));
|
||||
expect(getCheckbox()).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should emit onChange calls", () => {
|
||||
const props: CompProps = {
|
||||
label: "Hello world",
|
||||
value: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
render(getComponent(props));
|
||||
|
||||
expect(props.onChange).not.toHaveBeenCalled();
|
||||
fireEvent.click(getCheckbox());
|
||||
expect(props.onChange).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("should react to value and disabled prop changes", () => {
|
||||
const props: CompProps = {
|
||||
label: "Hello world",
|
||||
value: false,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const { rerender } = render(getComponent(props));
|
||||
|
||||
let checkbox = getCheckbox();
|
||||
expect(checkbox).not.toBeChecked();
|
||||
expect(checkbox).not.toBeDisabled();
|
||||
|
||||
props.disabled = true;
|
||||
props.value = true;
|
||||
rerender(getComponent(props));
|
||||
|
||||
checkbox = getCheckbox();
|
||||
expect(checkbox).toBeChecked();
|
||||
expect(checkbox).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should render with a custom class name", () => {
|
||||
const className = "some class name";
|
||||
const props: CompProps = {
|
||||
label: "Hello world",
|
||||
value: false,
|
||||
onChange: jest.fn(),
|
||||
className,
|
||||
};
|
||||
const { container } = render(getComponent(props));
|
||||
expect(container.firstElementChild?.className).toContain(className);
|
||||
});
|
||||
});
|
49
test/unit-tests/components/views/elements/LearnMore-test.tsx
Normal file
49
test/unit-tests/components/views/elements/LearnMore-test.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render } from "jest-matrix-react";
|
||||
|
||||
import LearnMore from "../../../../../src/components/views/elements/LearnMore";
|
||||
import Modal from "../../../../../src/Modal";
|
||||
import InfoDialog from "../../../../../src/components/views/dialogs/InfoDialog";
|
||||
|
||||
describe("<LearnMore />", () => {
|
||||
const defaultProps = {
|
||||
title: "Test",
|
||||
description: "test test test",
|
||||
["data-testid"]: "testid",
|
||||
};
|
||||
const getComponent = (props = {}) => <LearnMore {...defaultProps} {...props} />;
|
||||
|
||||
const modalSpy = jest.spyOn(Modal, "createDialog").mockReturnValue({
|
||||
finished: new Promise(() => {}),
|
||||
close: jest.fn(),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders button", () => {
|
||||
const { container } = render(getComponent());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("opens modal on click", async () => {
|
||||
const { getByTestId } = render(getComponent());
|
||||
fireEvent.click(getByTestId("testid"));
|
||||
|
||||
expect(modalSpy).toHaveBeenCalledWith(InfoDialog, {
|
||||
button: "Got it",
|
||||
description: defaultProps.description,
|
||||
hasCloseButton: true,
|
||||
title: defaultProps.title,
|
||||
});
|
||||
});
|
||||
});
|
282
test/unit-tests/components/views/elements/Pill-test.tsx
Normal file
282
test/unit-tests/components/views/elements/Pill-test.tsx
Normal file
|
@ -0,0 +1,282 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { act, render, RenderResult, screen } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { mocked, Mocked } from "jest-mock";
|
||||
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import dis from "../../../../../src/dispatcher/dispatcher";
|
||||
import { Pill, PillProps, PillType } from "../../../../../src/components/views/elements/Pill";
|
||||
import {
|
||||
filterConsole,
|
||||
flushPromises,
|
||||
mkMessage,
|
||||
mkRoomCanonicalAliasEvent,
|
||||
mkRoomMemberJoinEvent,
|
||||
stubClient,
|
||||
} from "../../../../test-utils";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
import { ButtonEvent } from "../../../../../src/components/views/elements/AccessibleButton";
|
||||
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
|
||||
|
||||
describe("<Pill>", () => {
|
||||
let client: Mocked<MatrixClient>;
|
||||
const permalinkPrefix = "https://matrix.to/#/";
|
||||
const room1Alias = "#room1:example.com";
|
||||
const room1Id = "!room1:example.com";
|
||||
let room1: Room;
|
||||
let room1Message: MatrixEvent;
|
||||
const room2Id = "!room2:example.com";
|
||||
let room2: Room;
|
||||
const space1Id = "!space1:example.com";
|
||||
let space1: Room;
|
||||
const user1Id = "@user1:example.com";
|
||||
const user2Id = "@user2:example.com";
|
||||
const user3Id = "@user3:example.com";
|
||||
let renderResult: RenderResult;
|
||||
let pillParentClickHandler: (e: ButtonEvent) => void;
|
||||
|
||||
const renderPill = (props: PillProps): void => {
|
||||
const withDefault = {
|
||||
inMessage: true,
|
||||
shouldShowPillAvatar: true,
|
||||
...props,
|
||||
} as PillProps;
|
||||
// wrap Pill with a div to allow testing of event bubbling
|
||||
renderResult = render(
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||
<div onClick={pillParentClickHandler}>
|
||||
<Pill {...withDefault} />
|
||||
</div>,
|
||||
);
|
||||
};
|
||||
|
||||
filterConsole(
|
||||
"Failed to parse permalink Error: Unknown entity type in permalink",
|
||||
"Room !room1:example.com does not have an m.room.create event",
|
||||
"Room !space1:example.com does not have an m.room.create event",
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
client = mocked(stubClient());
|
||||
SdkContextClass.instance.client = client;
|
||||
DMRoomMap.makeShared(client);
|
||||
room1 = new Room(room1Id, client, user1Id);
|
||||
room1.name = "Room 1";
|
||||
const user1JoinRoom1Event = mkRoomMemberJoinEvent(user1Id, room1Id, {
|
||||
displayname: "User 1",
|
||||
});
|
||||
room1.currentState.setStateEvents([
|
||||
mkRoomCanonicalAliasEvent(user1Id, room1Id, room1Alias),
|
||||
user1JoinRoom1Event,
|
||||
]);
|
||||
room1.getMember(user1Id)!.setMembershipEvent(user1JoinRoom1Event);
|
||||
room1Message = mkMessage({
|
||||
id: "$123-456",
|
||||
event: true,
|
||||
user: user1Id,
|
||||
room: room1Id,
|
||||
msg: "Room 1 Message",
|
||||
});
|
||||
room1.addLiveEvents([room1Message]);
|
||||
|
||||
room2 = new Room(room2Id, client, user1Id);
|
||||
room2.currentState.setStateEvents([mkRoomMemberJoinEvent(user2Id, room2Id)]);
|
||||
room2.name = "Room 2";
|
||||
|
||||
space1 = new Room(space1Id, client, client.getSafeUserId());
|
||||
space1.name = "Space 1";
|
||||
|
||||
client.getRooms.mockReturnValue([room1, room2, space1]);
|
||||
client.getRoom.mockImplementation((roomId: string) => {
|
||||
if (roomId === room1.roomId) return room1;
|
||||
if (roomId === room2.roomId) return room2;
|
||||
if (roomId === space1.roomId) return space1;
|
||||
return null;
|
||||
});
|
||||
|
||||
client.getProfileInfo.mockImplementation(async (userId: string) => {
|
||||
if (userId === user2Id) return { displayname: "User 2" };
|
||||
throw new Error(`Unknown user ${userId}`);
|
||||
});
|
||||
|
||||
jest.spyOn(dis, "dispatch");
|
||||
pillParentClickHandler = jest.fn();
|
||||
|
||||
jest.spyOn(global.Math, "random").mockReturnValue(0.123456);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.spyOn(global.Math, "random").mockRestore();
|
||||
});
|
||||
|
||||
describe("when rendering a pill for a room", () => {
|
||||
beforeEach(() => {
|
||||
renderPill({
|
||||
url: permalinkPrefix + room1Id,
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the expected pill", () => {
|
||||
expect(renderResult.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when hovering the pill", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.hover(screen.getByText("Room 1"));
|
||||
});
|
||||
|
||||
it("should show a tooltip with the room Id", async () => {
|
||||
expect(await screen.findByRole("tooltip", { name: room1Id })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when not hovering the pill any more", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.unhover(screen.getByText("Room 1"));
|
||||
});
|
||||
|
||||
it("should dimiss a tooltip with the room Id", () => {
|
||||
expect(screen.queryByRole("tooltip")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should not render a non-permalink", () => {
|
||||
renderPill({
|
||||
url: "https://example.com/hello",
|
||||
});
|
||||
expect(renderResult.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render the expected pill for a space", () => {
|
||||
renderPill({
|
||||
url: permalinkPrefix + space1Id,
|
||||
});
|
||||
expect(renderResult.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render the expected pill for a room alias", () => {
|
||||
renderPill({
|
||||
url: permalinkPrefix + room1Alias,
|
||||
});
|
||||
expect(renderResult.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render the expected pill for @room", () => {
|
||||
renderPill({
|
||||
room: room1,
|
||||
type: PillType.AtRoomMention,
|
||||
});
|
||||
expect(renderResult.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when rendering a pill for a user in the room", () => {
|
||||
beforeEach(() => {
|
||||
renderPill({
|
||||
room: room1,
|
||||
url: permalinkPrefix + user1Id,
|
||||
});
|
||||
});
|
||||
|
||||
it("should render as expected", () => {
|
||||
expect(renderResult.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when clicking the pill", () => {
|
||||
beforeEach(async () => {
|
||||
await userEvent.click(screen.getByText("User 1"));
|
||||
});
|
||||
|
||||
it("should dipsatch a view user action and prevent event bubbling", () => {
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewUser,
|
||||
member: room1.getMember(user1Id),
|
||||
});
|
||||
|
||||
expect(pillParentClickHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the expected pill for a known user not in the room", async () => {
|
||||
renderPill({
|
||||
room: room1,
|
||||
url: permalinkPrefix + user2Id,
|
||||
});
|
||||
|
||||
// wait for profile query via API
|
||||
await act(async () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
expect(renderResult.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render the expected pill for an uknown user not in the room", async () => {
|
||||
renderPill({
|
||||
room: room1,
|
||||
url: permalinkPrefix + user3Id,
|
||||
});
|
||||
|
||||
// wait for profile query via API
|
||||
await act(async () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
expect(renderResult.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should not render anything if the type cannot be detected", () => {
|
||||
renderPill({
|
||||
url: permalinkPrefix,
|
||||
});
|
||||
expect(renderResult.asFragment()).toMatchInlineSnapshot(`
|
||||
<DocumentFragment>
|
||||
<div />
|
||||
</DocumentFragment>
|
||||
`);
|
||||
});
|
||||
|
||||
it("should not render an avatar or link when called with inMessage = false and shouldShowPillAvatar = false", () => {
|
||||
renderPill({
|
||||
inMessage: false,
|
||||
shouldShowPillAvatar: false,
|
||||
url: permalinkPrefix + room1Id,
|
||||
});
|
||||
expect(renderResult.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
it("should render the expected pill for a message in the same room", () => {
|
||||
renderPill({
|
||||
room: room1,
|
||||
url: `${permalinkPrefix}${room1Id}/${room1Message.getId()}`,
|
||||
});
|
||||
expect(renderResult.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render the expected pill for a message in another room", () => {
|
||||
renderPill({
|
||||
room: room2,
|
||||
url: `${permalinkPrefix}${room1Id}/${room1Message.getId()}`,
|
||||
});
|
||||
expect(renderResult.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should not render a pill with an unknown type", () => {
|
||||
// @ts-ignore
|
||||
renderPill({ type: "unknown" });
|
||||
expect(renderResult.asFragment()).toMatchInlineSnapshot(`
|
||||
<DocumentFragment>
|
||||
<div />
|
||||
</DocumentFragment>
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,295 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, RenderResult } from "jest-matrix-react";
|
||||
import {
|
||||
Room,
|
||||
MatrixEvent,
|
||||
M_POLL_KIND_DISCLOSED,
|
||||
M_POLL_KIND_UNDISCLOSED,
|
||||
M_POLL_START,
|
||||
M_TEXT,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
||||
import { ReplacementEvent } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { getMockClientWithEventEmitter } from "../../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import PollCreateDialog from "../../../../../src/components/views/elements/PollCreateDialog";
|
||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||
|
||||
// Fake date to give a predictable snapshot
|
||||
const realDateNow = Date.now;
|
||||
const realDateToISOString = Date.prototype.toISOString;
|
||||
Date.now = jest.fn(() => 2345678901234);
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Date.prototype.toISOString = jest.fn(() => "2021-11-23T14:35:14.240Z");
|
||||
|
||||
afterAll(() => {
|
||||
Date.now = realDateNow;
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Date.prototype.toISOString = realDateToISOString;
|
||||
});
|
||||
|
||||
describe("PollCreateDialog", () => {
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
sendEvent: jest.fn().mockResolvedValue({ event_id: "1" }),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.sendEvent.mockClear();
|
||||
});
|
||||
|
||||
it("renders a blank poll", () => {
|
||||
const dialog = render(
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
expect(dialog.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("autofocuses the poll topic on mount", () => {
|
||||
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
|
||||
expect(dialog.container.querySelector("#poll-topic-input")).toHaveFocus();
|
||||
});
|
||||
|
||||
it("autofocuses the new poll option field after clicking add option button", () => {
|
||||
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
|
||||
expect(dialog.container.querySelector("#poll-topic-input")).toHaveFocus();
|
||||
|
||||
fireEvent.click(dialog.container.querySelector("div.mx_PollCreateDialog_addOption")!);
|
||||
|
||||
expect(dialog.container.querySelector("#poll-topic-input")).not.toHaveFocus();
|
||||
expect(dialog.container.querySelector("#pollcreate_option_1")).not.toHaveFocus();
|
||||
expect(dialog.container.querySelector("#pollcreate_option_2")).toHaveFocus();
|
||||
});
|
||||
|
||||
it("renders a question and some options", () => {
|
||||
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
|
||||
expectSubmitToBeDisabled(dialog, true);
|
||||
|
||||
// When I set some values in the boxes
|
||||
changeValue(dialog, "Question or topic", "How many turnips is the optimal number?");
|
||||
changeValue(dialog, "Option 1", "As many as my neighbour");
|
||||
changeValue(dialog, "Option 2", "The question is meaningless");
|
||||
fireEvent.click(dialog.container.querySelector("div.mx_PollCreateDialog_addOption")!);
|
||||
changeValue(dialog, "Option 3", "Mu");
|
||||
expect(dialog.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders info from a previous event", () => {
|
||||
const previousEvent: MatrixEvent = new MatrixEvent(
|
||||
PollStartEvent.from("Poll Q", ["Answer 1", "Answer 2"], M_POLL_KIND_DISCLOSED).serialize(),
|
||||
);
|
||||
|
||||
const dialog = render(
|
||||
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} editingMxEvent={previousEvent} />,
|
||||
);
|
||||
|
||||
expectSubmitToBeDisabled(dialog, false);
|
||||
expect(dialog.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("doesn't allow submitting until there are options", () => {
|
||||
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
|
||||
expectSubmitToBeDisabled(dialog, true);
|
||||
});
|
||||
|
||||
it("does allow submitting when there are options and a question", () => {
|
||||
// Given a dialog with no info in (which I am unable to submit)
|
||||
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
|
||||
expectSubmitToBeDisabled(dialog, true);
|
||||
|
||||
// When I set some values in the boxes
|
||||
changeValue(dialog, "Question or topic", "Q");
|
||||
changeValue(dialog, "Option 1", "A1");
|
||||
changeValue(dialog, "Option 2", "A2");
|
||||
|
||||
// Then I am able to submit
|
||||
expectSubmitToBeDisabled(dialog, false);
|
||||
});
|
||||
|
||||
it("shows the open poll description at first", () => {
|
||||
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
|
||||
expect(dialog.container.querySelector("select")).toHaveValue(M_POLL_KIND_DISCLOSED.name);
|
||||
expect(dialog.container.querySelector("p")).toHaveTextContent("Voters see results as soon as they have voted");
|
||||
});
|
||||
|
||||
it("shows the closed poll description if we choose it", () => {
|
||||
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
|
||||
changeKind(dialog, M_POLL_KIND_UNDISCLOSED.name);
|
||||
expect(dialog.container.querySelector("select")).toHaveValue(M_POLL_KIND_UNDISCLOSED.name);
|
||||
expect(dialog.container.querySelector("p")).toHaveTextContent(
|
||||
"Results are only revealed when you end the poll",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the open poll description if we choose it", () => {
|
||||
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
|
||||
changeKind(dialog, M_POLL_KIND_UNDISCLOSED.name);
|
||||
changeKind(dialog, M_POLL_KIND_DISCLOSED.name);
|
||||
expect(dialog.container.querySelector("select")).toHaveValue(M_POLL_KIND_DISCLOSED.name);
|
||||
expect(dialog.container.querySelector("p")).toHaveTextContent("Voters see results as soon as they have voted");
|
||||
});
|
||||
|
||||
it("shows the closed poll description when editing a closed poll", () => {
|
||||
const previousEvent: MatrixEvent = new MatrixEvent(
|
||||
PollStartEvent.from("Poll Q", ["Answer 1", "Answer 2"], M_POLL_KIND_UNDISCLOSED).serialize(),
|
||||
);
|
||||
previousEvent.event.event_id = "$prevEventId";
|
||||
|
||||
const dialog = render(
|
||||
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} editingMxEvent={previousEvent} />,
|
||||
);
|
||||
|
||||
expect(dialog.container.querySelector("select")).toHaveValue(M_POLL_KIND_UNDISCLOSED.name);
|
||||
expect(dialog.container.querySelector("p")).toHaveTextContent(
|
||||
"Results are only revealed when you end the poll",
|
||||
);
|
||||
});
|
||||
|
||||
it("displays a spinner after submitting", () => {
|
||||
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
|
||||
changeValue(dialog, "Question or topic", "Q");
|
||||
changeValue(dialog, "Option 1", "A1");
|
||||
changeValue(dialog, "Option 2", "A2");
|
||||
expect(dialog.container.querySelector(".mx_Spinner")).toBeFalsy();
|
||||
|
||||
fireEvent.click(dialog.container.querySelector("button")!);
|
||||
expect(dialog.container.querySelector(".mx_Spinner")).toBeDefined();
|
||||
});
|
||||
|
||||
it("sends a poll create event when submitted", () => {
|
||||
const dialog = render(<PollCreateDialog room={createRoom()} onFinished={jest.fn()} />);
|
||||
changeValue(dialog, "Question or topic", "Q");
|
||||
changeValue(dialog, "Option 1", "A1");
|
||||
changeValue(dialog, "Option 2", "A2");
|
||||
|
||||
fireEvent.click(dialog.container.querySelector("button")!);
|
||||
const [, , eventType, sentEventContent] = mockClient.sendEvent.mock.calls[0];
|
||||
expect(M_POLL_START.matches(eventType)).toBeTruthy();
|
||||
expect(sentEventContent).toEqual({
|
||||
[M_TEXT.name]: "Q\n1. A1\n2. A2",
|
||||
[M_POLL_START.name]: {
|
||||
answers: [
|
||||
{
|
||||
id: expect.any(String),
|
||||
[M_TEXT.name]: "A1",
|
||||
},
|
||||
{
|
||||
id: expect.any(String),
|
||||
[M_TEXT.name]: "A2",
|
||||
},
|
||||
],
|
||||
kind: M_POLL_KIND_DISCLOSED.name,
|
||||
max_selections: 1,
|
||||
question: {
|
||||
body: "Q",
|
||||
format: undefined,
|
||||
formatted_body: undefined,
|
||||
msgtype: "m.text",
|
||||
[M_TEXT.name]: "Q",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("sends a poll edit event when editing", () => {
|
||||
const previousEvent: MatrixEvent = new MatrixEvent(
|
||||
PollStartEvent.from("Poll Q", ["Answer 1", "Answer 2"], M_POLL_KIND_DISCLOSED).serialize(),
|
||||
);
|
||||
previousEvent.event.event_id = "$prevEventId";
|
||||
|
||||
const dialog = render(
|
||||
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} editingMxEvent={previousEvent} />,
|
||||
);
|
||||
|
||||
changeValue(dialog, "Question or topic", "Poll Q updated");
|
||||
changeValue(dialog, "Option 2", "Answer 2 updated");
|
||||
changeKind(dialog, M_POLL_KIND_UNDISCLOSED.name);
|
||||
fireEvent.click(dialog.container.querySelector("button")!);
|
||||
|
||||
const [, , eventType, sentEventContent] = mockClient.sendEvent.mock.calls[0];
|
||||
expect(M_POLL_START.matches(eventType)).toBeTruthy();
|
||||
expect(sentEventContent).toEqual({
|
||||
"m.new_content": {
|
||||
[M_TEXT.name]: "Poll Q updated\n1. Answer 1\n2. Answer 2 updated",
|
||||
[M_POLL_START.name]: {
|
||||
answers: [
|
||||
{
|
||||
id: expect.any(String),
|
||||
[M_TEXT.name]: "Answer 1",
|
||||
},
|
||||
{
|
||||
id: expect.any(String),
|
||||
[M_TEXT.name]: "Answer 2 updated",
|
||||
},
|
||||
],
|
||||
kind: M_POLL_KIND_UNDISCLOSED.name,
|
||||
max_selections: 1,
|
||||
question: {
|
||||
body: "Poll Q updated",
|
||||
format: undefined,
|
||||
formatted_body: undefined,
|
||||
msgtype: "m.text",
|
||||
[M_TEXT.name]: "Poll Q updated",
|
||||
},
|
||||
},
|
||||
},
|
||||
"m.relates_to": {
|
||||
event_id: previousEvent.getId(),
|
||||
rel_type: "m.replace",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("retains poll disclosure type when editing", () => {
|
||||
const previousEvent: MatrixEvent = new MatrixEvent(
|
||||
PollStartEvent.from("Poll Q", ["Answer 1", "Answer 2"], M_POLL_KIND_DISCLOSED).serialize(),
|
||||
);
|
||||
previousEvent.event.event_id = "$prevEventId";
|
||||
|
||||
const dialog = render(
|
||||
<PollCreateDialog room={createRoom()} onFinished={jest.fn()} editingMxEvent={previousEvent} />,
|
||||
);
|
||||
|
||||
changeValue(dialog, "Question or topic", "Poll Q updated");
|
||||
fireEvent.click(dialog.container.querySelector("button")!);
|
||||
|
||||
const [, , eventType, sentEventContent] = mockClient.sendEvent.mock.calls[0];
|
||||
expect(M_POLL_START.matches(eventType)).toBeTruthy();
|
||||
// didnt change
|
||||
expect((sentEventContent as ReplacementEvent<any>)["m.new_content"][M_POLL_START.name].kind).toEqual(
|
||||
M_POLL_KIND_DISCLOSED.name,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function createRoom(): Room {
|
||||
return new Room("roomid", MatrixClientPeg.safeGet(), "@name:example.com", {});
|
||||
}
|
||||
|
||||
function changeValue(wrapper: RenderResult, labelText: string, value: string) {
|
||||
fireEvent.change(wrapper.container.querySelector(`input[label="${labelText}"]`)!, {
|
||||
target: { value: value },
|
||||
});
|
||||
}
|
||||
|
||||
function changeKind(wrapper: RenderResult, value: string) {
|
||||
fireEvent.change(wrapper.container.querySelector("select")!, { target: { value: value } });
|
||||
}
|
||||
|
||||
function expectSubmitToBeDisabled(wrapper: RenderResult, disabled: boolean) {
|
||||
if (disabled) {
|
||||
expect(wrapper.container.querySelector('button[type="submit"]')).toHaveAttribute("aria-disabled", "true");
|
||||
} else {
|
||||
expect(wrapper.container.querySelector('button[type="submit"]')).not.toHaveAttribute("aria-disabled", "true");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, screen } from "jest-matrix-react";
|
||||
import { defer } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import PowerSelector from "../../../../../src/components/views/elements/PowerSelector";
|
||||
|
||||
describe("<PowerSelector />", () => {
|
||||
it("should reset back to custom value when custom input is blurred blank", async () => {
|
||||
const fn = jest.fn();
|
||||
render(<PowerSelector value={25} maxValue={100} usersDefault={0} onChange={fn} />);
|
||||
|
||||
const input = screen.getByLabelText("Power level");
|
||||
fireEvent.change(input, { target: { value: "" } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
await screen.findByDisplayValue(25);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should reset back to preset value when custom input is blurred blank", async () => {
|
||||
const fn = jest.fn();
|
||||
render(<PowerSelector value={50} maxValue={100} usersDefault={0} onChange={fn} />);
|
||||
|
||||
const select = screen.getByLabelText("Power level");
|
||||
fireEvent.change(select, { target: { value: "SELECT_VALUE_CUSTOM" } });
|
||||
|
||||
const input = screen.getByLabelText("Power level");
|
||||
fireEvent.change(input, { target: { value: "" } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
const option = await screen.findByText<HTMLOptionElement>("Moderator");
|
||||
expect(option.selected).toBeTruthy();
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call onChange when custom input is blurred with a number in it", async () => {
|
||||
const fn = jest.fn();
|
||||
render(<PowerSelector value={25} maxValue={100} usersDefault={0} onChange={fn} powerLevelKey="key" />);
|
||||
|
||||
const input = screen.getByLabelText("Power level");
|
||||
fireEvent.change(input, { target: { value: 40 } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
await screen.findByDisplayValue(40);
|
||||
expect(fn).toHaveBeenCalledWith(40, "key");
|
||||
});
|
||||
|
||||
it("should reset when props get changed", async () => {
|
||||
const fn = jest.fn();
|
||||
const { rerender } = render(<PowerSelector value={50} maxValue={100} usersDefault={0} onChange={fn} />);
|
||||
|
||||
const select = screen.getByLabelText("Power level");
|
||||
fireEvent.change(select, { target: { value: "SELECT_VALUE_CUSTOM" } });
|
||||
|
||||
rerender(<PowerSelector value={51} maxValue={100} usersDefault={0} onChange={fn} />);
|
||||
await screen.findByDisplayValue(51);
|
||||
|
||||
rerender(<PowerSelector value={50} maxValue={100} usersDefault={0} onChange={fn} />);
|
||||
const option = await screen.findByText<HTMLOptionElement>("Moderator");
|
||||
expect(option.selected).toBeTruthy();
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should reset when onChange promise rejects", async () => {
|
||||
const deferred = defer<void>();
|
||||
render(
|
||||
<PowerSelector
|
||||
value={25}
|
||||
maxValue={100}
|
||||
usersDefault={0}
|
||||
onChange={() => deferred.promise}
|
||||
powerLevelKey="key"
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("Power level");
|
||||
fireEvent.change(input, { target: { value: 40 } });
|
||||
fireEvent.blur(input);
|
||||
|
||||
await screen.findByDisplayValue(40);
|
||||
deferred.reject("Some error");
|
||||
await screen.findByDisplayValue(25);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { act, render } from "jest-matrix-react";
|
||||
|
||||
import ProgressBar from "../../../../../src/components/views/elements/ProgressBar";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("<ProgressBar/>", () => {
|
||||
it("works when animated", () => {
|
||||
const { container, rerender } = render(<ProgressBar max={100} value={50} animated={true} />);
|
||||
const progress = container.querySelector<HTMLProgressElement>("progress")!;
|
||||
|
||||
// The animation always starts from 0
|
||||
expect(progress.value).toBe(0);
|
||||
|
||||
// Await the animation to conclude to our initial value of 50
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(progress.position).toBe(0.5);
|
||||
|
||||
// Move the needle to 80%
|
||||
rerender(<ProgressBar max={100} value={80} animated={true} />);
|
||||
expect(progress.position).toBe(0.5);
|
||||
|
||||
// Let the animaiton run a tiny bit, assert it has moved from where it was to where it needs to go
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(150);
|
||||
});
|
||||
expect(progress.position).toBeGreaterThan(0.5);
|
||||
expect(progress.position).toBeLessThan(0.8);
|
||||
});
|
||||
|
||||
it("works when not animated", () => {
|
||||
const { container, rerender } = render(<ProgressBar max={100} value={50} animated={false} />);
|
||||
const progress = container.querySelector<HTMLProgressElement>("progress")!;
|
||||
|
||||
// Without animation all positional updates are immediate, not requiring timers to run
|
||||
expect(progress.position).toBe(0.5);
|
||||
rerender(<ProgressBar max={100} value={80} animated={false} />);
|
||||
expect(progress.position).toBe(0.8);
|
||||
});
|
||||
});
|
35
test/unit-tests/components/views/elements/QRCode-test.tsx
Normal file
35
test/unit-tests/components/views/elements/QRCode-test.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render, waitFor, cleanup } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
|
||||
import QRCode from "../../../../../src/components/views/elements/QRCode";
|
||||
|
||||
describe("<QRCode />", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("shows a spinner when data is null", async () => {
|
||||
const { container } = render(<QRCode data={null} />);
|
||||
expect(container.querySelector(".mx_Spinner")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders a QR with defaults", async () => {
|
||||
const { container, getAllByAltText } = render(<QRCode data="asd" />);
|
||||
await waitFor(() => getAllByAltText("QR Code").length === 1);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders a QR with high error correction level", async () => {
|
||||
const { container, getAllByAltText } = render(<QRCode data="asd" errorCorrectionLevel="high" />);
|
||||
await waitFor(() => getAllByAltText("QR Code").length === 1);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import * as testUtils from "../../../../test-utils";
|
||||
import { getParentEventId } from "../../../../../src/utils/Reply";
|
||||
|
||||
describe("ReplyChain", () => {
|
||||
describe("getParentEventId", () => {
|
||||
it("retrieves relation reply from unedited event", () => {
|
||||
const originalEventWithRelation = testUtils.mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"msgtype": "m.text",
|
||||
"body": "> Reply to this message\n\n foo",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og",
|
||||
},
|
||||
},
|
||||
},
|
||||
user: "some_other_user",
|
||||
room: "room_id",
|
||||
});
|
||||
|
||||
expect(getParentEventId(originalEventWithRelation)).toStrictEqual(
|
||||
"$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og",
|
||||
);
|
||||
});
|
||||
|
||||
it("retrieves relation reply from original event when edited", () => {
|
||||
const originalEventWithRelation = testUtils.mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"msgtype": "m.text",
|
||||
"body": "> Reply to this message\n\n foo",
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og",
|
||||
},
|
||||
},
|
||||
},
|
||||
user: "some_other_user",
|
||||
room: "room_id",
|
||||
});
|
||||
|
||||
const editEvent = testUtils.mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"msgtype": "m.text",
|
||||
"body": "> Reply to this message\n\n * foo bar",
|
||||
"m.new_content": {
|
||||
msgtype: "m.text",
|
||||
body: "foo bar",
|
||||
},
|
||||
"m.relates_to": {
|
||||
rel_type: "m.replace",
|
||||
event_id: originalEventWithRelation.getId(),
|
||||
},
|
||||
},
|
||||
user: "some_other_user",
|
||||
room: "room_id",
|
||||
});
|
||||
|
||||
// The edit replaces the original event
|
||||
originalEventWithRelation.makeReplaced(editEvent);
|
||||
|
||||
// The relation should be pulled from the original event
|
||||
expect(getParentEventId(originalEventWithRelation)).toStrictEqual(
|
||||
"$qkjmFBTEc0VvfVyzq1CJuh1QZi_xDIgNEFjZ4Pq34og",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { mkRoom, mkRoomMember, stubClient, withClientContextRenderOptions } from "../../../../test-utils";
|
||||
import RoomFacePile from "../../../../../src/components/views/elements/RoomFacePile";
|
||||
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
|
||||
describe("<RoomFacePile />", () => {
|
||||
it("renders", () => {
|
||||
const cli = stubClient();
|
||||
DMRoomMap.makeShared(cli);
|
||||
const room = mkRoom(cli, "!123");
|
||||
|
||||
jest.spyOn(room, "getJoinedMembers").mockReturnValue([
|
||||
mkRoomMember(room.roomId, "@bob:example.org", KnownMembership.Join),
|
||||
]);
|
||||
|
||||
const { asFragment } = render(
|
||||
<RoomFacePile onlyKnownUsers={false} room={room} />,
|
||||
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
107
test/unit-tests/components/views/elements/RoomTopic-test.tsx
Normal file
107
test/unit-tests/components/views/elements/RoomTopic-test.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { fireEvent, render, screen, waitFor } from "jest-matrix-react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { mkEvent, stubClient } from "../../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import RoomTopic from "../../../../../src/components/views/elements/RoomTopic";
|
||||
import dis from "../../../../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../../../../src/dispatcher/actions";
|
||||
|
||||
jest.mock("../../../../../src/dispatcher/dispatcher");
|
||||
|
||||
describe("<RoomTopic/>", () => {
|
||||
const originalHref = window.location.href;
|
||||
|
||||
afterEach(() => {
|
||||
window.location.href = originalHref;
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a room with the given topic
|
||||
* @param topic
|
||||
*/
|
||||
function createRoom(topic: string) {
|
||||
stubClient();
|
||||
const room = new Room("!pMBteVpcoJRdCJxDmn:matrix.org", MatrixClientPeg.safeGet(), "@alice:example.org");
|
||||
const topicEvent = mkEvent({
|
||||
type: "m.room.topic",
|
||||
room: "!pMBteVpcoJRdCJxDmn:matrix.org",
|
||||
user: "@alice:example.org",
|
||||
content: { topic },
|
||||
ts: 123,
|
||||
event: true,
|
||||
});
|
||||
room.addLiveEvents([topicEvent]);
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a room and render it
|
||||
* @param topic
|
||||
*/
|
||||
const renderRoom = (topic: string) => {
|
||||
const room = createRoom(topic);
|
||||
render(<RoomTopic room={room} />);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a room and click on the given text
|
||||
* @param topic
|
||||
* @param clickText
|
||||
*/
|
||||
function runClickTest(topic: string, clickText: string) {
|
||||
renderRoom(topic);
|
||||
fireEvent.click(screen.getByText(clickText));
|
||||
}
|
||||
|
||||
it("should capture permalink clicks", () => {
|
||||
const permalink =
|
||||
"https://matrix.to/#/!pMBteVpcoJRdCJxDmn:matrix.org/$K4Kg0fL-GKpW1EQ6lS36bP4eUXadWJFkdK_FH73Df8A?via=matrix.org";
|
||||
const expectedHref =
|
||||
"http://localhost/#/room/!pMBteVpcoJRdCJxDmn:matrix.org/$K4Kg0fL-GKpW1EQ6lS36bP4eUXadWJFkdK_FH73Df8A?via=matrix.org";
|
||||
runClickTest(`... ${permalink} ...`, permalink);
|
||||
expect(window.location.href).toEqual(expectedHref);
|
||||
expect(dis.fire).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("should not capture non-permalink clicks", () => {
|
||||
const link = "https://matrix.org";
|
||||
const expectedHref = originalHref;
|
||||
runClickTest(`... ${link} ...`, link);
|
||||
expect(window.location.href).toEqual(expectedHref);
|
||||
expect(dis.fire).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("should open topic dialog when not clicking a link", () => {
|
||||
const topic = "foobar";
|
||||
const expectedHref = originalHref;
|
||||
runClickTest(topic, topic);
|
||||
expect(window.location.href).toEqual(expectedHref);
|
||||
expect(dis.fire).toHaveBeenCalledWith(Action.ShowRoomTopic);
|
||||
});
|
||||
|
||||
it("should open the tooltip when hovering a text", async () => {
|
||||
const topic = "room topic";
|
||||
renderRoom(topic);
|
||||
await userEvent.hover(screen.getByText(topic));
|
||||
await waitFor(() => expect(screen.getByRole("tooltip", { name: "Click to read topic" })).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it("should not open the tooltip when hovering a link", async () => {
|
||||
const topic = "https://matrix.org";
|
||||
renderRoom(topic);
|
||||
await userEvent.hover(screen.getByText(topic));
|
||||
await waitFor(() => expect(screen.queryByRole("tooltip", { name: "Click to read topic" })).toBeNull());
|
||||
});
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
|
||||
import SdkConfig from "../../../../../src/SdkConfig";
|
||||
import SearchWarning, { WarningKind } from "../../../../../src/components/views/elements/SearchWarning";
|
||||
|
||||
describe("<SearchWarning />", () => {
|
||||
describe("with desktop builds available", () => {
|
||||
beforeEach(() => {
|
||||
SdkConfig.put({
|
||||
brand: "Element",
|
||||
desktop_builds: {
|
||||
available: true,
|
||||
logo: "https://logo",
|
||||
url: "https://url",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("renders with a logo by default", () => {
|
||||
const { asFragment, getByRole } = render(
|
||||
<SearchWarning isRoomEncrypted={true} kind={WarningKind.Search} />,
|
||||
);
|
||||
expect(getByRole("img")).toHaveAttribute("src", "https://logo");
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders without a logo when showLogo=false", () => {
|
||||
const { asFragment, queryByRole } = render(
|
||||
<SearchWarning isRoomEncrypted={true} kind={WarningKind.Search} showLogo={false} />,
|
||||
);
|
||||
|
||||
expect(queryByRole("img")).not.toBeInTheDocument();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render, screen, waitForElementToBeRemoved } from "jest-matrix-react";
|
||||
|
||||
import SpellCheckLanguagesDropdown from "../../../../../src/components/views/elements/SpellCheckLanguagesDropdown";
|
||||
import PlatformPeg from "../../../../../src/PlatformPeg";
|
||||
|
||||
describe("<SpellCheckLanguagesDropdown />", () => {
|
||||
it("renders as expected", async () => {
|
||||
const platform: any = {
|
||||
getAvailableSpellCheckLanguages: jest.fn().mockResolvedValue(["en", "de", "qq"]),
|
||||
supportsSetting: jest.fn(),
|
||||
};
|
||||
PlatformPeg.set(platform);
|
||||
|
||||
const { asFragment } = render(
|
||||
<SpellCheckLanguagesDropdown
|
||||
className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
|
||||
value="en"
|
||||
onOptionChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
await waitForElementToBeRemoved(() => screen.queryAllByLabelText("Loading…"));
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { fireEvent, render, RenderResult } from "jest-matrix-react";
|
||||
|
||||
import StyledRadioGroup from "../../../../../src/components/views/elements/StyledRadioGroup";
|
||||
|
||||
describe("<StyledRadioGroup />", () => {
|
||||
const optionA = {
|
||||
value: "Anteater",
|
||||
label: <span>Anteater label</span>,
|
||||
description: "anteater description",
|
||||
className: "a-class",
|
||||
};
|
||||
const optionB = {
|
||||
value: "Badger",
|
||||
label: <span>Badger label</span>,
|
||||
};
|
||||
const optionC = {
|
||||
value: "Canary",
|
||||
label: <span>Canary label</span>,
|
||||
description: <span>Canary description</span>,
|
||||
};
|
||||
const defaultDefinitions = [optionA, optionB, optionC];
|
||||
const defaultProps = {
|
||||
name: "test",
|
||||
className: "test-class",
|
||||
definitions: defaultDefinitions,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const getComponent = (props = {}) => render(<StyledRadioGroup {...defaultProps} {...props} />);
|
||||
|
||||
const getInputByValue = (component: RenderResult, value: string) =>
|
||||
component.container.querySelector<HTMLInputElement>(`input[value="${value}"]`);
|
||||
const getCheckedInput = (component: RenderResult) =>
|
||||
component.container.querySelector<HTMLInputElement>("input[checked]");
|
||||
|
||||
it("renders radios correctly when no value is provided", () => {
|
||||
const component = getComponent();
|
||||
|
||||
expect(component.asFragment()).toMatchSnapshot();
|
||||
expect(getCheckedInput(component)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("selects correct button when value is provided", () => {
|
||||
const component = getComponent({
|
||||
value: optionC.value,
|
||||
});
|
||||
|
||||
expect(getCheckedInput(component)?.value).toEqual(optionC.value);
|
||||
});
|
||||
|
||||
it("selects correct buttons when definitions have checked prop", () => {
|
||||
const definitions = [{ ...optionA, checked: true }, optionB, { ...optionC, checked: false }];
|
||||
const component = getComponent({
|
||||
value: optionC.value,
|
||||
definitions,
|
||||
});
|
||||
|
||||
expect(getInputByValue(component, optionA.value)).toBeChecked();
|
||||
expect(getInputByValue(component, optionB.value)).not.toBeChecked();
|
||||
// optionC.checked = false overrides value matching
|
||||
expect(getInputByValue(component, optionC.value)).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("disables individual buttons based on definition.disabled", () => {
|
||||
const definitions = [optionA, { ...optionB, disabled: true }, { ...optionC, disabled: true }];
|
||||
const component = getComponent({ definitions });
|
||||
expect(getInputByValue(component, optionA.value)).not.toBeDisabled();
|
||||
expect(getInputByValue(component, optionB.value)).toBeDisabled();
|
||||
expect(getInputByValue(component, optionC.value)).toBeDisabled();
|
||||
});
|
||||
|
||||
it("disables all buttons with disabled prop", () => {
|
||||
const component = getComponent({ disabled: true });
|
||||
expect(getInputByValue(component, optionA.value)).toBeDisabled();
|
||||
expect(getInputByValue(component, optionB.value)).toBeDisabled();
|
||||
expect(getInputByValue(component, optionC.value)).toBeDisabled();
|
||||
});
|
||||
|
||||
it("calls onChange on click", () => {
|
||||
const onChange = jest.fn();
|
||||
const component = getComponent({
|
||||
value: optionC.value,
|
||||
onChange,
|
||||
});
|
||||
|
||||
fireEvent.click(getInputByValue(component, optionB.value)!);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(optionB.value);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/* eslint @typescript-eslint/no-unused-vars: ["error", { "varsIgnorePattern": "^_" }] */
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { render, waitFor } from "jest-matrix-react";
|
||||
import hljs, { type HighlightOptions } from "highlight.js";
|
||||
import React from "react";
|
||||
|
||||
import SyntaxHighlight from "../../../../../src/components/views/elements/SyntaxHighlight";
|
||||
|
||||
describe("<SyntaxHighlight />", () => {
|
||||
it("renders", async () => {
|
||||
const { container } = render(<SyntaxHighlight>console.log("Hello, World!");</SyntaxHighlight>);
|
||||
await waitFor(() => expect(container.querySelector(".language-arcade")).toBeTruthy());
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each(["json", "javascript", "css"])("uses the provided language", async (lang) => {
|
||||
const mock = jest.spyOn(hljs, "highlight");
|
||||
|
||||
const { container } = render(<SyntaxHighlight language={lang}>// Hello, World</SyntaxHighlight>);
|
||||
await waitFor(() => expect(container.querySelector(`.language-${lang}`)).toBeTruthy());
|
||||
|
||||
const [_lang, opts] = mock.mock.lastCall!;
|
||||
expect((opts as unknown as HighlightOptions)["language"]).toBe(lang);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<AccessibleButton /> renders a button element 1`] = `
|
||||
<DocumentFragment>
|
||||
<button
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
i am a button
|
||||
</button>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<AccessibleButton /> renders div with role button by default 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AccessibleButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
i am a button
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<AccessibleButton /> renders with correct classes when button has kind 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
i am a button
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,471 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AppTile destroys non-persisted right panel widget on room change 1`] = `
|
||||
<DocumentFragment>
|
||||
<aside
|
||||
class="mx_RightPanel"
|
||||
id="mx_RightPanel"
|
||||
>
|
||||
<div
|
||||
class="mx_BaseCard mx_WidgetCard"
|
||||
>
|
||||
<div
|
||||
class="mx_BaseCard_header"
|
||||
>
|
||||
<div
|
||||
class="mx_BaseCard_header_title"
|
||||
>
|
||||
<h4
|
||||
class="mx_Heading_h4 mx_BaseCard_header_title_heading"
|
||||
>
|
||||
Example 1
|
||||
</h4>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Options"
|
||||
class="mx_AccessibleButton mx_BaseCard_header_title_button--option"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
aria-labelledby="floating-ui-1"
|
||||
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
|
||||
data-testid="base-card-close-button"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 28px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppTileFullWidth"
|
||||
id="1"
|
||||
>
|
||||
<div
|
||||
class="mx_AppTileBody mx_AppTileBody--large"
|
||||
>
|
||||
<div
|
||||
class="mx_AppTileBody_fadeInSpinner"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner_Msg"
|
||||
>
|
||||
Loading…
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`AppTile for a persistent app should render 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AppTile_mini"
|
||||
id="1"
|
||||
>
|
||||
<div
|
||||
class="mx_AppTile_persistedWrapper"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`AppTile for a pinned widget should render 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AppTile"
|
||||
id="1"
|
||||
>
|
||||
<div
|
||||
class="mx_AppTileMenuBar"
|
||||
>
|
||||
<span
|
||||
class="mx_AppTileMenuBar_title"
|
||||
style="pointer-events: none;"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar mx_WidgetAvatar"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_mcap2_50"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="image-file-stub"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
<h3>
|
||||
Example 1
|
||||
</h3>
|
||||
<span />
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="mx_AppTileMenuBar_widgets"
|
||||
>
|
||||
<div
|
||||
aria-label="Un-maximise"
|
||||
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_12"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Minimise"
|
||||
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_12"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Options"
|
||||
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_12"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppTile_persistedWrapper"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`AppTile for a pinned widget should render permission request 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AppTile"
|
||||
id="1"
|
||||
>
|
||||
<div
|
||||
class="mx_AppTileMenuBar"
|
||||
>
|
||||
<span
|
||||
class="mx_AppTileMenuBar_title"
|
||||
style="pointer-events: none;"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar mx_WidgetAvatar"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_mcap2_50"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="image-file-stub"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
<h3>
|
||||
Example 1
|
||||
</h3>
|
||||
<span />
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="mx_AppTileMenuBar_widgets"
|
||||
>
|
||||
<div
|
||||
aria-label="Un-maximise"
|
||||
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_12"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Minimise"
|
||||
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_12"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Options"
|
||||
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_12"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppTileBody mx_AppTileBody--large"
|
||||
>
|
||||
<div
|
||||
class="mx_AppPermission"
|
||||
>
|
||||
<div
|
||||
class="mx_AppPermission_content"
|
||||
>
|
||||
<div
|
||||
class="mx_AppPermission_content_bolder"
|
||||
>
|
||||
Widget added by
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 38px;"
|
||||
>
|
||||
u
|
||||
</span>
|
||||
<h4
|
||||
class="mx_Heading_h4"
|
||||
>
|
||||
@userAnother
|
||||
</h4>
|
||||
<div />
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
Using this widget may share data
|
||||
<div
|
||||
aria-describedby="floating-ui-87"
|
||||
aria-labelledby="floating-ui-86"
|
||||
class="mx_TextWithTooltip_target mx_TextWithTooltip_target--helpIcon"
|
||||
>
|
||||
<svg
|
||||
class="mx_Icon mx_Icon_12"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 8a1.5 1.5 0 0 0-1.5 1.5 1 1 0 1 1-2 0 3.5 3.5 0 1 1 6.01 2.439c-.122.126-.24.243-.352.355-.287.288-.54.54-.76.824-.293.375-.398.651-.398.882a1 1 0 1 1-2 0c0-.874.407-1.58.819-2.11.305-.392.688-.775 1-1.085l.257-.26A1.5 1.5 0 0 0 12 8Zm1 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
|
||||
/>
|
||||
<path
|
||||
d="M8.1 21.212A9.738 9.738 0 0 0 12 22a9.738 9.738 0 0 0 3.9-.788 10.098 10.098 0 0 0 3.175-2.137c.9-.9 1.613-1.958 2.137-3.175A9.738 9.738 0 0 0 22 12a9.738 9.738 0 0 0-.788-3.9 10.099 10.099 0 0 0-2.137-3.175c-.9-.9-1.958-1.612-3.175-2.137A9.738 9.738 0 0 0 12 2a9.738 9.738 0 0 0-3.9.788 10.099 10.099 0 0 0-3.175 2.137c-.9.9-1.612 1.958-2.137 3.175A9.738 9.738 0 0 0 2 12a9.74 9.74 0 0 0 .788 3.9 10.098 10.098 0 0 0 2.137 3.175c.9.9 1.958 1.613 3.175 2.137Zm9.575-3.537C16.125 19.225 14.233 20 12 20c-2.233 0-4.125-.775-5.675-2.325C4.775 16.125 4 14.233 4 12c0-2.233.775-4.125 2.325-5.675C7.875 4.775 9.767 4 12 4c2.233 0 4.125.775 5.675 2.325C19.225 7.875 20 9.767 20 12c0 2.233-.775 4.125-2.325 5.675Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
with example.com.
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
This widget may use cookies.
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Continue
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`AppTile preserves non-persisted widget on container move 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AppsDrawer"
|
||||
>
|
||||
<div
|
||||
class="mx_AppsDrawer_resizer"
|
||||
style="position: relative; user-select: auto; width: auto; height: 280px; max-height: 576px; min-height: 100px; box-sizing: border-box; flex-shrink: 0;"
|
||||
>
|
||||
<div
|
||||
class="mx_AppsContainer"
|
||||
>
|
||||
<div
|
||||
class="mx_AppTileFullWidth"
|
||||
id="1"
|
||||
>
|
||||
<div
|
||||
class="mx_AppTileMenuBar"
|
||||
>
|
||||
<span
|
||||
class="mx_AppTileMenuBar_title"
|
||||
style="pointer-events: none;"
|
||||
>
|
||||
<span>
|
||||
<span
|
||||
aria-label="Avatar"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar mx_WidgetAvatar"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
style="--cpd-avatar-size: 20px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_mcap2_50"
|
||||
data-type="round"
|
||||
height="20px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="image-file-stub"
|
||||
width="20px"
|
||||
/>
|
||||
</span>
|
||||
<h3>
|
||||
Example 1
|
||||
</h3>
|
||||
<span />
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="mx_AppTileMenuBar_widgets"
|
||||
>
|
||||
<div
|
||||
aria-label="Maximise"
|
||||
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_12"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Minimise"
|
||||
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_12"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Options"
|
||||
class="mx_AccessibleButton mx_AppTileMenuBar_widgets_button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Icon mx_Icon_12"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppTileBody mx_AppTileBody--large"
|
||||
>
|
||||
<div
|
||||
class="mx_AppTileBody_fadeInSpinner"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner_Msg"
|
||||
>
|
||||
Loading…
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-label="Loading…"
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AppsDrawer_resizer_container"
|
||||
>
|
||||
<div
|
||||
class="mx_AppsDrawer_resizer_container_handle"
|
||||
style="position: absolute; user-select: none; width: 100%; height: 10px; left: 0px; cursor: row-resize; bottom: -5px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,12 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<EffectsOverlay/> should render 1`] = `
|
||||
<DocumentFragment>
|
||||
<canvas
|
||||
aria-hidden="true"
|
||||
height="768"
|
||||
style="display: block; z-index: 999999; pointer-events: none; position: fixed; top: 0px; right: 0px;"
|
||||
width="100"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,40 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ExternalLink /> renders link correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<a
|
||||
class="mx_ExternalLink myCustomClass"
|
||||
data-testid="test"
|
||||
href="test.com"
|
||||
rel="noopener"
|
||||
target="_self"
|
||||
>
|
||||
<span>
|
||||
react element
|
||||
<b>
|
||||
children
|
||||
</b>
|
||||
</span>
|
||||
<i
|
||||
class="mx_ExternalLink_icon"
|
||||
/>
|
||||
</a>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<ExternalLink /> renders plain text link correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<a
|
||||
class="mx_ExternalLink myCustomClass"
|
||||
data-testid="test"
|
||||
href="test.com"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
test
|
||||
<i
|
||||
class="mx_ExternalLink_icon"
|
||||
/>
|
||||
</a>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,27 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<FacePile /> renders with a tooltip 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-labelledby="floating-ui-1"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_stacked-avatars_mcap2_111"
|
||||
>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="4"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
4
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,137 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<FilterDropdown /> renders dropdown options in menu 1`] = `
|
||||
<ul
|
||||
class="mx_Dropdown_menu"
|
||||
id="test_listbox"
|
||||
role="listbox"
|
||||
>
|
||||
<li
|
||||
aria-selected="true"
|
||||
class="mx_Dropdown_option mx_Dropdown_option_highlight"
|
||||
id="test__one"
|
||||
role="option"
|
||||
>
|
||||
<div
|
||||
class="mx_FilterDropdown_option"
|
||||
data-testid="filter-option-one"
|
||||
>
|
||||
<div
|
||||
class="mx_FilterDropdown_optionSelectedIcon"
|
||||
/>
|
||||
<span
|
||||
class="mx_FilterDropdown_optionLabel"
|
||||
>
|
||||
Option one
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
aria-selected="false"
|
||||
class="mx_Dropdown_option"
|
||||
id="test__two"
|
||||
role="option"
|
||||
>
|
||||
<div
|
||||
class="mx_FilterDropdown_option"
|
||||
data-testid="filter-option-two"
|
||||
>
|
||||
<span
|
||||
class="mx_FilterDropdown_optionLabel"
|
||||
>
|
||||
Option two
|
||||
</span>
|
||||
<span
|
||||
class="mx_FilterDropdown_optionDescription"
|
||||
>
|
||||
with description
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
`;
|
||||
|
||||
exports[`<FilterDropdown /> renders selected option 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_Dropdown mx_FilterDropdown test"
|
||||
>
|
||||
<div
|
||||
aria-describedby="test_value"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-label="test label"
|
||||
aria-owns="test_input"
|
||||
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Dropdown_option"
|
||||
id="test_value"
|
||||
>
|
||||
Option one
|
||||
</div>
|
||||
<span
|
||||
class="mx_Dropdown_arrow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<FilterDropdown /> renders selected option with selectedLabel 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_Dropdown mx_FilterDropdown test"
|
||||
>
|
||||
<div
|
||||
aria-describedby="test_value"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-label="test label"
|
||||
aria-owns="test_input"
|
||||
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Dropdown_option"
|
||||
id="test_value"
|
||||
>
|
||||
Show: Option one
|
||||
</div>
|
||||
<span
|
||||
class="mx_Dropdown_arrow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<FilterDropdown /> renders when selected option is not in options 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_Dropdown mx_FilterDropdown test"
|
||||
>
|
||||
<div
|
||||
aria-describedby="test_value"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-label="test label"
|
||||
aria-owns="test_input"
|
||||
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Dropdown_option"
|
||||
id="test_value"
|
||||
/>
|
||||
<span
|
||||
class="mx_Dropdown_arrow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,48 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<FilterTabGroup /> renders options 1`] = `
|
||||
<div>
|
||||
<fieldset
|
||||
class="mx_FilterTabGroup"
|
||||
data-testid="test"
|
||||
>
|
||||
<label
|
||||
data-testid="filter-tab-test-Apple"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
name="test"
|
||||
type="radio"
|
||||
value="Apple"
|
||||
/>
|
||||
<span>
|
||||
Label for Apple
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
data-testid="filter-tab-test-Banana"
|
||||
>
|
||||
<input
|
||||
name="test"
|
||||
type="radio"
|
||||
value="Banana"
|
||||
/>
|
||||
<span>
|
||||
Label for Banana
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
data-testid="filter-tab-test-Orange"
|
||||
>
|
||||
<input
|
||||
name="test"
|
||||
type="radio"
|
||||
value="Orange"
|
||||
/>
|
||||
<span>
|
||||
Label for Orange
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,82 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ImageView /> renders correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-label="Image view"
|
||||
class="mx_ImageView"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_ImageView_panel"
|
||||
>
|
||||
<div />
|
||||
<div
|
||||
class="mx_ImageView_toolbar"
|
||||
>
|
||||
<div
|
||||
aria-describedby="floating-ui-2"
|
||||
aria-label="Zoom out"
|
||||
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_zoomOut"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-label="Zoom in"
|
||||
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_zoomIn"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-label="Rotate Left"
|
||||
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_rotateCCW"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-label="Rotate Right"
|
||||
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_rotateCW"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-label="Download"
|
||||
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_download"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-label="Close"
|
||||
class="mx_AccessibleButton mx_ImageView_button mx_ImageView_button_close"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_ImageView_image_wrapper"
|
||||
>
|
||||
<img
|
||||
class="mx_ImageView_image "
|
||||
draggable="true"
|
||||
src="https://example.com/image.png"
|
||||
style="transform: translateX(0px)
|
||||
translateY(0px)
|
||||
scale(0)
|
||||
rotate(0deg); cursor: zoom-out;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,17 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`InfoTooltip should show tooltip on hover 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-describedby="floating-ui-2"
|
||||
class="mx_InfoTooltip"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
aria-label="Information"
|
||||
class="mx_InfoTooltip_icon mx_InfoTooltip_icon_info"
|
||||
/>
|
||||
Trigger text
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,82 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<LabelledCheckbox /> should render with byline of "this is a byline" 1`] = `
|
||||
<DocumentFragment>
|
||||
<label
|
||||
class="mx_LabelledCheckbox"
|
||||
>
|
||||
<span
|
||||
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
id="checkbox_vY7Q4uEh9K"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="checkbox_vY7Q4uEh9K"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_background"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_checkmark"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
<div
|
||||
class="mx_LabelledCheckbox_labels"
|
||||
>
|
||||
<span
|
||||
class="mx_LabelledCheckbox_label"
|
||||
>
|
||||
Hello world
|
||||
</span>
|
||||
<span
|
||||
class="mx_LabelledCheckbox_byline"
|
||||
>
|
||||
this is a byline
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<LabelledCheckbox /> should render with byline of undefined 1`] = `
|
||||
<DocumentFragment>
|
||||
<label
|
||||
class="mx_LabelledCheckbox"
|
||||
>
|
||||
<span
|
||||
class="mx_Checkbox mx_Checkbox_hasKind mx_Checkbox_kind_solid"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
id="checkbox_vY7Q4uEh9K"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="checkbox_vY7Q4uEh9K"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_background"
|
||||
>
|
||||
<div
|
||||
class="mx_Checkbox_checkmark"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
<div
|
||||
class="mx_LabelledCheckbox_labels"
|
||||
>
|
||||
<span
|
||||
class="mx_LabelledCheckbox_label"
|
||||
>
|
||||
Hello world
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,14 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<LearnMore /> renders button 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||
data-testid="testid"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Learn more
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,294 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Pill> should not render a non-permalink 1`] = `
|
||||
<DocumentFragment>
|
||||
<div />
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<Pill> should not render an avatar or link when called with inMessage = false and shouldShowPillAvatar = false 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<bdi>
|
||||
<span
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_Pill mx_RoomPill"
|
||||
>
|
||||
<span
|
||||
class="mx_Pill_text"
|
||||
>
|
||||
Room 1
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</bdi>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<Pill> should render the expected pill for @room 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<bdi>
|
||||
<span
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_Pill mx_AtRoomPill"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 16px;"
|
||||
>
|
||||
R
|
||||
</span>
|
||||
<span
|
||||
class="mx_Pill_text"
|
||||
>
|
||||
@room
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</bdi>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<Pill> should render the expected pill for a known user not in the room 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<bdi>
|
||||
<a
|
||||
class="mx_Pill mx_UserPill"
|
||||
href="https://matrix.to/#/@user2:example.com"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="5"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 16px;"
|
||||
>
|
||||
U
|
||||
</span>
|
||||
<span
|
||||
class="mx_Pill_text"
|
||||
>
|
||||
User 2
|
||||
</span>
|
||||
</a>
|
||||
</bdi>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<Pill> should render the expected pill for a message in another room 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<bdi>
|
||||
<a
|
||||
class="mx_Pill mx_EventPill"
|
||||
href="https://matrix.to/#/!room1:example.com/$123-456"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 16px;"
|
||||
>
|
||||
R
|
||||
</span>
|
||||
<span
|
||||
class="mx_Pill_text"
|
||||
>
|
||||
Message in Room 1
|
||||
</span>
|
||||
</a>
|
||||
</bdi>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<Pill> should render the expected pill for a message in the same room 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<bdi>
|
||||
<a
|
||||
class="mx_Pill mx_EventPill"
|
||||
href="https://matrix.to/#/!room1:example.com/$123-456"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="4"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 16px;"
|
||||
>
|
||||
U
|
||||
</span>
|
||||
<span
|
||||
class="mx_Pill_text"
|
||||
>
|
||||
Message from User 1
|
||||
</span>
|
||||
</a>
|
||||
</bdi>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<Pill> should render the expected pill for a room alias 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<bdi>
|
||||
<a
|
||||
class="mx_Pill mx_RoomPill"
|
||||
href="https://matrix.to/#/#room1:example.com"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 16px;"
|
||||
>
|
||||
R
|
||||
</span>
|
||||
<span
|
||||
class="mx_Pill_text"
|
||||
>
|
||||
Room 1
|
||||
</span>
|
||||
</a>
|
||||
</bdi>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<Pill> should render the expected pill for a space 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<bdi>
|
||||
<a
|
||||
class="mx_Pill mx_RoomPill"
|
||||
href="https://matrix.to/#/!space1:example.com"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="2"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 16px;"
|
||||
>
|
||||
S
|
||||
</span>
|
||||
<span
|
||||
class="mx_Pill_text"
|
||||
>
|
||||
Space 1
|
||||
</span>
|
||||
</a>
|
||||
</bdi>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<Pill> should render the expected pill for an uknown user not in the room 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<bdi>
|
||||
<a
|
||||
class="mx_Pill mx_UserPill"
|
||||
href="https://matrix.to/#/@user3:example.com"
|
||||
>
|
||||
<div
|
||||
class="mx_Pill_UserIcon mx_BaseAvatar"
|
||||
/>
|
||||
<span
|
||||
class="mx_Pill_text"
|
||||
>
|
||||
@user3:example.com
|
||||
</span>
|
||||
</a>
|
||||
</bdi>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<Pill> when rendering a pill for a room should render the expected pill 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<bdi>
|
||||
<a
|
||||
class="mx_Pill mx_RoomPill"
|
||||
href="https://matrix.to/#/!room1:example.com"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="1"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 16px;"
|
||||
>
|
||||
R
|
||||
</span>
|
||||
<span
|
||||
class="mx_Pill_text"
|
||||
>
|
||||
Room 1
|
||||
</span>
|
||||
</a>
|
||||
</bdi>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<Pill> when rendering a pill for a user in the room should render as expected 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<bdi>
|
||||
<a
|
||||
class="mx_Pill mx_UserPill"
|
||||
href="https://matrix.to/#/@user1:example.com"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="4"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 16px;"
|
||||
>
|
||||
U
|
||||
</span>
|
||||
<span
|
||||
class="mx_Pill_text"
|
||||
>
|
||||
User 1
|
||||
</span>
|
||||
</a>
|
||||
</bdi>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,560 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PollCreateDialog renders a blank poll 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-describedby="mx_CompoundDialog_content"
|
||||
aria-labelledby="mx_CompoundDialog_title"
|
||||
class="mx_CompoundDialog mx_ScrollableBaseDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_CompoundDialog_header"
|
||||
>
|
||||
<h1>
|
||||
Create poll
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_CompoundDialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
<form
|
||||
class="mx_CompoundDialog_form"
|
||||
>
|
||||
<div
|
||||
class="mx_CompoundDialog_content"
|
||||
>
|
||||
<div
|
||||
class="mx_PollCreateDialog"
|
||||
>
|
||||
<h2>
|
||||
Poll type
|
||||
</h2>
|
||||
<div
|
||||
class="mx_Field mx_Field_select"
|
||||
>
|
||||
<select
|
||||
id="mx_Field_1"
|
||||
type="text"
|
||||
>
|
||||
<option
|
||||
value="org.matrix.msc3381.poll.disclosed"
|
||||
>
|
||||
Open poll
|
||||
</option>
|
||||
<option
|
||||
value="org.matrix.msc3381.poll.undisclosed"
|
||||
>
|
||||
Closed poll
|
||||
</option>
|
||||
</select>
|
||||
<label
|
||||
for="mx_Field_1"
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
Voters see results as soon as they have voted
|
||||
</p>
|
||||
<h2>
|
||||
What is your poll question or topic?
|
||||
</h2>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
|
||||
>
|
||||
<input
|
||||
id="poll-topic-input"
|
||||
label="Question or topic"
|
||||
maxlength="340"
|
||||
placeholder="Write something…"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="poll-topic-input"
|
||||
>
|
||||
Question or topic
|
||||
</label>
|
||||
</div>
|
||||
<h2>
|
||||
Create options
|
||||
</h2>
|
||||
<div
|
||||
class="mx_PollCreateDialog_option"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
|
||||
>
|
||||
<input
|
||||
id="pollcreate_option_0"
|
||||
label="Option 1"
|
||||
maxlength="340"
|
||||
placeholder="Write an option"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="pollcreate_option_0"
|
||||
>
|
||||
Option 1
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_PollCreateDialog_option"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
|
||||
>
|
||||
<input
|
||||
id="pollcreate_option_1"
|
||||
label="Option 2"
|
||||
maxlength="340"
|
||||
placeholder="Write an option"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="pollcreate_option_1"
|
||||
>
|
||||
Option 2
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Add option
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_CompoundDialog_footer"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
<button
|
||||
aria-disabled="true"
|
||||
class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled"
|
||||
disabled=""
|
||||
role="button"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
Create Poll
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`PollCreateDialog renders a question and some options 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-describedby="mx_CompoundDialog_content"
|
||||
aria-labelledby="mx_CompoundDialog_title"
|
||||
class="mx_CompoundDialog mx_ScrollableBaseDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_CompoundDialog_header"
|
||||
>
|
||||
<h1>
|
||||
Create poll
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_CompoundDialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
<form
|
||||
class="mx_CompoundDialog_form"
|
||||
>
|
||||
<div
|
||||
class="mx_CompoundDialog_content"
|
||||
>
|
||||
<div
|
||||
class="mx_PollCreateDialog"
|
||||
>
|
||||
<h2>
|
||||
Poll type
|
||||
</h2>
|
||||
<div
|
||||
class="mx_Field mx_Field_select"
|
||||
>
|
||||
<select
|
||||
id="mx_Field_4"
|
||||
type="text"
|
||||
>
|
||||
<option
|
||||
value="org.matrix.msc3381.poll.disclosed"
|
||||
>
|
||||
Open poll
|
||||
</option>
|
||||
<option
|
||||
value="org.matrix.msc3381.poll.undisclosed"
|
||||
>
|
||||
Closed poll
|
||||
</option>
|
||||
</select>
|
||||
<label
|
||||
for="mx_Field_4"
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
Voters see results as soon as they have voted
|
||||
</p>
|
||||
<h2>
|
||||
What is your poll question or topic?
|
||||
</h2>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
|
||||
>
|
||||
<input
|
||||
id="poll-topic-input"
|
||||
label="Question or topic"
|
||||
maxlength="340"
|
||||
placeholder="Write something…"
|
||||
type="text"
|
||||
value="How many turnips is the optimal number?"
|
||||
/>
|
||||
<label
|
||||
for="poll-topic-input"
|
||||
>
|
||||
Question or topic
|
||||
</label>
|
||||
</div>
|
||||
<h2>
|
||||
Create options
|
||||
</h2>
|
||||
<div
|
||||
class="mx_PollCreateDialog_option"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
|
||||
>
|
||||
<input
|
||||
id="pollcreate_option_0"
|
||||
label="Option 1"
|
||||
maxlength="340"
|
||||
placeholder="Write an option"
|
||||
type="text"
|
||||
value="As many as my neighbour"
|
||||
/>
|
||||
<label
|
||||
for="pollcreate_option_0"
|
||||
>
|
||||
Option 1
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_PollCreateDialog_option"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
|
||||
>
|
||||
<input
|
||||
id="pollcreate_option_1"
|
||||
label="Option 2"
|
||||
maxlength="340"
|
||||
placeholder="Write an option"
|
||||
type="text"
|
||||
value="The question is meaningless"
|
||||
/>
|
||||
<label
|
||||
for="pollcreate_option_1"
|
||||
>
|
||||
Option 2
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_PollCreateDialog_option"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
|
||||
>
|
||||
<input
|
||||
id="pollcreate_option_2"
|
||||
label="Option 3"
|
||||
maxlength="340"
|
||||
placeholder="Write an option"
|
||||
type="text"
|
||||
value="Mu"
|
||||
/>
|
||||
<label
|
||||
for="pollcreate_option_2"
|
||||
>
|
||||
Option 3
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Add option
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_CompoundDialog_footer"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
<button
|
||||
class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
Create Poll
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`PollCreateDialog renders info from a previous event 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
aria-describedby="mx_CompoundDialog_content"
|
||||
aria-labelledby="mx_CompoundDialog_title"
|
||||
class="mx_CompoundDialog mx_ScrollableBaseDialog"
|
||||
data-focus-lock-disabled="false"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
class="mx_CompoundDialog_header"
|
||||
>
|
||||
<h1>
|
||||
Edit poll
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Close dialog"
|
||||
class="mx_AccessibleButton mx_CompoundDialog_cancelButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
<form
|
||||
class="mx_CompoundDialog_form"
|
||||
>
|
||||
<div
|
||||
class="mx_CompoundDialog_content"
|
||||
>
|
||||
<div
|
||||
class="mx_PollCreateDialog"
|
||||
>
|
||||
<h2>
|
||||
Poll type
|
||||
</h2>
|
||||
<div
|
||||
class="mx_Field mx_Field_select"
|
||||
>
|
||||
<select
|
||||
id="mx_Field_5"
|
||||
type="text"
|
||||
>
|
||||
<option
|
||||
value="org.matrix.msc3381.poll.disclosed"
|
||||
>
|
||||
Open poll
|
||||
</option>
|
||||
<option
|
||||
value="org.matrix.msc3381.poll.undisclosed"
|
||||
>
|
||||
Closed poll
|
||||
</option>
|
||||
</select>
|
||||
<label
|
||||
for="mx_Field_5"
|
||||
/>
|
||||
</div>
|
||||
<p>
|
||||
Voters see results as soon as they have voted
|
||||
</p>
|
||||
<h2>
|
||||
What is your poll question or topic?
|
||||
</h2>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
|
||||
>
|
||||
<input
|
||||
id="poll-topic-input"
|
||||
label="Question or topic"
|
||||
maxlength="340"
|
||||
placeholder="Write something…"
|
||||
type="text"
|
||||
value="Poll Q"
|
||||
/>
|
||||
<label
|
||||
for="poll-topic-input"
|
||||
>
|
||||
Question or topic
|
||||
</label>
|
||||
</div>
|
||||
<h2>
|
||||
Create options
|
||||
</h2>
|
||||
<div
|
||||
class="mx_PollCreateDialog_option"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
|
||||
>
|
||||
<input
|
||||
id="pollcreate_option_0"
|
||||
label="Option 1"
|
||||
maxlength="340"
|
||||
placeholder="Write an option"
|
||||
type="text"
|
||||
value="Answer 1"
|
||||
/>
|
||||
<label
|
||||
for="pollcreate_option_0"
|
||||
>
|
||||
Option 1
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_PollCreateDialog_option"
|
||||
>
|
||||
<div
|
||||
class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"
|
||||
>
|
||||
<input
|
||||
id="pollcreate_option_1"
|
||||
label="Option 2"
|
||||
maxlength="340"
|
||||
placeholder="Write an option"
|
||||
type="text"
|
||||
value="Answer 2"
|
||||
/>
|
||||
<label
|
||||
for="pollcreate_option_1"
|
||||
>
|
||||
Option 2
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_PollCreateDialog_removeOption"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Add option
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_CompoundDialog_footer"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
<button
|
||||
class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
type="submit"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard="true"
|
||||
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,29 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<QRCode /> renders a QR with defaults 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_QRCode"
|
||||
>
|
||||
<img
|
||||
alt="QR Code"
|
||||
class="mx_VerificationQRCode"
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHQAAAB0CAYAAABUmhYnAAAAAklEQVR4AewaftIAAALASURBVO3BQW7kQAwEwSxC//9yro88NSBIM2sTjIg/WGMUa5RijVKsUYo1SrFGKdYoxRqlWKMUa5RijVKsUYo1SrFGKdYoxRrl4qEkfJPKSRI6lS4JJypdEr5J5YlijVKsUYo1ysXLVN6UhCeScKJyh8qbkvCmYo1SrFGKNcrFhyXhDpU7knCi0iWhS0KnckcS7lD5pGKNUqxRijXKxR+n0iXhDpVJijVKsUYp1igXw6h0SehUuiR0Kn9ZsUYp1ijFGuXiw1T+J5VPUvlNijVKsUYp1igXL0vCNyWhU+mS0Kk8kYTfrFijFGuUYo1y8ZDKb5KETqVLwh0qf0mxRinWKMUa5eKhJNyh0iXhk5JwotIl4UTljiR0Kp9UrFGKNUqxRrl4SKVLQqfSJaFT6ZLQqXyTSpeEO5LQqZwkoVN5olijFGuUYo1y8VASOpUTlTuScKLSJaFTOUlCp9KpnCThjiR8UrFGKdYoxRrl4j9LQqfypiR0Kp3KSRI6lU7lDpVPKtYoxRqlWKNcfFgSnkhCp3KHykkSOpU7ktCp3JGETuWJYo1SrFGKNUr8wR+WhCdUuiR0Kl0SOpU7knCi8kSxRinWKMUa5eKhJHyTyolKl4QnkvBEEr6pWKMUa5RijXLxMpU3JeEJlS4JXRJOVLokdEk4UTlJwpuKNUqxRinWKBcfloQ7VL5JpUtCl4QTlZMkdCqfVKxRijVKsUa5+ONUuiR0Kp3KiUqXhE7liSR0Km8q1ijFGqVYo1z8cUk4SUKn0iWhU+lUTpLQqXQqJ0noVJ4o1ijFGqVYo1x8mMo3qXRJOFE5SUKn0qmcJOFE5U3FGqVYoxRrlIuXJeE3UTlJQqdyRxI6lTuS0Kk8UaxRijVKsUaJP1hjFGuUYo1SrFGKNUqxRinWKMUapVijFGuUYo1SrFGKNUqxRinWKMUa5R93wRT2jE+pTAAAAABJRU5ErkJggg=="
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<QRCode /> renders a QR with high error correction level 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_QRCode"
|
||||
>
|
||||
<img
|
||||
alt="QR Code"
|
||||
class="mx_VerificationQRCode"
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHQAAAB0CAYAAABUmhYnAAAAAklEQVR4AewaftIAAAKwSURBVO3BQY6YUAwFwW6L+1/5ZZZefQkBk8RylfnBGqNYoxRrlGKNUqxRijVKsUYp1ijFGqVYoxRrlGKNUqxRijVKsUYp1igXD6n8piQ8oXKShE7lNyXhiWKNUqxRijXKxcuS8CaVE5UuCZ3KSRLuSMKbVN5UrFGKNUqxRrn4mModSXhC5SQJnUqXhDtU7kjCl4o1SrFGKdYoF8MkoVM5ScIkxRqlWKMUa5SLYVS6JHQqJ0n4nxVrlGKNUqxRLj6WhN+UhE6lS0Kn8kQS/iXFGqVYoxRrlIuXqUym8i8r1ijFGqVYo1w8lIS/KQmdypuS8D8p1ijFGqVYo1x8TKVLwonKE0noVDqVLgmdSpeETuVNSXhTsUYp1ijFGsX84EUqXRJOVLokvEnlS0m4Q+UkCU8Ua5RijVKsUcwPPqTypST8JpWTJNyh0iXhiWKNUqxRijXKxUMqdyShUzlJQqfSqXRJ6FS6JLwpCZ1Kl4ROpUvCm4o1SrFGKdYo5gcfUumScKJyRxI6lTuS0Kl0SehU7khCp3KShCeKNUqxRinWKBcfS8IdSbhD5Y4kdCpdEjqVLgknKidJ6FTeVKxRijVKsUa5eEjlNyXhjiR0Kr9JpUvCl4o1SrFGKdYoFy9LwptU7kjCSRI6lU6lS8KJSpeEE5UvFWuUYo1SrFEuPqZyRxLuSEKn0iWhU+mS8EQSOpW/qVijFGuUYo1yMZzKHSpdEjqVLgl3JOFLxRqlWKMUa5SL/5zKl1ROVE6S0KmcJOGJYo1SrFGKNcrFx5LwpSR0Kl0STlS6JJyodEnoVDqVLglfKtYoxRqlWKNcvEzlX6JykoQTlROVJ1S6JDxRrFGKNUqxRjE/WGMUa5RijVKsUYo1SrFGKdYoxRqlWKMUa5RijVKsUYo1SrFGKdYoxRrlD3iZAgP0EjHgAAAAAElFTkSuQmCC"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,28 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<RoomFacePile /> renders 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
aria-describedby="floating-ui-2"
|
||||
aria-labelledby="floating-ui-1"
|
||||
class="mx_AccessibleButton mx_FacePile"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_stacked-avatars_mcap2_111"
|
||||
>
|
||||
<span
|
||||
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
|
||||
data-color="4"
|
||||
data-testid="avatar-img"
|
||||
data-type="round"
|
||||
role="presentation"
|
||||
style="--cpd-avatar-size: 28px;"
|
||||
>
|
||||
b
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,50 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SearchWarning /> with desktop builds available renders with a logo by default 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_SearchWarning"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
src="https://logo"
|
||||
width="32px"
|
||||
/>
|
||||
<span>
|
||||
<span>
|
||||
Use the
|
||||
<a
|
||||
href="https://url"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Desktop app
|
||||
</a>
|
||||
to search encrypted messages
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<SearchWarning /> with desktop builds available renders without a logo when showLogo=false 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_SearchWarning"
|
||||
>
|
||||
<span>
|
||||
<span>
|
||||
Use the
|
||||
<a
|
||||
href="https://url"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Desktop app
|
||||
</a>
|
||||
to search encrypted messages
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,32 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SpellCheckLanguagesDropdown /> renders as expected 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_Dropdown mx_GeneralUserSettingsTab_spellCheckLanguageInput"
|
||||
>
|
||||
<div
|
||||
aria-describedby="mx_LanguageDropdown_value"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-label="Language Dropdown"
|
||||
aria-owns="mx_LanguageDropdown_input"
|
||||
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="mx_Dropdown_option"
|
||||
id="mx_LanguageDropdown_value"
|
||||
>
|
||||
<div>
|
||||
English
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="mx_Dropdown_arrow"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,89 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<StyledRadioGroup /> renders radios correctly when no value is provided 1`] = `
|
||||
<DocumentFragment>
|
||||
<label
|
||||
class="mx_StyledRadioButton test-class a-class mx_StyledRadioButton_enabled"
|
||||
>
|
||||
<input
|
||||
aria-describedby="test-Anteater-description"
|
||||
id="test-Anteater"
|
||||
name="test"
|
||||
type="radio"
|
||||
value="Anteater"
|
||||
/>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_content"
|
||||
>
|
||||
<span>
|
||||
Anteater label
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_spacer"
|
||||
/>
|
||||
</label>
|
||||
<span
|
||||
id="test-Anteater-description"
|
||||
>
|
||||
anteater description
|
||||
</span>
|
||||
<label
|
||||
class="mx_StyledRadioButton test-class mx_StyledRadioButton_enabled"
|
||||
>
|
||||
<input
|
||||
id="test-Badger"
|
||||
name="test"
|
||||
type="radio"
|
||||
value="Badger"
|
||||
/>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_content"
|
||||
>
|
||||
<span>
|
||||
Badger label
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_spacer"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
class="mx_StyledRadioButton test-class mx_StyledRadioButton_enabled"
|
||||
>
|
||||
<input
|
||||
aria-describedby="test-Canary-description"
|
||||
id="test-Canary"
|
||||
name="test"
|
||||
type="radio"
|
||||
value="Canary"
|
||||
/>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_content"
|
||||
>
|
||||
<span>
|
||||
Canary label
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_spacer"
|
||||
/>
|
||||
</label>
|
||||
<span
|
||||
id="test-Canary-description"
|
||||
>
|
||||
<span>
|
||||
Canary description
|
||||
</span>
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,30 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<SyntaxHighlight /> renders 1`] = `
|
||||
<div>
|
||||
<pre
|
||||
class="mx_SyntaxHighlight hljs language-arcade"
|
||||
>
|
||||
<code>
|
||||
<span
|
||||
class="hljs-built_in"
|
||||
>
|
||||
console
|
||||
</span>
|
||||
.
|
||||
<span
|
||||
class="hljs-built_in"
|
||||
>
|
||||
log
|
||||
</span>
|
||||
(
|
||||
<span
|
||||
class="hljs-string"
|
||||
>
|
||||
"Hello, World!"
|
||||
</span>
|
||||
);
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { cleanup, render, waitFor } from "jest-matrix-react";
|
||||
import React from "react";
|
||||
|
||||
import VerificationQRCode from "../../../../../../src/components/views/elements/crypto/VerificationQRCode";
|
||||
|
||||
describe("<VerificationQRCode />", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("renders a QR code", async () => {
|
||||
const { container, getAllByAltText } = render(<VerificationQRCode qrCodeBytes={Buffer.from("asd")} />);
|
||||
// wait for the spinner to go away
|
||||
await waitFor(() => getAllByAltText("QR Code").length === 1);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<VerificationQRCode /> renders a QR code 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="mx_QRCode mx_VerificationQRCode"
|
||||
>
|
||||
<img
|
||||
alt="QR Code"
|
||||
class="mx_VerificationQRCode"
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMQAAADECAYAAADApo5rAAAAAklEQVR4AewaftIAAAPVSURBVO3BQQ4bRxAEwawG///lsiADfdqBQWpFkVZGpD8g6adB0hokrUHSGiStQdIaJK1B0hokrUHSGiStQdIaJK1B0hokrUHSGiStQdIaJK1B0hokrUHSGiStQdIaJK1B0hokrUHSevBmSfhWbXlWEk7acpKEZ7XlJAnfqi3vMkhag6Q1SFqDpDVIWg8+RFs+QRLeJQmvaMud2vIJkvCnDZLWIGkNktYgaQ2S1iBpPfgCSbhTW+6WhGe15SQJJ0m40pa7JeFObflkg6Q1SFqDpDVIWoOkNUhaD3SLtlxJwt3aot9jkLQGSWuQtAZJa5C0BknrgX6rtpwk4aQtJ0m40hb9mkHSGiStQdIaJK1B0nrwBdryf9SWT9eWv8kgaQ2S1iBpDZLWIGkNktaDD5GEb5aEK205ScJJW94lCfrXIGkNktYgaQ2S1iBpDZLWgzdry98kCSdtOUnCndqi/zZIWoOkNUhag6Q1SFqDpPXgzZJwp7acJOGTJeEVbbmShFe05U5JOGnLJxskrUHSGiStQdIaJK0Hb9aWK0k4actJEk7aciUJJ235Vm05ScKdknDSllck4Upb3mWQtAZJa5C0BklrkLQGSevBmyXhSlte0ZY7JeEVbbmShJO2vCIJV9py0pZXJOFOSfhkg6Q1SFqDpDVIWoOkNUhaD/7HknClLZ8gCSdtOWnLs5Jw0paTttypLZ9skLQGSWuQtAZJa5C0BknrwRdIwrsk4aQtd2rLK5JwpS13S8KVttwtCVfa8i6DpDVIWoOkNUhag6SV/oB+WRLepS1XknDSlpMknLTlTkl4VlveZZC0BklrkLQGSWuQtAZJ68GbJeFbteVZbTlJwrsk4V2S8K0GSWuQtAZJa5C0BklrkLQefIi2fIIkvEtbTpJwkoRnteUkCSdJeFZbXpGEP22QtAZJa5C0BklrkLQGSevBF0jCndryzdpyJQknSXhFW56VhJO2fLJB0hokrUHSGiStQdJ6oFu05UoSTtpy0pZnteUkCSdteZcknLTlTxskrUHSGiStQdIaJK1B0nqgWyThWUk4actJEq605aQtr0jClbactOUVSbjSlncZJK1B0hokrUHSGiStQdJ68AXa8q3acpKEV7TlWUk4actJW56VhFe05U8bJK1B0hokrUHSGiStQdJ68CGS8LdpyyuScKUtd0vClbbcLQlX2vIug6Q1SFqDpDVIWoOklf6ApJ8GSWuQtAZJa5C0BklrkLQGSWuQtAZJa5C0BklrkLQGSWuQtAZJa5C0BklrkLQGSWuQtAZJa5C0BklrkLQGSWuQtP4BohkVlic75PUAAAAASUVORK5CYII="
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
Loading…
Add table
Add a link
Reference in a new issue