mv element.io @types __mocks__/ debian docker module_system/ playwright res src test webapp Dockerfile .dockerignore .eslintignore .stylelintrc.cjs babel.config.cjs recorder-worklet-loader.cjs .modernizr.json components.json config.json config.sample.json package.json project.json tsconfig.json tsconfig.module_system.json jest.config.ts playwright.config.ts webpack.config.ts build_config.sample.yaml apps/web/

mkdir apps/web/scripts
mv scripts/{cleanup.sh,ci_package.sh,copy-res.ts,deploy.py,package.sh} apps/web/scripts

And a couple of gitignore tweaks

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2026-02-24 15:43:58 +00:00
parent e7509c92a1
commit 91a3cb03c1
3408 changed files with 28 additions and 32 deletions
@@ -0,0 +1,90 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { render } from "jest-matrix-react";
import { mocked } from "jest-mock";
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import React from "react";
import AddExistingToSpaceDialog from "../../../../../src/components/views/dialogs/AddExistingToSpaceDialog";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { mkRoom, mkSpace, stubClient } from "../../../../test-utils";
describe("<AddExistingToSpaceDialog />", () => {
beforeEach(() => {
jest.spyOn(Element.prototype, "clientHeight", "get").mockReturnValue(600);
});
it("looks as expected", () => {
const client = stubClient();
const dialog = renderAddExistingToSpaceDialog(client);
expect(dialog.asFragment()).toMatchSnapshot();
});
it("should show 'no results' if appropriate", () => {
const client = stubClient();
const { getByText } = renderAddExistingToSpaceDialog(client);
expect(getByText("No results")).toBeInTheDocument();
});
it("should not show 'no results' if we have results to show", () => {
const client = stubClient();
mocked(client.getVisibleRooms).mockReturnValue([
mkSpace(client, "!space2:example.com"),
mkRoom(client, "!room2:example.com"),
]);
const { queryByText, getByText } = renderAddExistingToSpaceDialog(client);
expect(queryByText("No results")).not.toBeInTheDocument();
expect(getByText("!room2:example.com")).toBeInTheDocument();
});
describe("If the feature_dynamic_room_predecessors is not enabled", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
});
it("Passes through the dynamic predecessor setting", async () => {
const client = stubClient();
mocked(client.getVisibleRooms).mockClear();
renderAddExistingToSpaceDialog(client);
expect(client.getVisibleRooms).toHaveBeenCalledWith(false);
});
});
describe("If the feature_dynamic_room_predecessors is enabled", () => {
beforeEach(() => {
// Turn on feature_dynamic_room_predecessors setting
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => settingName === "feature_dynamic_room_predecessors",
);
});
it("Passes through the dynamic predecessor setting", async () => {
const client = stubClient();
mocked(client.getVisibleRooms).mockClear();
renderAddExistingToSpaceDialog(client);
expect(client.getVisibleRooms).toHaveBeenCalledWith(true);
});
});
});
function renderAddExistingToSpaceDialog(client: MatrixClient) {
const dmRoomMap = new DMRoomMap(client);
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
const space = mkSpace(client, "!spaceid:example.com");
const dialog = render(
<AddExistingToSpaceDialog
space={space}
onCreateRoomClick={jest.fn()}
onAddSubspaceClick={jest.fn()}
onFinished={jest.fn()}
/>,
);
return dialog;
}
@@ -0,0 +1,93 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
import QuickSettingsButton from "../../../../../src/components/views/spaces/QuickSettingsButton";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
describe("QuickSettingsButton", () => {
const roomId = "!room:example.com";
const renderQuickSettingsButton = () => {
render(<QuickSettingsButton isPanelCollapsed={true} />);
};
const getQuickSettingsButton = () => {
return screen.getByRole("button", { name: "Quick settings" });
};
const openQuickSettings = async () => {
await userEvent.click(getQuickSettingsButton());
await screen.findByRole("heading", { name: "Quick settings" });
};
it("should render the quick settings button", () => {
renderQuickSettingsButton();
expect(getQuickSettingsButton()).toBeInTheDocument();
});
it("should render the quick settings button in expanded mode", () => {
const { asFragment } = render(<QuickSettingsButton isPanelCollapsed={false} />);
expect(asFragment()).toMatchSnapshot();
});
describe("when the quick settings are open", () => {
beforeEach(async () => {
renderQuickSettingsButton();
await openQuickSettings();
});
it("should not render the »Developer tools« button", () => {
renderQuickSettingsButton();
expect(screen.queryByText("Developer tools")).not.toBeInTheDocument();
});
});
describe("when developer mode is enabled", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "developerMode");
renderQuickSettingsButton();
});
afterEach(() => {
mocked(SettingsStore.getValue).mockRestore();
});
describe("and no room is viewed", () => {
it("should not render the »Developer tools« button", () => {
renderQuickSettingsButton();
expect(screen.queryByText("Developer tools")).not.toBeInTheDocument();
});
});
describe("and a room is viewed", () => {
beforeEach(() => {
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(roomId);
});
afterEach(() => {
mocked(SdkContextClass.instance.roomViewStore.getRoomId).mockRestore();
});
describe("and the quick settings are open", () => {
beforeEach(async () => {
await openQuickSettings();
});
it("should render the »Developer tools« button", () => {
expect(screen.getByRole("button", { name: "Developer tools" })).toBeInTheDocument();
});
});
});
});
});
@@ -0,0 +1,126 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
import QuickThemeSwitcher from "../../../../../src/components/views/spaces/QuickThemeSwitcher";
import { getOrderedThemes } from "../../../../../src/theme";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
import dis from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
import { mockPlatformPeg } from "../../../../test-utils/platform";
import { useTheme } from "../../../../../src/hooks/useTheme";
jest.mock("../../../../../src/hooks/useTheme", () => ({
useTheme: jest.fn(),
}));
jest.mock("../../../../../src/theme");
jest.mock("../../../../../src/settings/SettingsStore", () => ({
setValue: jest.fn(),
getValue: jest.fn(),
monitorSetting: jest.fn(),
watchSetting: jest.fn(),
}));
jest.mock("../../../../../src/dispatcher/dispatcher", () => ({
dispatch: jest.fn(),
register: jest.fn(),
}));
mockPlatformPeg({ overrideBrowserShortcuts: jest.fn().mockReturnValue(false) });
describe("<QuickThemeSwitcher />", () => {
const defaultProps = {
requestClose: jest.fn(),
};
const renderComponent = (props = {}) => render(<QuickThemeSwitcher {...defaultProps} {...props} />);
beforeEach(() => {
mocked(getOrderedThemes)
.mockClear()
.mockReturnValue([
{ id: "light", name: "Light" },
{ id: "dark", name: "Dark" },
]);
mocked(useTheme).mockClear().mockReturnValue({
theme: "light",
systemThemeActivated: false,
});
mocked(SettingsStore).setValue.mockClear().mockResolvedValue();
mocked(dis).dispatch.mockClear();
});
const selectFromDropdown = async (getByTextArg: RegExp | string) => {
const dropdown = screen.getByRole("button", { name: "Theme" });
await userEvent.click(dropdown);
await waitFor(() => {
expect(dropdown).toHaveAttribute("aria-expanded", "true");
});
await userEvent.click(screen.getByText(getByTextArg));
return waitFor(() => {
expect(dropdown).toHaveAttribute("aria-expanded", "false");
});
};
it("renders dropdown correctly when light theme is selected", () => {
renderComponent();
expect(screen.getByText("Light")).toBeInTheDocument();
});
it("renders dropdown correctly when use system theme is truthy", () => {
mocked(useTheme).mockClear().mockReturnValue({
theme: "light",
systemThemeActivated: true,
});
renderComponent();
expect(screen.getByText("Match system")).toBeInTheDocument();
});
it("updates settings when match system is selected", async () => {
const requestClose = jest.fn();
renderComponent({ requestClose });
await selectFromDropdown(/match system/i);
expect(SettingsStore.setValue).toHaveBeenCalledTimes(1);
expect(SettingsStore.setValue).toHaveBeenCalledWith("use_system_theme", null, SettingLevel.DEVICE, true);
expect(dis.dispatch).not.toHaveBeenCalled();
expect(requestClose).toHaveBeenCalled();
});
it("updates settings when a theme is selected", async () => {
// ie not match system
const requestClose = jest.fn();
renderComponent({ requestClose });
await selectFromDropdown(/dark/i);
expect(SettingsStore.setValue).toHaveBeenCalledWith("use_system_theme", null, SettingLevel.DEVICE, false);
expect(SettingsStore.setValue).toHaveBeenCalledWith("theme", null, SettingLevel.DEVICE, "dark");
expect(dis.dispatch).toHaveBeenCalledWith({ action: Action.RecheckTheme, forceTheme: "dark" });
expect(requestClose).toHaveBeenCalled();
});
it("rechecks theme when setting theme fails", async () => {
mocked(SettingsStore.setValue).mockRejectedValue("oops");
const requestClose = jest.fn();
renderComponent({ requestClose });
await selectFromDropdown(/match system/i);
expect(dis.dispatch).toHaveBeenCalledWith({ action: Action.RecheckTheme });
expect(requestClose).toHaveBeenCalled();
});
});
@@ -0,0 +1,121 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, cleanup } from "jest-matrix-react";
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import userEvent from "@testing-library/user-event";
import { MatrixError } from "matrix-js-sdk/src/matrix";
import { type MockedObject } from "jest-mock";
import SpaceCreateMenu from "../../../../../src/components/views/spaces/SpaceCreateMenu";
import {
getMockClientWithEventEmitter,
mockClientMethodsRooms,
mockClientMethodsServer,
mockClientMethodsUser,
withClientContextRenderOptions,
} from "../../../../test-utils";
import { UIFeature } from "../../../../../src/settings/UIFeature";
import SettingsStore from "../../../../../src/settings/SettingsStore";
describe("<SpaceCreateMenu />", () => {
let client: MockedObject<MatrixClient>;
beforeEach(() => {
client = getMockClientWithEventEmitter({
...mockClientMethodsUser(),
...mockClientMethodsServer(),
...mockClientMethodsRooms(),
createRoom: jest.fn(),
getRoomIdForAlias: jest.fn().mockImplementation(async () => {
throw new MatrixError({ errcode: "M_NOT_FOUND", error: "Test says no alias found" }, 404);
}),
});
});
afterEach(() => {
cleanup();
jest.restoreAllMocks();
});
it("should render", async () => {
const { asFragment } = render(
<SpaceCreateMenu onFinished={jest.fn()} />,
withClientContextRenderOptions(client),
);
expect(asFragment()).toMatchSnapshot();
});
it("should be able to create a public space", async () => {
const onFinished = jest.fn();
client.createRoom.mockResolvedValue({ room_id: "!room:id" });
const { getByText, getByLabelText } = render(
<SpaceCreateMenu onFinished={onFinished} />,
withClientContextRenderOptions(client),
);
await userEvent.click(getByText("Public"));
await userEvent.type(getByLabelText("Name"), "My Name");
await userEvent.type(getByLabelText("Address"), "foobar");
await userEvent.type(getByLabelText("Description"), "A description");
await userEvent.click(getByText("Create"));
expect(onFinished).toHaveBeenCalledTimes(1);
expect(client.createRoom).toHaveBeenCalledWith({
creation_content: { type: "m.space" },
initial_state: [
{ content: { guest_access: "can_join" }, state_key: "", type: "m.room.guest_access" },
{ content: { history_visibility: "world_readable" }, type: "m.room.history_visibility" },
],
name: "My Name",
power_level_content_override: {
events_default: 100,
invite: 0,
},
preset: "public_chat",
room_alias_name: "my-namefoobar",
topic: "A description",
visibility: "private",
});
});
it("should be prompted to automatically create a private space when configured", async () => {
const realGetValue = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
if (name === UIFeature.AllowCreatingPublicSpaces) {
return false;
}
return realGetValue(name, roomId);
});
const onFinished = jest.fn();
client.createRoom.mockResolvedValue({ room_id: "!room:id" });
const { getByText, getByLabelText } = render(
<SpaceCreateMenu onFinished={onFinished} />,
withClientContextRenderOptions(client),
);
await userEvent.type(getByLabelText("Name"), "My Name");
await userEvent.type(getByLabelText("Description"), "A description");
await userEvent.click(getByText("Create"));
expect(onFinished).toHaveBeenCalledTimes(1);
expect(client.createRoom).toHaveBeenCalledWith({
creation_content: { type: "m.space" },
initial_state: [
{ content: { guest_access: "can_join" }, state_key: "", type: "m.room.guest_access" },
{ content: { history_visibility: "invited" }, type: "m.room.history_visibility" },
],
name: "My Name",
power_level_content_override: {
events_default: 100,
invite: 50,
},
room_alias_name: undefined,
preset: "private_chat",
topic: "A description",
visibility: "private",
});
});
});
@@ -0,0 +1,192 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen, fireEvent, act, cleanup } from "jest-matrix-react";
import { mocked } from "jest-mock";
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { MetaSpace, type SpaceKey } from "../../../../../src/stores/spaces";
import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents";
import { UIComponent } from "../../../../../src/settings/UIFeature";
import { mkStubRoom, wrapInMatrixClientContext, wrapInSdkContext } from "../../../../test-utils";
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
import SpaceStore from "../../../../../src/stores/spaces/SpaceStore";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { type SpaceNotificationState } from "../../../../../src/stores/notifications/SpaceNotificationState";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import UnwrappedSpacePanel from "../../../../../src/components/views/spaces/SpacePanel";
// DND test utilities based on
// https://github.com/colinrobertbrooks/react-beautiful-dnd-test-utils/issues/18#issuecomment-1373388693
enum Keys {
SPACE = 32,
ARROW_LEFT = 37,
ARROW_UP = 38,
ARROW_RIGHT = 39,
ARROW_DOWN = 40,
}
enum DragDirection {
LEFT = Keys.ARROW_LEFT,
UP = Keys.ARROW_UP,
RIGHT = Keys.ARROW_RIGHT,
DOWN = Keys.ARROW_DOWN,
}
// taken from https://github.com/hello-pangea/dnd/blob/main/test/unit/integration/util/controls.ts#L20
const createTransitionEndEvent = (): Event => {
const event = new Event("transitionend", {
bubbles: true,
cancelable: true,
}) as TransitionEvent;
// cheating and adding property to event as
// TransitionEvent constructor does not exist.
// This is needed because of the following check
// https://github.com/atlassian/react-beautiful-dnd/blob/master/src/view/draggable/draggable.jsx#L130
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(event as any).propertyName = "transform";
return event;
};
const pickUp = async (element: HTMLElement) => {
fireEvent.keyDown(element, {
keyCode: Keys.SPACE,
});
await screen.findByText(/You have lifted an item/i);
act(() => {
jest.runOnlyPendingTimers();
});
};
const move = async (element: HTMLElement, direction: DragDirection) => {
fireEvent.keyDown(element, {
keyCode: direction,
});
await screen.findByText(/(You have moved the item | has been combined with)/i);
};
const drop = async (element: HTMLElement) => {
fireEvent.keyDown(element, {
keyCode: Keys.SPACE,
});
fireEvent(element.parentElement!, createTransitionEndEvent());
await screen.findByText(/You have dropped the item/i);
};
jest.mock("../../../../../src/stores/spaces/SpaceStore", () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const EventEmitter = require("events");
class MockSpaceStore extends EventEmitter {
invitedSpaces: SpaceKey[] = [];
enabledMetaSpaces: MetaSpace[] = [];
spacePanelSpaces: string[] = [];
activeSpace: SpaceKey = "!space1";
getChildSpaces = () => [] as Room[];
getNotificationState = () => null as SpaceNotificationState | null;
setActiveSpace = jest.fn();
moveRootSpace = jest.fn();
}
return {
instance: new MockSpaceStore(),
};
});
jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
shouldShowComponent: jest.fn(),
}));
describe("<SpacePanel />", () => {
const mockClient = {
getUserId: jest.fn().mockReturnValue("@test:test"),
getSafeUserId: jest.fn().mockReturnValue("@test:test"),
mxcUrlToHttp: jest.fn(),
getRoom: jest.fn(),
isGuest: jest.fn(),
getAccountData: jest.fn(),
on: jest.fn(),
off: jest.fn(),
removeListener: jest.fn(),
isVersionSupported: jest.fn().mockResolvedValue(true),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
} as unknown as MatrixClient;
const SpacePanel = wrapInSdkContext(wrapInMatrixClientContext(UnwrappedSpacePanel), SdkContextClass.instance);
beforeAll(() => {
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
});
beforeEach(() => {
SpaceStore.instance.enabledMetaSpaces.push(
MetaSpace.Home,
MetaSpace.Favourites,
MetaSpace.People,
MetaSpace.Orphans,
MetaSpace.VideoRooms,
);
mocked(shouldShowComponent).mockClear().mockReturnValue(true);
});
afterEach(() => {
cleanup();
});
it("should show all activated MetaSpaces in the correct order", async () => {
const originalGetValue = SettingsStore.getValue;
const spySettingsStore = jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => {
return setting === "feature_video_rooms" ? true : originalGetValue(setting);
});
const renderResult = render(<SpacePanel />);
expect(renderResult.asFragment()).toMatchSnapshot();
spySettingsStore.mockRestore();
});
describe("create new space button", () => {
it("renders create space button when UIComponent.CreateSpaces component should be shown", () => {
render(<SpacePanel />);
screen.getByTestId("create-space-button");
});
it("does not render create space button when UIComponent.CreateSpaces component should not be shown", () => {
mocked(shouldShowComponent).mockReturnValue(false);
render(<SpacePanel />);
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.CreateSpaces);
expect(screen.queryByTestId("create-space-button")).toBeFalsy();
});
it("opens context menu on create space button click", () => {
render(<SpacePanel />);
fireEvent.click(screen.getByTestId("create-space-button"));
screen.getByTestId("create-space-button");
});
});
it("should allow rearranging via drag and drop", async () => {
(SpaceStore.instance.spacePanelSpaces as any) = [
mkStubRoom("!room1:server", "Room 1", mockClient),
mkStubRoom("!room2:server", "Room 2", mockClient),
mkStubRoom("!room3:server", "Room 3", mockClient),
];
DMRoomMap.makeShared(mockClient);
jest.useFakeTimers();
const { getByLabelText } = render(<SpacePanel />);
const room1 = getByLabelText("Room 1");
await pickUp(room1);
await move(room1, DragDirection.DOWN);
await drop(room1);
expect(SpaceStore.instance.moveRootSpace).toHaveBeenCalledWith(0, 1);
});
});
@@ -0,0 +1,254 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { mocked } from "jest-mock";
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
import { act, fireEvent, render, type RenderResult } from "jest-matrix-react";
import {
EventType,
type MatrixClient,
type Room,
GuestAccess,
HistoryVisibility,
JoinRule,
Visibility,
} from "matrix-js-sdk/src/matrix";
import _SpaceSettingsVisibilityTab from "../../../../../src/components/views/spaces/SpaceSettingsVisibilityTab";
import {
createTestClient,
mkEvent,
wrapInMatrixClientContext,
mkSpace,
mockStateEventImplementation,
} from "../../../../test-utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
const SpaceSettingsVisibilityTab = wrapInMatrixClientContext(_SpaceSettingsVisibilityTab);
jest.useFakeTimers();
describe("<SpaceSettingsVisibilityTab />", () => {
const mockMatrixClient = createTestClient() as MatrixClient;
mocked(mockMatrixClient.isVersionSupported).mockImplementation(async (v) => v === "v1.4");
const makeJoinEvent = (rule: JoinRule = JoinRule.Invite) =>
mkEvent({
type: EventType.RoomJoinRules,
event: true,
content: {
join_rule: rule,
},
} as any);
const makeGuestAccessEvent = (rule: GuestAccess = GuestAccess.CanJoin) =>
mkEvent({
type: EventType.RoomGuestAccess,
event: true,
content: {
guest_access: rule,
},
} as any);
const makeHistoryEvent = (rule: HistoryVisibility = HistoryVisibility.Shared) =>
mkEvent({
type: EventType.RoomHistoryVisibility,
event: true,
content: {
history_visibility: rule,
},
} as any);
const mockSpaceId = "mock-space";
// TODO case for canonical
const makeMockSpace = (
client: MatrixClient,
joinRule: JoinRule = JoinRule.Invite,
guestRule: GuestAccess = GuestAccess.CanJoin,
historyRule: HistoryVisibility = HistoryVisibility.WorldReadable,
): Room => {
const events = [makeJoinEvent(joinRule), makeGuestAccessEvent(guestRule), makeHistoryEvent(historyRule)];
const space = mkSpace(client, mockSpaceId);
mocked(client.getRoom).mockImplementation((roomId) => (roomId === mockSpaceId ? space : null));
const getStateEvents = mockStateEventImplementation(events);
mocked(space.currentState).getStateEvents.mockImplementation(getStateEvents);
mocked(space.currentState).mayClientSendStateEvent.mockReturnValue(false);
space.getJoinRule.mockReturnValue(joinRule);
mocked(space.currentState).getJoinRule.mockReturnValue(joinRule);
return space as unknown as Room;
};
const defaultProps = {
matrixClient: mockMatrixClient,
space: makeMockSpace(mockMatrixClient),
closeSettingsFn: jest.fn(),
};
const getComponent = (props = {}) => {
return render(<SpaceSettingsVisibilityTab {...defaultProps} {...props} />);
};
const toggleGuestAccessSection = async ({ getByTestId }: RenderResult) => {
const toggleButton = getByTestId("toggle-guest-access-btn")!;
fireEvent.click(toggleButton);
};
const getGuestAccessToggle = ({ getByLabelText }: RenderResult) => getByLabelText("Enable guest access");
const getHistoryVisibilityToggle = ({ getByLabelText }: RenderResult) => getByLabelText("Preview Space");
const getErrorMessage = ({ getByTestId }: RenderResult) => getByTestId("space-settings-error")?.textContent;
beforeEach(() => {
let i = 0;
mocked(secureRandomString).mockImplementation(() => {
return "testid_" + i++;
});
(mockMatrixClient.sendStateEvent as jest.Mock).mockClear().mockResolvedValue({});
MatrixClientPeg.get = jest.fn().mockReturnValue(mockMatrixClient);
MatrixClientPeg.safeGet = jest.fn().mockReturnValue(mockMatrixClient);
});
afterEach(() => {
jest.runAllTimers();
});
it("renders container", () => {
const { asFragment } = getComponent();
expect(asFragment()).toMatchSnapshot();
});
describe("for a private space", () => {
const joinRule = JoinRule.Invite;
it("does not render addresses section", () => {
const space = makeMockSpace(mockMatrixClient, joinRule);
const { queryByTestId } = getComponent({ space });
expect(queryByTestId("published-address-fieldset")).toBeFalsy();
expect(queryByTestId("local-address-fieldset")).toBeFalsy();
});
});
describe("for a public space", () => {
const joinRule = JoinRule.Public;
const guestRule = GuestAccess.CanJoin;
const historyRule = HistoryVisibility.Joined;
mocked(mockMatrixClient.getRoomDirectoryVisibility).mockResolvedValue({ visibility: Visibility.Public });
describe("Access", () => {
it("renders guest access section toggle", async () => {
const space = makeMockSpace(mockMatrixClient, joinRule, guestRule);
const component = getComponent({ space });
await toggleGuestAccessSection(component);
expect(getGuestAccessToggle(component)).toMatchSnapshot();
});
it("send guest access event on toggle", async () => {
const space = makeMockSpace(mockMatrixClient, joinRule, guestRule);
const component = getComponent({ space });
await toggleGuestAccessSection(component);
const guestAccessInput = getGuestAccessToggle(component);
expect(guestAccessInput).toBeChecked();
fireEvent.click(guestAccessInput!);
expect(mockMatrixClient.sendStateEvent).toHaveBeenCalledWith(
mockSpaceId,
EventType.RoomGuestAccess,
// toggled off
{ guest_access: GuestAccess.Forbidden },
"",
);
// toggled off
expect(guestAccessInput).not.toBeChecked();
});
it("renders error message when update fails", async () => {
const space = makeMockSpace(mockMatrixClient, joinRule, guestRule);
(mockMatrixClient.sendStateEvent as jest.Mock).mockRejectedValue({});
const component = getComponent({ space });
await toggleGuestAccessSection(component);
await act(() => {
fireEvent.click(getGuestAccessToggle(component)!);
});
expect(getErrorMessage(component)).toEqual("Failed to update the guest access of this space");
});
it("disables guest access toggle when setting guest access is not allowed", async () => {
const space = makeMockSpace(mockMatrixClient, joinRule, guestRule);
(space.currentState.maySendStateEvent as jest.Mock).mockReturnValue(false);
const component = getComponent({ space });
await toggleGuestAccessSection(component);
expect(getGuestAccessToggle(component)).toBeDisabled();
});
});
describe("Preview", () => {
it("renders preview space toggle", () => {
const space = makeMockSpace(mockMatrixClient, joinRule, guestRule, historyRule);
const component = getComponent({ space });
// toggle off because space settings is != WorldReadable
expect(getHistoryVisibilityToggle(component)).not.toBeChecked();
});
it("updates history visibility on toggle", () => {
const space = makeMockSpace(mockMatrixClient, joinRule, guestRule, historyRule);
const component = getComponent({ space });
// toggle off because space settings is != WorldReadable
expect(getHistoryVisibilityToggle(component)).not.toBeChecked();
fireEvent.click(getHistoryVisibilityToggle(component)!);
expect(mockMatrixClient.sendStateEvent).toHaveBeenCalledWith(
mockSpaceId,
EventType.RoomHistoryVisibility,
{ history_visibility: HistoryVisibility.WorldReadable },
"",
);
expect(getHistoryVisibilityToggle(component)).toBeChecked();
});
it("renders error message when history update fails", async () => {
const space = makeMockSpace(mockMatrixClient, joinRule, guestRule, historyRule);
(mockMatrixClient.sendStateEvent as jest.Mock).mockRejectedValue({});
const component = getComponent({ space });
await act(() => {
fireEvent.click(getHistoryVisibilityToggle(component)!);
});
expect(getErrorMessage(component)).toEqual("Failed to update the history visibility of this space");
});
it("disables room preview toggle when history visibility changes are not allowed", () => {
const space = makeMockSpace(mockMatrixClient, joinRule, guestRule, historyRule);
(space.currentState.maySendStateEvent as jest.Mock).mockReturnValue(false);
const component = getComponent({ space });
expect(getHistoryVisibilityToggle(component)).toBeDisabled();
});
});
it("renders addresses section with publish toggle", async () => {
const space = makeMockSpace(mockMatrixClient, joinRule, guestRule);
const { findByLabelText, getByTestId, asFragment } = getComponent({ space });
expect(getByTestId("published-address-fieldset")).toBeTruthy();
expect(getByTestId("local-address-fieldset")).toBeTruthy();
await expect(
findByLabelText("Publish this room to the public in matrix.org's room directory?"),
).resolves.toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
});
});
@@ -0,0 +1,157 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fireEvent, getByTestId, render } from "jest-matrix-react";
import { mocked } from "jest-mock";
import { mkRoom, stubClient } from "../../../../test-utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
import { SpaceItem, SpaceButton } from "../../../../../src/components/views/spaces/SpaceTreeLevel";
import { MetaSpace, type SpaceKey } from "../../../../../src/stores/spaces";
import SpaceStore from "../../../../../src/stores/spaces/SpaceStore";
import { StaticNotificationState } from "../../../../../src/stores/notifications/StaticNotificationState";
import { NotificationLevel } from "../../../../../src/stores/notifications/NotificationLevel";
jest.mock("../../../../../src/stores/spaces/SpaceStore", () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const EventEmitter = require("events");
class MockSpaceStore extends EventEmitter {
activeSpace: SpaceKey = "!space1";
setActiveSpace = jest.fn();
getChildSpaces = jest.fn();
getNotificationState = jest.fn();
}
return { instance: new MockSpaceStore() };
});
describe("SpaceButton", () => {
stubClient();
const space = mkRoom(MatrixClientPeg.safeGet(), "!1:example.org");
DMRoomMap.makeShared(MatrixClientPeg.safeGet());
const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch");
afterEach(jest.clearAllMocks);
describe("real space", () => {
it("activates the space on click", () => {
const { container } = render(
<SpaceButton
space={space}
selected={false}
label="My space"
data-testid="create-space-button"
size="32px"
/>,
);
expect(SpaceStore.instance.setActiveSpace).not.toHaveBeenCalled();
fireEvent.click(getByTestId(container, "create-space-button"));
expect(SpaceStore.instance.setActiveSpace).toHaveBeenCalledWith("!1:example.org");
});
it("navigates to the space home on click if already active", () => {
const { container } = render(
<SpaceButton
space={space}
selected={true}
label="My space"
data-testid="create-space-button"
size="32px"
/>,
);
expect(dispatchSpy).not.toHaveBeenCalled();
fireEvent.click(getByTestId(container, "create-space-button"));
expect(dispatchSpy).toHaveBeenCalledWith({ action: Action.ViewRoom, room_id: "!1:example.org" });
});
});
describe("metaspace", () => {
it("activates the metaspace on click", () => {
const { container } = render(
<SpaceButton
spaceKey={MetaSpace.People}
selected={false}
label="People"
data-testid="create-space-button"
size="32px"
/>,
);
expect(SpaceStore.instance.setActiveSpace).not.toHaveBeenCalled();
fireEvent.click(getByTestId(container, "create-space-button"));
expect(SpaceStore.instance.setActiveSpace).toHaveBeenCalledWith(MetaSpace.People);
});
it("does nothing on click if already active", () => {
const { container } = render(
<SpaceButton
spaceKey={MetaSpace.People}
selected={true}
label="People"
data-testid="create-space-button"
size="32px"
/>,
);
fireEvent.click(getByTestId(container, "create-space-button"));
expect(dispatchSpy).not.toHaveBeenCalled();
// Re-activating the metaspace is a no-op
expect(SpaceStore.instance.setActiveSpace).toHaveBeenCalledWith(MetaSpace.People);
});
it("should render notificationState if one is provided", () => {
const notificationState = new StaticNotificationState(null, 8, NotificationLevel.Notification);
const { container, asFragment } = render(
<SpaceButton
spaceKey={MetaSpace.People}
selected={true}
label="People"
data-testid="create-space-button"
notificationState={notificationState}
size="32px"
/>,
);
expect(container.querySelector(".mx_NotificationBadge_count")).toHaveTextContent("8");
expect(asFragment()).toMatchSnapshot();
});
});
});
describe("SpaceItem", () => {
const cli = stubClient();
const space = mkRoom(cli, "!1:example.org");
space.name = "Root Space";
const subspace = mkRoom(cli, "!2:example.org");
subspace.name = "Subspace";
it("should render a space with subspaces", () => {
mocked(SpaceStore.instance.getChildSpaces).mockImplementation((spaceId) =>
spaceId === space.roomId ? [subspace] : [],
);
const { asFragment, queryByText, getByLabelText } = render(<SpaceItem space={space} activeSpaces={[]} />);
expect(queryByText("Root Space")).toBeVisible();
expect(queryByText("Subspace")).toBeNull();
expect(asFragment()).toMatchSnapshot();
fireEvent.click(getByLabelText("Expand"));
expect(queryByText("Root Space")).toBeVisible();
expect(queryByText("Subspace")).toBeVisible();
expect(asFragment()).toMatchSnapshot();
});
});
@@ -0,0 +1,239 @@
/*
* Copyright 2024 New Vector Ltd.
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React, { type ComponentProps } from "react";
import { getByText, render, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { NotificationCountType, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
import { ThreadsActivityCentre } from "../../../../../src/components/views/spaces/threads-activity-centre";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { stubClient } from "../../../../test-utils";
import { populateThread } from "../../../../test-utils/threads";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
describe("ThreadsActivityCentre", () => {
const getTACButton = () => {
return screen.getByRole("button", { name: "Threads" });
};
const getTACMenu = () => {
return screen.getByRole("menu");
};
const getTACDescription = (container: ReturnType<typeof render>["container"]) => {
return container.querySelector(".mx_ThreadsActivityCentreButton_Text");
};
const renderTAC = (props?: ComponentProps<typeof ThreadsActivityCentre>) => {
return render(
<MatrixClientContext.Provider value={cli}>
<ThreadsActivityCentre {...props} />
</MatrixClientContext.Provider>,
);
};
const cli = stubClient();
cli.supportsThreads = () => true;
const roomWithActivity = new Room("!room:server", cli, cli.getSafeUserId(), {
pendingEventOrdering: PendingEventOrdering.Detached,
});
roomWithActivity.name = "Just activity";
const roomWithNotif = new Room("!room2:server", cli, cli.getSafeUserId(), {
pendingEventOrdering: PendingEventOrdering.Detached,
});
roomWithNotif.name = "A notification";
const roomWithHighlight = new Room("!room3:server", cli, cli.getSafeUserId(), {
pendingEventOrdering: PendingEventOrdering.Detached,
});
roomWithHighlight.name = "This is a real highlight";
const getDefaultThreadArgs = (room: Room) => ({
room: room,
client: cli,
authorId: "@foo:bar",
participantUserIds: ["@fee:bar"],
});
beforeAll(async () => {
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(cli);
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(cli);
const dmRoomMap = new DMRoomMap(cli);
jest.spyOn(dmRoomMap, "getUserIdForRoomId");
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
await populateThread(getDefaultThreadArgs(roomWithActivity));
const notifThreadInfo = await populateThread(getDefaultThreadArgs(roomWithNotif));
roomWithNotif.setThreadUnreadNotificationCount(notifThreadInfo.thread.id, NotificationCountType.Total, 1);
const highlightThreadInfo = await populateThread({
...getDefaultThreadArgs(roomWithHighlight),
// timestamp
ts: 5,
});
roomWithHighlight.setThreadUnreadNotificationCount(
highlightThreadInfo.thread.id,
NotificationCountType.Highlight,
1,
);
});
it("should render the threads activity centre button", async () => {
renderTAC();
expect(getTACButton()).toBeInTheDocument();
});
it("should render the threads activity centre button and the display label", async () => {
const { container } = renderTAC({ displayButtonLabel: true });
expect(getTACButton()).toBeInTheDocument();
expect(getTACDescription(container)).toBeInTheDocument();
});
it("should render the threads activity centre menu when the button is clicked", async () => {
renderTAC();
await userEvent.click(getTACButton());
expect(getTACMenu()).toBeInTheDocument();
});
it("should not render a room with a activity in the TAC", async () => {
cli.getVisibleRooms = jest.fn().mockReturnValue([roomWithActivity]);
renderTAC();
await userEvent.click(getTACButton());
// We should not render the room with activity
expect(() => screen.getAllByRole("menuitem")).toThrow();
});
it("should render a room with a regular notification in the TAC", async () => {
cli.getVisibleRooms = jest.fn().mockReturnValue([roomWithNotif]);
renderTAC();
await userEvent.click(getTACButton());
const tacRows = screen.getAllByRole("menuitem");
expect(tacRows.length).toEqual(1);
getByText(tacRows[0], "A notification");
expect(tacRows[0].getElementsByClassName("mx_NotificationBadge_level_notification").length).toEqual(1);
});
it("should render a room with a highlight notification in the TAC", async () => {
cli.getVisibleRooms = jest.fn().mockReturnValue([roomWithHighlight]);
renderTAC();
await userEvent.click(getTACButton());
const tacRows = screen.getAllByRole("menuitem");
expect(tacRows.length).toEqual(1);
getByText(tacRows[0], "This is a real highlight");
expect(tacRows[0].getElementsByClassName("mx_NotificationBadge_level_highlight").length).toEqual(1);
});
it("renders notifications matching the snapshot", async () => {
cli.getVisibleRooms = jest.fn().mockReturnValue([roomWithHighlight, roomWithNotif, roomWithActivity]);
renderTAC();
await userEvent.click(getTACButton());
expect(screen.getByRole("menu")).toMatchSnapshot();
});
it("should display a caption when no threads are unread", async () => {
cli.getVisibleRooms = jest.fn().mockReturnValue([]);
renderTAC();
await userEvent.click(getTACButton());
expect(screen.getByRole("menu").getElementsByClassName("mx_ThreadsActivityCentre_emptyCaption").length).toEqual(
1,
);
});
it("should match snapshot when empty", async () => {
cli.getVisibleRooms = jest.fn().mockReturnValue([]);
renderTAC();
await userEvent.click(getTACButton());
expect(screen.getByRole("menu")).toMatchSnapshot();
});
it("should order the room with the same notification level by most recent", async () => {
// Generate two new rooms with threads
const secondRoomWithHighlight = new Room("!room4:server", cli, cli.getSafeUserId(), {
pendingEventOrdering: PendingEventOrdering.Detached,
});
secondRoomWithHighlight.name = "This is a second real highlight";
const secondHighlightThreadInfo = await populateThread({
...getDefaultThreadArgs(secondRoomWithHighlight),
// timestamp
ts: 1,
});
secondRoomWithHighlight.setThreadUnreadNotificationCount(
secondHighlightThreadInfo.thread.id,
NotificationCountType.Highlight,
1,
);
const thirdRoomWithHighlight = new Room("!room5:server", cli, cli.getSafeUserId(), {
pendingEventOrdering: PendingEventOrdering.Detached,
});
thirdRoomWithHighlight.name = "This is a third real highlight";
const thirdHighlightThreadInfo = await populateThread({
...getDefaultThreadArgs(thirdRoomWithHighlight),
// timestamp
ts: 7,
});
thirdRoomWithHighlight.setThreadUnreadNotificationCount(
thirdHighlightThreadInfo.thread.id,
NotificationCountType.Highlight,
1,
);
cli.getVisibleRooms = jest
.fn()
.mockReturnValue([roomWithHighlight, secondRoomWithHighlight, thirdRoomWithHighlight]);
renderTAC();
await userEvent.click(getTACButton());
// The room should be ordered by the most recent thread
// thirdHighlightThreadInfo (timestamp 7) > highlightThreadInfo (timestamp 5) > secondHighlightThreadInfo (timestamp 1)
expect(screen.getByRole("menu")).toMatchSnapshot();
});
it("should block Ctrl/CMD + k shortcut", async () => {
cli.getVisibleRooms = jest.fn().mockReturnValue([roomWithHighlight]);
const keyDownHandler = jest.fn();
render(
<div
onKeyDown={(evt) => {
keyDownHandler(evt.key, evt.ctrlKey);
}}
>
<MatrixClientContext.Provider value={cli}>
<ThreadsActivityCentre />
</MatrixClientContext.Provider>
</div>,
);
await userEvent.click(getTACButton());
// CTRL/CMD + k should be blocked
await userEvent.keyboard("{Control>}k{/Control}");
expect(keyDownHandler).not.toHaveBeenCalledWith("k", true);
// Sanity test
await userEvent.keyboard("{Control>}a{/Control}");
expect(keyDownHandler).toHaveBeenCalledWith("a", true);
});
});
@@ -0,0 +1,153 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<AddExistingToSpaceDialog /> looks as expected 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_AddExistingToSpace"
aria-labelledby="mx_BaseDialog_title"
class="mx_AddExistingToSpaceDialog"
data-focus-lock-disabled="false"
role="dialog"
tabindex="-1"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
<div
class="mx_SubspaceSelector"
>
<span
aria-label="Avatar"
class="_avatar_zysgz_8 mx_BaseAvatar"
data-color="2"
data-testid="avatar-img"
data-type="square"
style="--cpd-avatar-size: 40px;"
>
<img
alt=""
class="_image_zysgz_43"
data-type="square"
height="40px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="40px"
/>
</span>
<div>
<h1>
Add existing rooms
</h1>
<div
class="mx_SubspaceSelector_onlySpace"
>
!spaceid:example.com
</div>
</div>
</div>
</h1>
</div>
<div
class="mx_AddExistingToSpace"
>
<div
class="mx_SearchBox mx_textinput"
>
<input
autocomplete="off"
class="mx_textinput_icon mx_textinput_search mx_textinput_icon mx_textinput_search"
data-testid="searchbox-input"
placeholder="Search for rooms"
type="text"
value=""
/>
<div
class="mx_AccessibleButton mx_SearchBox_closeButton"
role="button"
tabindex="-1"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
/>
</svg>
</div>
</div>
<div
class="mx_AutoHideScrollbar mx_AddExistingToSpace_content"
tabindex="-1"
>
<span
class="mx_AddExistingToSpace_noResults"
>
No results
</span>
</div>
<div
class="mx_AddExistingToSpace_footer"
>
<span>
<div>
Want to add a new room instead?
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
role="button"
tabindex="0"
>
Create a new room
</div>
</span>
<div
aria-disabled="true"
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled"
disabled=""
role="button"
tabindex="0"
>
Add
</div>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
/>
</svg>
</div>
</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,38 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`QuickSettingsButton should render the quick settings button in expanded mode 1`] = `
<DocumentFragment>
<button
aria-expanded="true"
aria-label="Quick settings"
class="_icon-button_1215g_8 mx_QuickSettingsButton expanded"
data-kind="primary"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.731 2C13.432 2 14 2.568 14 3.269c0 .578.396 1.074.935 1.286q.128.052.253.106c.531.23 1.162.16 1.572-.25a1.27 1.27 0 0 1 1.794 0l1.034 1.035a1.27 1.27 0 0 1 0 1.794c-.41.41-.48 1.04-.248 1.572l.105.253c.212.539.708.935 1.286.935.701 0 1.269.568 1.269 1.269v1.462c0 .701-.568 1.269-1.269 1.269-.578 0-1.074.396-1.287.935q-.05.128-.104.253c-.232.531-.161 1.162.248 1.572a1.27 1.27 0 0 1 0 1.794l-1.034 1.034a1.27 1.27 0 0 1-1.794 0c-.41-.41-1.04-.48-1.572-.248a8 8 0 0 1-.253.105c-.539.212-.935.708-.935 1.286 0 .701-.568 1.269-1.269 1.269H11.27c-.702 0-1.27-.568-1.27-1.269 0-.578-.396-1.074-.935-1.287a8 8 0 0 1-.253-.104c-.531-.232-1.162-.161-1.572.248a1.27 1.27 0 0 1-1.794 0l-1.034-1.034a1.27 1.27 0 0 1 0-1.794c.41-.41.48-1.04.249-1.572a8 8 0 0 1-.106-.253C4.343 14.396 3.847 14 3.27 14 2.568 14 2 13.432 2 12.731V11.27c0-.702.568-1.27 1.269-1.27.578 0 1.074-.396 1.286-.935q.052-.128.106-.253c.23-.531.16-1.162-.25-1.572a1.27 1.27 0 0 1 0-1.794l1.035-1.034a1.27 1.27 0 0 1 1.794 0c.41.41 1.04.48 1.572.249a8 8 0 0 1 .253-.106c.539-.212.935-.708.935-1.286C10 2.568 10.568 2 11.269 2zM12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8"
/>
</svg>
<span
class="_typography_6v6n8_153 _font-body-md-regular_6v6n8_50 mx_QuickSettingsButton_label"
title="Settings"
>
Settings
</span>
</div>
</button>
</DocumentFragment>
`;
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<SpaceCreateMenu /> should render 1`] = `<DocumentFragment />`;
@@ -0,0 +1,394 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<SpacePanel /> should show all activated MetaSpaces in the correct order 1`] = `
<DocumentFragment>
<nav
aria-label="Spaces"
class="mx_SpacePanel collapsed newUi"
>
<div
class="mx_UserMenu"
>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="User menu"
class="mx_AccessibleButton mx_UserMenu_contextMenuButton"
role="button"
tabindex="0"
>
<div
class="mx_UserMenu_userAvatar"
>
<span
class="_avatar_zysgz_8 mx_BaseAvatar mx_UserMenu_userAvatar_BaseAvatar _avatar-imageless_zysgz_55"
data-color="5"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
>
t
</span>
</div>
</div>
<div
aria-label="Expand"
class="mx_AccessibleButton mx_SpacePanel_toggleCollapse"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
/>
</svg>
</div>
</div>
<ul
aria-label="Spaces"
class="mx_AutoHideScrollbar mx_SpaceTreeLevel"
data-rbd-droppable-context-id="0"
data-rbd-droppable-id="top-level-spaces"
role="tree"
tabindex="-1"
>
<li
aria-selected="false"
class="mx_SpaceItem collapsed"
role="treeitem"
>
<div
aria-label="Home"
class="mx_AccessibleButton mx_SpaceButton mx_SpaceButton_narrow mx_SpaceButton_withIcon"
role="button"
tabindex="0"
>
<div
class="mx_SpaceButton_selectionWrapper"
>
<div
class="mx_SpaceButton_avatarWrapper"
>
<div
class="mx_SpaceButton_avatarPlaceholder"
>
<div
class="mx_SpaceButton_icon"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.971 3.54 7 3.889A2 2 0 0 1 21 9.177V19a2 2 0 0 1-2 2h-4v-9H9v9H5a2 2 0 0 1-2-2V9.177a2 2 0 0 1 1.029-1.748l7-3.89a2 2 0 0 1 1.942 0"
/>
</svg>
</div>
</div>
</div>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_SpaceButton_menuButton"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
/>
</svg>
</div>
</div>
</div>
</li>
<li
aria-selected="false"
class="mx_SpaceItem collapsed"
role="treeitem"
>
<div
aria-label="Favourites"
class="mx_AccessibleButton mx_SpaceButton mx_SpaceButton_narrow mx_SpaceButton_withIcon"
role="button"
tabindex="-1"
>
<div
class="mx_SpaceButton_selectionWrapper"
>
<div
class="mx_SpaceButton_avatarWrapper"
>
<div
class="mx_SpaceButton_avatarPlaceholder"
>
<div
class="mx_SpaceButton_icon"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.897 2.817 2.336 4.733 5.223.76a1 1 0 0 1 .555 1.705L17.23 13.7l.892 5.202a1 1 0 0 1-1.45 1.054L12 17.5l-4.672 2.456a1 1 0 0 1-1.451-1.054l.892-5.202-3.78-3.685a1 1 0 0 1 .555-1.706l5.223-.759 2.336-4.733a1 1 0 0 1 1.794 0"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</li>
<li
aria-selected="false"
class="mx_SpaceItem collapsed"
role="treeitem"
>
<div
aria-label="People"
class="mx_AccessibleButton mx_SpaceButton mx_SpaceButton_narrow mx_SpaceButton_withIcon"
role="button"
tabindex="-1"
>
<div
class="mx_SpaceButton_selectionWrapper"
>
<div
class="mx_SpaceButton_avatarWrapper"
>
<div
class="mx_SpaceButton_avatarPlaceholder"
>
<div
class="mx_SpaceButton_icon"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 15q-1.65 0-2.825-1.175T8 11t1.175-2.825T12 7t2.825 1.175T16 11t-1.175 2.825T12 15"
/>
<path
d="M19.528 18.583A9.96 9.96 0 0 0 22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 2.52.933 4.824 2.472 6.583A9.98 9.98 0 0 0 12 22a9.98 9.98 0 0 0 7.528-3.417M8.75 16.388q-1.373.332-2.709.95a8 8 0 1 1 11.918 0 14.7 14.7 0 0 0-2.709-.95A13.8 13.8 0 0 0 12 16q-1.65 0-3.25.387"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</li>
<li
aria-selected="false"
class="mx_SpaceItem collapsed"
role="treeitem"
>
<div
aria-label="Other rooms"
class="mx_AccessibleButton mx_SpaceButton mx_SpaceButton_narrow mx_SpaceButton_withIcon"
role="button"
tabindex="-1"
>
<div
class="mx_SpaceButton_selectionWrapper"
>
<div
class="mx_SpaceButton_avatarWrapper"
>
<div
class="mx_SpaceButton_avatarPlaceholder"
>
<div
class="mx_SpaceButton_icon"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m8.566 17-.944 4.094q-.086.406-.372.656t-.687.25q-.543 0-.887-.469a1.18 1.18 0 0 1-.2-1.031l.801-3.5H3.158q-.572 0-.916-.484a1.27 1.27 0 0 1-.2-1.078 1.12 1.12 0 0 1 1.116-.938H6.85l1.145-5h-3.12q-.57 0-.915-.484a1.27 1.27 0 0 1-.2-1.078A1.12 1.12 0 0 1 4.875 7h3.691l.945-4.094q.085-.406.372-.656.286-.25.686-.25.544 0 .887.469.345.468.2 1.031l-.8 3.5h4.578l.944-4.094q.085-.406.372-.656.286-.25.687-.25.543 0 .887.469t.2 1.031L17.723 7h3.119q.573 0 .916.484.343.485.2 1.079a1.12 1.12 0 0 1-1.116.937H17.15l-1.145 5h3.12q.57 0 .915.484.343.485.2 1.079a1.12 1.12 0 0 1-1.116.937h-3.691l-.944 4.094q-.087.406-.373.656t-.686.25q-.544 0-.887-.469a1.18 1.18 0 0 1-.2-1.031l.8-3.5zm.573-2.5h4.578l1.144-5h-4.578z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</li>
<li
aria-selected="false"
class="mx_SpaceItem collapsed"
role="treeitem"
>
<div
aria-label="Conferences"
class="mx_AccessibleButton mx_SpaceButton mx_SpaceButton_narrow mx_SpaceButton_withIcon"
role="button"
tabindex="-1"
>
<div
class="mx_SpaceButton_selectionWrapper"
>
<div
class="mx_SpaceButton_avatarWrapper"
>
<div
class="mx_SpaceButton_avatarPlaceholder"
>
<div
class="mx_SpaceButton_icon"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</li>
<li
aria-selected="false"
class="mx_SpaceItem mx_SpaceItem_new collapsed"
role="treeitem"
>
<div
aria-label="Create a space"
class="mx_AccessibleButton mx_SpaceButton mx_SpaceButton_new mx_SpaceButton_narrow mx_SpaceButton_withIcon"
data-testid="create-space-button"
role="button"
tabindex="-1"
>
<div
class="mx_SpaceButton_selectionWrapper"
>
<div
class="mx_SpaceButton_avatarWrapper"
>
<div
class="mx_SpaceButton_avatarPlaceholder"
>
<div
class="mx_SpaceButton_icon"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 13H6a.97.97 0 0 1-.713-.287A.97.97 0 0 1 5 12q0-.424.287-.713A.97.97 0 0 1 6 11h5V6q0-.424.287-.713A.97.97 0 0 1 12 5q.424 0 .713.287Q13 5.576 13 6v5h5q.424 0 .712.287.288.288.288.713 0 .424-.288.713A.97.97 0 0 1 18 13h-5v5q0 .424-.287.712A.97.97 0 0 1 12 19a.97.97 0 0 1-.713-.288A.97.97 0 0 1 11 18z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</li>
</ul>
<div
class="mx_ThreadsActivityCentre_container"
>
<button
aria-disabled="false"
aria-expanded="false"
aria-haspopup="menu"
aria-label="Threads"
aria-labelledby="_r_12_"
class="_icon-button_1215g_8 mx_ThreadsActivityCentreButton"
data-kind="primary"
data-state="closed"
id="radix-_r_10_"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
type="button"
>
<div
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
class="mx_ThreadsActivityCentreButton_Icon"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 3h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6l-2.293 2.293c-.63.63-1.707.184-1.707-.707V5a2 2 0 0 1 2-2m3 7h10q.424 0 .712-.287A.97.97 0 0 0 18 9a.97.97 0 0 0-.288-.713A.97.97 0 0 0 17 8H7a.97.97 0 0 0-.713.287A.97.97 0 0 0 6 9q0 .424.287.713Q6.576 10 7 10m0 4h6q.424 0 .713-.287A.97.97 0 0 0 14 13a.97.97 0 0 0-.287-.713A.97.97 0 0 0 13 12H7a.97.97 0 0 0-.713.287A.97.97 0 0 0 6 13q0 .424.287.713Q6.576 14 7 14"
/>
</svg>
</div>
</button>
</div>
<button
aria-expanded="false"
aria-label="Quick settings"
aria-labelledby="_r_17_"
class="_icon-button_1215g_8 mx_QuickSettingsButton"
data-kind="primary"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
title="Quick settings"
>
<div
class="_indicator-icon_147l5_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.731 2C13.432 2 14 2.568 14 3.269c0 .578.396 1.074.935 1.286q.128.052.253.106c.531.23 1.162.16 1.572-.25a1.27 1.27 0 0 1 1.794 0l1.034 1.035a1.27 1.27 0 0 1 0 1.794c-.41.41-.48 1.04-.248 1.572l.105.253c.212.539.708.935 1.286.935.701 0 1.269.568 1.269 1.269v1.462c0 .701-.568 1.269-1.269 1.269-.578 0-1.074.396-1.287.935q-.05.128-.104.253c-.232.531-.161 1.162.248 1.572a1.27 1.27 0 0 1 0 1.794l-1.034 1.034a1.27 1.27 0 0 1-1.794 0c-.41-.41-1.04-.48-1.572-.248a8 8 0 0 1-.253.105c-.539.212-.935.708-.935 1.286 0 .701-.568 1.269-1.269 1.269H11.27c-.702 0-1.27-.568-1.27-1.269 0-.578-.396-1.074-.935-1.287a8 8 0 0 1-.253-.104c-.531-.232-1.162-.161-1.572.248a1.27 1.27 0 0 1-1.794 0l-1.034-1.034a1.27 1.27 0 0 1 0-1.794c.41-.41.48-1.04.249-1.572a8 8 0 0 1-.106-.253C4.343 14.396 3.847 14 3.27 14 2.568 14 2 13.432 2 12.731V11.27c0-.702.568-1.27 1.269-1.27.578 0 1.074-.396 1.286-.935q.052-.128.106-.253c.23-.531.16-1.162-.25-1.572a1.27 1.27 0 0 1 0-1.794l1.035-1.034a1.27 1.27 0 0 1 1.794 0c.41.41 1.04.48 1.572.249a8 8 0 0 1 .253-.106c.539-.212.935-.708.935-1.286C10 2.568 10.568 2 11.269 2zM12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8"
/>
</svg>
</div>
</button>
</nav>
</DocumentFragment>
`;
@@ -0,0 +1,539 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`<SpaceSettingsVisibilityTab /> for a public space Access renders guest access section toggle 1`] = `
<input
checked=""
class="_input_udcm8_24"
id="_r_h_"
role="switch"
type="checkbox"
/>
`;
exports[`<SpaceSettingsVisibilityTab /> for a public space renders addresses section with publish toggle 1`] = `
<DocumentFragment>
<div
class="mx_SettingsTab"
>
<div
class="mx_SettingsTab_sections"
>
<div
class="mx_SettingsSection"
>
<h2
class="mx_Heading_h3"
>
Visibility
</h2>
<div
class="mx_SettingsSection_subSections"
>
<fieldset
class="mx_SettingsFieldset"
data-testid="access-fieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
Access
</legend>
<div
class="mx_SettingsFieldset_description"
>
<div
class="mx_SettingsSubsection_text"
>
Decide who can view and join mock-space.
</div>
</div>
<div
class="mx_SettingsFieldset_content"
>
<label
class="mx_StyledRadioButton mx_JoinRuleSettings_radioButton mx_StyledRadioButton_disabled"
>
<input
aria-describedby="joinRule-invite-description"
disabled=""
id="joinRule-invite"
name="joinRule"
type="radio"
value="invite"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Private (invite only)
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<span
id="joinRule-invite-description"
>
Only invited people can join.
</span>
<label
class="mx_StyledRadioButton mx_JoinRuleSettings_radioButton mx_StyledRadioButton_disabled mx_StyledRadioButton_checked"
>
<input
aria-describedby="joinRule-public-description"
checked=""
disabled=""
id="joinRule-public"
name="joinRule"
type="radio"
value="public"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Public
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<span
id="joinRule-public-description"
>
Anyone can find and join.
</span>
<form
class="_root_19upo_16"
>
<div>
<div
aria-expanded="false"
class="mx_AccessibleButton mx_SettingsTab_showAdvanced mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
data-testid="toggle-guest-access-btn"
role="button"
tabindex="0"
>
Show advanced
</div>
</div>
<div
class="_inline-field_19upo_32"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_udcm8_10"
>
<input
checked=""
class="_input_udcm8_24"
id="_r_3j_"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="_r_3j_"
>
Preview Space
</label>
<span
class="_message_19upo_85 _help-message_19upo_91"
id="radix-_r_3l_"
>
Allow people to preview your space before they join.
</span>
</div>
</div>
<p>
<strong>
Recommended for public spaces.
</strong>
</p>
</form>
</div>
</fieldset>
<form
class="_root_19upo_16"
>
<div
class="mx_SettingsSection"
>
<h2
class="mx_Heading_h3"
>
Address
</h2>
<div
class="mx_SettingsSection_subSections"
>
<fieldset
class="mx_SettingsFieldset"
data-testid="published-address-fieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
Published Addresses
</legend>
<div
class="mx_SettingsFieldset_description"
>
<div
class="mx_SettingsSubsection_text"
>
Published addresses can be used by anyone on any server to join your space. To publish an address, it needs to be set as a local address first.
</div>
</div>
<div
class="mx_SettingsFieldset_content"
>
<div
class="mx_Field mx_Field_select"
>
<select
disabled=""
id="canonicalAlias"
label="Main address"
placeholder="Main address"
type="text"
>
<option
value=""
>
not specified
</option>
</select>
<label
for="canonicalAlias"
>
Main address
</label>
<svg
class="mx_Field_select_chevron"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
<div
class="_inline-field_19upo_32"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_udcm8_10"
>
<input
class="_input_udcm8_24"
disabled=""
id="_r_3u_"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="_r_3u_"
>
Publish this room to the public in matrix.org's room directory?
</label>
<span
class="_message_19upo_85 _help-message_19upo_91"
id="radix-_r_40_"
>
You must have permission to set the main address to publish this room.
</span>
</div>
</div>
<datalist
id="mx_AliasSettings_altRecommendations"
/>
<div
class="mx_EditableItemList"
id="roomAltAliases"
>
<div
class="mx_EditableItemList_label"
>
No other published addresses yet, add one below
</div>
<ul />
<div />
</div>
</div>
</fieldset>
<fieldset
class="mx_SettingsFieldset"
data-testid="local-address-fieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
Local Addresses
</legend>
<div
class="mx_SettingsFieldset_description"
>
<div
class="mx_SettingsSubsection_text"
>
Set addresses for this space so users can find this space through your homeserver (matrix.org)
</div>
</div>
<div
class="mx_SettingsFieldset_content"
>
<details>
<summary
class="mx_AliasSettings_localAddresses"
>
Show more
</summary>
<div
class="mx_EditableItemList"
id="roomAliases"
>
<div
class="mx_EditableItemList_label"
>
This space has no local addresses
</div>
<div
class="mx_Field mx_Field_input mx_RoomAliasField mx_Field_labelAlwaysTopLeft"
>
<span
class="mx_Field_prefix"
>
<span>
#
</span>
</span>
<input
autocomplete="off"
id="mx_Field_9"
label="Room address"
maxlength="243"
placeholder="e.g. my-room"
type="text"
value=""
/>
<label
for="mx_Field_9"
>
Room address
</label>
<span
class="mx_Field_postfix"
>
<span
title=":matrix.org"
>
:matrix.org
</span>
</span>
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Add
</div>
</div>
</details>
</div>
</fieldset>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;
exports[`<SpaceSettingsVisibilityTab /> renders container 1`] = `
<DocumentFragment>
<div
class="mx_SettingsTab"
>
<div
class="mx_SettingsTab_sections"
>
<div
class="mx_SettingsSection"
>
<h2
class="mx_Heading_h3"
>
Visibility
</h2>
<div
class="mx_SettingsSection_subSections"
>
<fieldset
class="mx_SettingsFieldset"
data-testid="access-fieldset"
>
<legend
class="mx_SettingsFieldset_legend"
>
Access
</legend>
<div
class="mx_SettingsFieldset_description"
>
<div
class="mx_SettingsSubsection_text"
>
Decide who can view and join mock-space.
</div>
</div>
<div
class="mx_SettingsFieldset_content"
>
<label
class="mx_StyledRadioButton mx_JoinRuleSettings_radioButton mx_StyledRadioButton_disabled mx_StyledRadioButton_checked"
>
<input
aria-describedby="joinRule-invite-description"
checked=""
disabled=""
id="joinRule-invite"
name="joinRule"
type="radio"
value="invite"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Private (invite only)
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<span
id="joinRule-invite-description"
>
Only invited people can join.
</span>
<label
class="mx_StyledRadioButton mx_JoinRuleSettings_radioButton mx_StyledRadioButton_disabled"
>
<input
aria-describedby="joinRule-public-description"
disabled=""
id="joinRule-public"
name="joinRule"
type="radio"
value="public"
/>
<div>
<div />
</div>
<div
class="mx_StyledRadioButton_content"
>
Public
</div>
<div
class="mx_StyledRadioButton_spacer"
/>
</label>
<span
id="joinRule-public-description"
>
Anyone can find and join.
</span>
<form
class="_root_19upo_16"
>
<div
class="_inline-field_19upo_32"
>
<div
class="_inline-field-control_19upo_44"
>
<div
class="_container_udcm8_10"
>
<input
checked=""
class="_input_udcm8_24"
id="_r_0_"
role="switch"
type="checkbox"
/>
<div
class="_ui_udcm8_34"
/>
</div>
</div>
<div
class="_inline-field-body_19upo_38"
>
<label
class="_label_19upo_59"
for="_r_0_"
>
Preview Space
</label>
<span
class="_message_19upo_85 _help-message_19upo_91"
id="radix-_r_2_"
>
Allow people to preview your space before they join.
</span>
</div>
</div>
<p>
<strong>
Recommended for public spaces.
</strong>
</p>
</form>
</div>
</fieldset>
<form
class="_root_19upo_16"
/>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;
@@ -0,0 +1,297 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`SpaceButton metaspace should render notificationState if one is provided 1`] = `
<DocumentFragment>
<div
aria-label="People"
class="mx_AccessibleButton mx_SpaceButton mx_SpaceButton_active"
data-testid="create-space-button"
role="button"
tabindex="-1"
>
<div
class="mx_SpaceButton_selectionWrapper"
>
<div
class="mx_SpaceButton_avatarWrapper"
>
<div
class="mx_SpaceButton_avatarPlaceholder"
>
<div
class="mx_SpaceButton_icon"
/>
</div>
<div
class="mx_SpacePanel_badgeContainer"
>
<div
class="mx_AccessibleButton mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_notification mx_NotificationBadge_2char cpd-theme-light"
role="button"
tabindex="-1"
>
<span
class="mx_NotificationBadge_count"
>
8
</span>
</div>
</div>
</div>
<span
class="mx_SpaceButton_name"
>
People
</span>
</div>
</div>
</DocumentFragment>
`;
exports[`SpaceItem should render a space with subspaces 1`] = `
<DocumentFragment>
<li
aria-expanded="false"
aria-selected="false"
class="mx_SpaceItem collapsed hasSubSpaces"
role="treeitem"
>
<div
aria-label="Root Space"
class="mx_AccessibleButton mx_SpaceButton"
role="button"
tabindex="-1"
>
<div
aria-label="Expand"
class="mx_AccessibleButton mx_SpaceButton_toggleCollapse"
role="button"
tabindex="-1"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
/>
</svg>
</div>
<div
class="mx_SpaceButton_selectionWrapper"
>
<div
class="mx_SpaceButton_avatarWrapper"
>
<span
aria-label="Avatar"
class="_avatar_zysgz_8 mx_BaseAvatar"
data-color="3"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_zysgz_43"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
<span
class="mx_SpaceButton_name"
>
Root Space
</span>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Space options"
class="mx_AccessibleButton mx_SpaceButton_menuButton"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
/>
</svg>
</div>
</div>
</div>
</li>
</DocumentFragment>
`;
exports[`SpaceItem should render a space with subspaces 2`] = `
<DocumentFragment>
<li
aria-expanded="true"
aria-selected="false"
class="mx_SpaceItem hasSubSpaces"
role="treeitem"
>
<div
aria-label="Root Space"
class="mx_AccessibleButton mx_SpaceButton"
role="button"
tabindex="-1"
>
<div
aria-label="Collapse"
class="mx_AccessibleButton mx_SpaceButton_toggleCollapse"
role="button"
tabindex="-1"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
<div
class="mx_SpaceButton_selectionWrapper"
>
<div
class="mx_SpaceButton_avatarWrapper"
>
<span
aria-label="Avatar"
class="_avatar_zysgz_8 mx_BaseAvatar"
data-color="3"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_zysgz_43"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
<span
class="mx_SpaceButton_name"
>
Root Space
</span>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Space options"
class="mx_AccessibleButton mx_SpaceButton_menuButton"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
/>
</svg>
</div>
</div>
</div>
<ul
class="mx_SpaceTreeLevel"
role="group"
>
<li
aria-selected="false"
class="mx_SpaceItem"
role="treeitem"
>
<div
aria-label="Subspace"
class="mx_AccessibleButton mx_SpaceButton"
role="button"
tabindex="-1"
>
<div
class="mx_SpaceButton_selectionWrapper"
>
<div
class="mx_SpaceButton_avatarWrapper"
>
<span
aria-label="Avatar"
class="_avatar_zysgz_8 mx_BaseAvatar"
data-color="4"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 24px;"
>
<img
alt=""
class="_image_zysgz_43"
data-type="round"
height="24px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="24px"
/>
</span>
</div>
<span
class="mx_SpaceButton_name"
>
Subspace
</span>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Space options"
class="mx_AccessibleButton mx_SpaceButton_menuButton"
role="button"
tabindex="0"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 14q-.824 0-1.412-.588A1.93 1.93 0 0 1 4 12q0-.825.588-1.412A1.93 1.93 0 0 1 6 10q.824 0 1.412.588Q8 11.175 8 12t-.588 1.412A1.93 1.93 0 0 1 6 14m6 0q-.825 0-1.412-.588A1.93 1.93 0 0 1 10 12q0-.825.588-1.412A1.93 1.93 0 0 1 12 10q.825 0 1.412.588Q14 11.175 14 12t-.588 1.412A1.93 1.93 0 0 1 12 14m6 0q-.824 0-1.413-.588A1.93 1.93 0 0 1 16 12q0-.825.587-1.412A1.93 1.93 0 0 1 18 10q.824 0 1.413.588Q20 11.175 20 12t-.587 1.412A1.93 1.93 0 0 1 18 14"
/>
</svg>
</div>
</div>
</div>
</li>
</ul>
</li>
</DocumentFragment>
`;
@@ -0,0 +1,333 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`ThreadsActivityCentre renders notifications matching the snapshot 1`] = `
<div
aria-labelledby="radix-_r_1m_"
aria-orientation="vertical"
class="_menu_1w1u7_8"
data-align="start"
data-orientation="vertical"
data-radix-menu-content=""
data-side="top"
data-state="open"
dir="ltr"
id="radix-_r_1n_"
role="menu"
style="outline: none; --radix-dropdown-menu-content-transform-origin: var(--radix-popper-transform-origin); --radix-dropdown-menu-content-available-width: var(--radix-popper-available-width); --radix-dropdown-menu-content-available-height: var(--radix-popper-available-height); --radix-dropdown-menu-trigger-width: var(--radix-popper-anchor-width); --radix-dropdown-menu-trigger-height: var(--radix-popper-anchor-height); pointer-events: auto;"
tabindex="-1"
>
<h3
class="_typography_6v6n8_153 _font-body-sm-semibold_6v6n8_36 _menu-title_1sgvx_8 _title_1w1u7_63"
id="_r_1u_"
>
Threads activity
</h3>
<div
class="mx_ThreadsActivityCentre_rows"
>
<button
class="mx_ThreadsActivityCentreRow _item_lqfwq_8 _interactive_lqfwq_26"
data-kind="primary"
data-orientation="vertical"
data-radix-collection-item=""
role="menuitem"
tabindex="-1"
>
<div
class="mx_DecoratedRoomAvatar _icon_lqfwq_50"
>
<span
class="_avatar_zysgz_8 mx_BaseAvatar _avatar-imageless_zysgz_55"
data-color="3"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
>
T
</span>
</div>
<span
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_lqfwq_34"
>
This is a real highlight
</span>
<svg
aria-hidden="true"
class="_nav-hint_lqfwq_59"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
/>
</svg>
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_dot"
>
<span
class="mx_NotificationBadge_count"
/>
</div>
</button>
<button
class="mx_ThreadsActivityCentreRow _item_lqfwq_8 _interactive_lqfwq_26"
data-kind="primary"
data-orientation="vertical"
data-radix-collection-item=""
role="menuitem"
tabindex="-1"
>
<div
class="mx_DecoratedRoomAvatar _icon_lqfwq_50"
>
<span
class="_avatar_zysgz_8 mx_BaseAvatar _avatar-imageless_zysgz_55"
data-color="2"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
>
A
</span>
</div>
<span
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_lqfwq_34"
>
A notification
</span>
<svg
aria-hidden="true"
class="_nav-hint_lqfwq_59"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
/>
</svg>
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_notification mx_NotificationBadge_dot"
>
<span
class="mx_NotificationBadge_count"
/>
</div>
</button>
</div>
</div>
`;
exports[`ThreadsActivityCentre should match snapshot when empty 1`] = `
<div
aria-labelledby="radix-_r_2a_"
aria-orientation="vertical"
class="_menu_1w1u7_8"
data-align="start"
data-orientation="vertical"
data-radix-menu-content=""
data-side="top"
data-state="open"
dir="ltr"
id="radix-_r_2b_"
role="menu"
style="outline: none; --radix-dropdown-menu-content-transform-origin: var(--radix-popper-transform-origin); --radix-dropdown-menu-content-available-width: var(--radix-popper-available-width); --radix-dropdown-menu-content-available-height: var(--radix-popper-available-height); --radix-dropdown-menu-trigger-width: var(--radix-popper-anchor-width); --radix-dropdown-menu-trigger-height: var(--radix-popper-anchor-height); pointer-events: auto;"
tabindex="-1"
>
<h3
class="_typography_6v6n8_153 _font-body-sm-semibold_6v6n8_36 _menu-title_1sgvx_8 _title_1w1u7_63"
id="_r_2i_"
>
Threads activity
</h3>
<div
class="mx_ThreadsActivityCentre_rows"
>
<div
class="mx_ThreadsActivityCentre_emptyCaption"
>
You don't have rooms with thread notifications yet.
</div>
</div>
</div>
`;
exports[`ThreadsActivityCentre should order the room with the same notification level by most recent 1`] = `
<div
aria-labelledby="radix-_r_2j_"
aria-orientation="vertical"
class="_menu_1w1u7_8"
data-align="start"
data-orientation="vertical"
data-radix-menu-content=""
data-side="top"
data-state="open"
dir="ltr"
id="radix-_r_2k_"
role="menu"
style="outline: none; --radix-dropdown-menu-content-transform-origin: var(--radix-popper-transform-origin); --radix-dropdown-menu-content-available-width: var(--radix-popper-available-width); --radix-dropdown-menu-content-available-height: var(--radix-popper-available-height); --radix-dropdown-menu-trigger-width: var(--radix-popper-anchor-width); --radix-dropdown-menu-trigger-height: var(--radix-popper-anchor-height); pointer-events: auto;"
tabindex="-1"
>
<h3
class="_typography_6v6n8_153 _font-body-sm-semibold_6v6n8_36 _menu-title_1sgvx_8 _title_1w1u7_63"
id="_r_2r_"
>
Threads activity
</h3>
<div
class="mx_ThreadsActivityCentre_rows"
>
<button
class="mx_ThreadsActivityCentreRow _item_lqfwq_8 _interactive_lqfwq_26"
data-kind="primary"
data-orientation="vertical"
data-radix-collection-item=""
role="menuitem"
tabindex="-1"
>
<div
class="mx_DecoratedRoomAvatar _icon_lqfwq_50"
>
<span
class="_avatar_zysgz_8 mx_BaseAvatar _avatar-imageless_zysgz_55"
data-color="5"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
>
T
</span>
</div>
<span
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_lqfwq_34"
>
This is a third real highlight
</span>
<svg
aria-hidden="true"
class="_nav-hint_lqfwq_59"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
/>
</svg>
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_dot"
>
<span
class="mx_NotificationBadge_count"
/>
</div>
</button>
<button
class="mx_ThreadsActivityCentreRow _item_lqfwq_8 _interactive_lqfwq_26"
data-kind="primary"
data-orientation="vertical"
data-radix-collection-item=""
role="menuitem"
tabindex="-1"
>
<div
class="mx_DecoratedRoomAvatar _icon_lqfwq_50"
>
<span
class="_avatar_zysgz_8 mx_BaseAvatar _avatar-imageless_zysgz_55"
data-color="3"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
>
T
</span>
</div>
<span
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_lqfwq_34"
>
This is a real highlight
</span>
<svg
aria-hidden="true"
class="_nav-hint_lqfwq_59"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
/>
</svg>
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_dot"
>
<span
class="mx_NotificationBadge_count"
/>
</div>
</button>
<button
class="mx_ThreadsActivityCentreRow _item_lqfwq_8 _interactive_lqfwq_26"
data-kind="primary"
data-orientation="vertical"
data-radix-collection-item=""
role="menuitem"
tabindex="-1"
>
<div
class="mx_DecoratedRoomAvatar _icon_lqfwq_50"
>
<span
class="_avatar_zysgz_8 mx_BaseAvatar _avatar-imageless_zysgz_55"
data-color="4"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
>
T
</span>
</div>
<span
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _label_lqfwq_34"
>
This is a second real highlight
</span>
<svg
aria-hidden="true"
class="_nav-hint_lqfwq_59"
fill="currentColor"
height="24"
viewBox="8 0 8 24"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7 17.3a.95.95 0 0 1-.275-.7q0-.425.275-.7l3.9-3.9-3.9-3.9a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l4.6 4.6q.15.15.213.325.062.175.062.375t-.062.375a.9.9 0 0 1-.213.325l-4.6 4.6a.95.95 0 0 1-.7.275.95.95 0 0 1-.7-.275"
/>
</svg>
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_dot"
>
<span
class="mx_NotificationBadge_count"
/>
</div>
</button>
</div>
</div>
`;
@@ -0,0 +1,164 @@
/*
* Copyright 2024 New Vector Ltd.
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import {
type MatrixClient,
MatrixEventEvent,
NotificationCountType,
PendingEventOrdering,
Room,
} from "matrix-js-sdk/src/matrix";
import { renderHook, act } from "jest-matrix-react";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { stubClient } from "../../../../test-utils";
import { populateThread } from "../../../../test-utils/threads";
import { NotificationLevel } from "../../../../../src/stores/notifications/NotificationLevel";
import { useUnreadThreadRooms } from "../../../../../src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms";
import SettingsStore from "../../../../../src/settings/SettingsStore";
describe("useUnreadThreadRooms", () => {
let client: MatrixClient;
let room: Room;
beforeEach(() => {
client = stubClient();
client.supportsThreads = () => true;
room = new Room("!room1:example.org", client, "@fee:bar", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
});
afterEach(() => {
jest.restoreAllMocks();
});
it("has no notifications with no rooms", async () => {
const { result } = renderHook(() => useUnreadThreadRooms(false));
const { greatestNotificationLevel, rooms } = result.current;
expect(greatestNotificationLevel).toBe(NotificationLevel.None);
expect(rooms.length).toEqual(0);
});
it("an activity notification is ignored by default", async () => {
const notifThreadInfo = await populateThread({
room: room,
client: client,
authorId: "@foo:bar",
participantUserIds: ["@fee:bar"],
});
room.setThreadUnreadNotificationCount(notifThreadInfo.thread.id, NotificationCountType.Total, 0);
client.getVisibleRooms = jest.fn().mockReturnValue([room]);
const wrapper = ({ children }: { children: React.ReactNode }) => (
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
);
const { result } = renderHook(() => useUnreadThreadRooms(true), { wrapper });
const { greatestNotificationLevel, rooms } = result.current;
expect(greatestNotificationLevel).toBe(NotificationLevel.None);
expect(rooms.length).toEqual(0);
});
it("an activity notification is displayed with the setting enabled", async () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
const notifThreadInfo = await populateThread({
room: room,
client: client,
authorId: "@foo:bar",
participantUserIds: ["@fee:bar"],
});
room.setThreadUnreadNotificationCount(notifThreadInfo.thread.id, NotificationCountType.Total, 0);
client.getVisibleRooms = jest.fn().mockReturnValue([room]);
const wrapper = ({ children }: { children: React.ReactNode }) => (
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
);
const { result } = renderHook(() => useUnreadThreadRooms(true), { wrapper });
const { greatestNotificationLevel, rooms } = result.current;
expect(greatestNotificationLevel).toBe(NotificationLevel.Activity);
expect(rooms.length).toEqual(1);
});
it("a notification and a highlight summarise to a highlight", async () => {
const notifThreadInfo = await populateThread({
room: room,
client: client,
authorId: "@foo:bar",
participantUserIds: ["@fee:bar"],
});
room.setThreadUnreadNotificationCount(notifThreadInfo.thread.id, NotificationCountType.Total, 1);
const highlightThreadInfo = await populateThread({
room: room,
client: client,
authorId: "@foo:bar",
participantUserIds: ["@fee:bar"],
});
room.setThreadUnreadNotificationCount(highlightThreadInfo.thread.id, NotificationCountType.Highlight, 1);
client.getVisibleRooms = jest.fn().mockReturnValue([room]);
const wrapper = ({ children }: { children: React.ReactNode }) => (
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
);
const { result } = renderHook(() => useUnreadThreadRooms(true), { wrapper });
const { greatestNotificationLevel, rooms } = result.current;
expect(greatestNotificationLevel).toBe(NotificationLevel.Highlight);
expect(rooms.length).toEqual(1);
});
describe("updates", () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it("updates on decryption within 1s", async () => {
const notifThreadInfo = await populateThread({
room: room,
client: client,
authorId: "@foo:bar",
participantUserIds: ["@fee:bar"],
});
room.setThreadUnreadNotificationCount(notifThreadInfo.thread.id, NotificationCountType.Total, 0);
client.getVisibleRooms = jest.fn().mockReturnValue([room]);
const wrapper = ({ children }: { children: React.ReactNode }) => (
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
);
const { result } = renderHook(() => useUnreadThreadRooms(true), { wrapper });
expect(result.current.greatestNotificationLevel).toBe(NotificationLevel.None);
act(() => {
room.setThreadUnreadNotificationCount(notifThreadInfo.thread.id, NotificationCountType.Highlight, 1);
client.emit(MatrixEventEvent.Decrypted, notifThreadInfo.thread.events[0]);
jest.advanceTimersByTime(1000);
});
expect(result.current.greatestNotificationLevel).toBe(NotificationLevel.Highlight);
});
});
});