Merge matrix-react-sdk into element-web

Merge remote-tracking branch 'repomerge/t3chguy/repomerge' into t3chguy/repo-merge

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2024-10-15 15:32:09 +01:00
3265 changed files with 484599 additions and 699 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
Please see LICENSE files in the repository root for full details.
*/
import { render } from "jest-matrix-react";
import { mocked } from "jest-mock";
import { 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
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.findByText("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
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,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
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 { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { MetaSpace, 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 { 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-var-requires
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,239 @@
/*
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 { mocked } from "jest-mock";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { act, fireEvent, render, RenderResult } from "jest-matrix-react";
import { EventType, MatrixClient, Room, GuestAccess, HistoryVisibility, JoinRule } 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;
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);
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(randomString).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;
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?.getAttribute("aria-checked")).toEqual("true");
fireEvent.click(guestAccessInput!);
expect(mockMatrixClient.sendStateEvent).toHaveBeenCalledWith(
mockSpaceId,
EventType.RoomGuestAccess,
// toggled off
{ guest_access: GuestAccess.Forbidden },
"",
);
// toggled off
expect(guestAccessInput?.getAttribute("aria-checked")).toEqual("false");
});
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)?.getAttribute("aria-disabled")).toEqual("true");
});
});
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)?.getAttribute("aria-checked")).toEqual("false");
});
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)?.getAttribute("aria-checked")).toEqual("false");
fireEvent.click(getHistoryVisibilityToggle(component)!);
expect(mockMatrixClient.sendStateEvent).toHaveBeenCalledWith(
mockSpaceId,
EventType.RoomHistoryVisibility,
{ history_visibility: HistoryVisibility.WorldReadable },
"",
);
expect(getHistoryVisibilityToggle(component)?.getAttribute("aria-checked")).toEqual("true");
});
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)?.getAttribute("aria-disabled")).toEqual("true");
});
});
it("renders addresses section", () => {
const space = makeMockSpace(mockMatrixClient, joinRule, guestRule);
const { getByTestId } = getComponent({ space });
expect(getByTestId("published-address-fieldset")).toBeTruthy();
expect(getByTestId("local-address-fieldset")).toBeTruthy();
});
});
});
@@ -0,0 +1,129 @@
/*
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, getByTestId, render } from "jest-matrix-react";
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 { SpaceButton } from "../../../../../src/components/views/spaces/SpaceTreeLevel";
import { MetaSpace, 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-var-requires
const EventEmitter = require("events");
class MockSpaceStore extends EventEmitter {
activeSpace: SpaceKey = "!space1";
setActiveSpace = 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();
});
});
});
@@ -0,0 +1,274 @@
/*
* 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, { 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";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
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,
);
});
beforeEach(async () => {
await SettingsStore.setValue("feature_release_announcement", null, SettingLevel.DEVICE, false);
});
it("should render the threads activity centre button", async () => {
renderTAC();
expect(getTACButton()).toBeInTheDocument();
});
it("should render the release announcement", async () => {
// Enable release announcement
await SettingsStore.setValue("feature_release_announcement", null, SettingLevel.DEVICE, true);
renderTAC();
expect(document.body).toMatchSnapshot();
});
it("should render not display the tooltip when the release announcement is displayed", async () => {
// Enable release announcement
await SettingsStore.setValue("feature_release_announcement", null, SettingLevel.DEVICE, true);
renderTAC();
// The tooltip should not be displayed
await userEvent.hover(getTACButton());
expect(screen.queryByRole("tooltip")).toBeNull();
});
it("should close the release announcement when the TAC button is clicked", async () => {
// Enable release announcement
await SettingsStore.setValue("feature_release_announcement", null, SettingLevel.DEVICE, true);
renderTAC();
await userEvent.click(getTACButton());
expect(getTACMenu()).toBeInTheDocument();
expect(document.body).toMatchSnapshot();
});
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,128 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
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"
>
<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_mcap2_17 mx_BaseAvatar"
data-color="2"
data-testid="avatar-img"
data-type="square"
style="--cpd-avatar-size: 40px;"
>
<img
alt=""
class="_image_mcap2_50"
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"
/>
</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"
/>
</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,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`QuickSettingsButton should render the quick settings button in expanded mode 1`] = `
<DocumentFragment>
<div
aria-expanded="true"
aria-label="Quick settings"
class="mx_AccessibleButton mx_QuickSettingsButton expanded"
role="button"
tabindex="0"
>
Settings
</div>
</DocumentFragment>
`;
@@ -0,0 +1,289 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SpacePanel /> should show all activated MetaSpaces in the correct order 1`] = `
<DocumentFragment>
<nav
aria-label="Spaces"
class="mx_SpacePanel collapsed"
>
<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_mcap2_17 mx_BaseAvatar mx_UserMenu_userAvatar_BaseAvatar _avatar-imageless_mcap2_61"
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"
/>
</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_home mx_SpaceButton_narrow"
role="button"
tabindex="0"
>
<div
class="mx_SpaceButton_selectionWrapper"
>
<div
class="mx_SpaceButton_avatarWrapper"
>
<div
class="mx_SpaceButton_avatarPlaceholder"
>
<div
class="mx_SpaceButton_icon"
/>
</div>
</div>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_SpaceButton_menuButton"
role="button"
tabindex="0"
/>
</div>
</div>
</li>
<li
aria-selected="false"
class="mx_SpaceItem collapsed"
role="treeitem"
>
<div
aria-label="Favourites"
class="mx_AccessibleButton mx_SpaceButton mx_SpaceButton_favourites mx_SpaceButton_narrow"
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>
</div>
</div>
</li>
<li
aria-selected="false"
class="mx_SpaceItem collapsed"
role="treeitem"
>
<div
aria-label="People"
class="mx_AccessibleButton mx_SpaceButton mx_SpaceButton_people mx_SpaceButton_narrow"
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>
</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_orphans mx_SpaceButton_narrow"
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>
</div>
</div>
</li>
<li
aria-selected="false"
class="mx_SpaceItem collapsed"
role="treeitem"
>
<div
aria-label="Conferences"
class="mx_AccessibleButton mx_SpaceButton mx_SpaceButton_videoRooms mx_SpaceButton_narrow"
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>
</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"
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>
</div>
</div>
</li>
</ul>
<div
class="mx_ThreadsActivityCentre_container"
>
<button
aria-controls="floating-ui-40"
aria-describedby="floating-ui-40"
aria-expanded="true"
aria-haspopup="dialog"
aria-label="Threads"
aria-labelledby="floating-ui-33"
class="_icon-button_bh2qc_17 mx_ThreadsActivityCentreButton"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
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-2Zm3 7h10a.97.97 0 0 0 .712-.287A.967.967 0 0 0 18 9a.967.967 0 0 0-.288-.713A.968.968 0 0 0 17 8H7a.968.968 0 0 0-.713.287A.968.968 0 0 0 6 9c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 4h6c.283 0 .52-.096.713-.287A.968.968 0 0 0 14 13a.968.968 0 0 0-.287-.713A.968.968 0 0 0 13 12H7a.967.967 0 0 0-.713.287A.968.968 0 0 0 6 13c0 .283.096.52.287.713.192.191.43.287.713.287Z"
/>
</svg>
</div>
</button>
<span
data-floating-ui-focus-guard=""
data-type="outside"
role="button"
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;"
tabindex="0"
/>
<span
aria-owns="undefined"
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;"
/>
<span
data-floating-ui-focus-guard=""
data-type="outside"
role="button"
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;"
tabindex="0"
/>
</div>
<div
aria-expanded="false"
aria-label="Quick settings"
class="mx_AccessibleButton mx_QuickSettingsButton"
role="button"
tabindex="0"
/>
</nav>
</DocumentFragment>
`;
@@ -0,0 +1,158 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SpaceSettingsVisibilityTab /> for a public space Access renders guest access section toggle 1`] = `
<div
aria-checked="true"
aria-disabled="false"
aria-labelledby="mx_LabelledToggleSwitch_testid_1"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
`;
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>
<div
class="mx_SettingsTab_toggleWithDescription"
>
<div
class="mx_SettingsFlag"
>
<span
class="mx_SettingsFlag_label"
>
<div
id="mx_LabelledToggleSwitch_testid_0"
>
Preview Space
</div>
</span>
<div
aria-checked="true"
aria-disabled="false"
aria-labelledby="mx_LabelledToggleSwitch_testid_0"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on mx_ToggleSwitch_enabled"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<p>
Allow people to preview your space before they join.
<br />
<strong>
Recommended for public spaces.
</strong>
</p>
</div>
</div>
</fieldset>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;
@@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
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>
`;
@@ -0,0 +1,651 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ThreadsActivityCentre renders notifications matching the snapshot 1`] = `
<div
aria-labelledby="radix-16"
aria-orientation="vertical"
class="_menu_1x5h1_17"
data-align="start"
data-orientation="vertical"
data-radix-menu-content=""
data-side="top"
data-state="open"
dir="ltr"
id="radix-17"
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_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _title_1x5h1_83"
id=":r6:"
>
Threads activity
</h3>
<div
class="mx_ThreadsActivityCentre_rows"
>
<button
class="mx_ThreadsActivityCentreRow _item_8j2l6_17 _interactive_8j2l6_35"
data-kind="primary"
data-orientation="vertical"
data-radix-collection-item=""
role="menuitem"
tabindex="-1"
>
<div
class="_icon_8j2l6_43"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="3"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
>
T
</span>
</div>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_8j2l6_52"
>
This is a real highlight
</span>
<svg
aria-hidden="true"
class="_nav-hint_8j2l6_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.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.877.877 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
/>
</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_8j2l6_17 _interactive_8j2l6_35"
data-kind="primary"
data-orientation="vertical"
data-radix-collection-item=""
role="menuitem"
tabindex="-1"
>
<div
class="_icon_8j2l6_43"
>
<span
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: 32px;"
>
A
</span>
</div>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_8j2l6_52"
>
A notification
</span>
<svg
aria-hidden="true"
class="_nav-hint_8j2l6_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.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.877.877 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
/>
</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 close the release announcement when the TAC button is clicked 1`] = `
<body
data-scroll-locked="1"
style="pointer-events: none;"
>
<span
aria-hidden="true"
data-aria-hidden="true"
data-radix-focus-guard=""
style="outline: none; opacity: 0; position: fixed; pointer-events: none;"
tabindex="0"
/>
<div
aria-hidden="true"
data-aria-hidden="true"
>
<div
class="mx_ThreadsActivityCentre_container"
>
<button
aria-controls="radix-3"
aria-disabled="false"
aria-expanded="true"
aria-haspopup="menu"
aria-label="Threads"
aria-labelledby="floating-ui-42"
class="_icon-button_bh2qc_17 mx_ThreadsActivityCentreButton"
data-state="open"
id="radix-2"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
type="button"
>
<div
class="_indicator-icon_133tf_26"
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-2Zm3 7h10a.97.97 0 0 0 .712-.287A.967.967 0 0 0 18 9a.967.967 0 0 0-.288-.713A.968.968 0 0 0 17 8H7a.968.968 0 0 0-.713.287A.968.968 0 0 0 6 9c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 4h6c.283 0 .52-.096.713-.287A.968.968 0 0 0 14 13a.968.968 0 0 0-.287-.713A.968.968 0 0 0 13 12H7a.967.967 0 0 0-.713.287A.968.968 0 0 0 6 13c0 .283.096.52.287.713.192.191.43.287.713.287Z"
/>
</svg>
</div>
</button>
</div>
</div>
<div
aria-hidden="true"
data-aria-hidden="true"
data-floating-ui-portal=""
id="floating-ui-46"
>
<div
class="_tooltip_1pslb_17 _invisible_1pslb_30"
style="position: absolute; left: 0px; top: 0px; transform: translate(6px, 5px);"
tabindex="-1"
>
<svg
aria-hidden="true"
class="_arrow_1pslb_42"
height="10"
style="position: absolute; pointer-events: none; right: calc(100% - 0px); transform: rotate(90deg); top: -1px;"
viewBox="0 0 10 10"
width="10"
>
<path
d="M0,0 H10 L5,6 Q5,6 5,6 Z"
stroke="none"
/>
<clippath
id="floating-ui-47"
>
<rect
height="10"
width="10"
x="0"
y="0"
/>
</clippath>
</svg>
<span
id="floating-ui-42"
>
Threads
</span>
</div>
</div>
<div
data-radix-popper-content-wrapper=""
dir="ltr"
style="position: fixed; left: 0px; top: 0px; transform: translate(0px, -8px); min-width: max-content; --radix-popper-available-width: 0px; --radix-popper-available-height: -8px; --radix-popper-anchor-width: 0px; --radix-popper-anchor-height: 0px; --radix-popper-transform-origin: 0% 0px;"
>
<div
aria-labelledby="radix-2"
aria-orientation="vertical"
class="_menu_1x5h1_17"
data-align="start"
data-orientation="vertical"
data-radix-menu-content=""
data-side="top"
data-state="open"
dir="ltr"
id="radix-3"
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_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _title_1x5h1_83"
id=":r1:"
>
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>
</div>
<span
aria-hidden="true"
data-aria-hidden="true"
data-radix-focus-guard=""
style="outline: none; opacity: 0; position: fixed; pointer-events: none;"
tabindex="0"
/>
</body>
`;
exports[`ThreadsActivityCentre should match snapshot when empty 1`] = `
<div
aria-labelledby="radix-22"
aria-orientation="vertical"
class="_menu_1x5h1_17"
data-align="start"
data-orientation="vertical"
data-radix-menu-content=""
data-side="top"
data-state="open"
dir="ltr"
id="radix-23"
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_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _title_1x5h1_83"
id=":r8:"
>
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-24"
aria-orientation="vertical"
class="_menu_1x5h1_17"
data-align="start"
data-orientation="vertical"
data-radix-menu-content=""
data-side="top"
data-state="open"
dir="ltr"
id="radix-25"
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_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _title_1x5h1_83"
id=":r9:"
>
Threads activity
</h3>
<div
class="mx_ThreadsActivityCentre_rows"
>
<button
class="mx_ThreadsActivityCentreRow _item_8j2l6_17 _interactive_8j2l6_35"
data-kind="primary"
data-orientation="vertical"
data-radix-collection-item=""
role="menuitem"
tabindex="-1"
>
<div
class="_icon_8j2l6_43"
>
<span
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: 32px;"
>
T
</span>
</div>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_8j2l6_52"
>
This is a third real highlight
</span>
<svg
aria-hidden="true"
class="_nav-hint_8j2l6_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.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.877.877 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
/>
</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_8j2l6_17 _interactive_8j2l6_35"
data-kind="primary"
data-orientation="vertical"
data-radix-collection-item=""
role="menuitem"
tabindex="-1"
>
<div
class="_icon_8j2l6_43"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="3"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
>
T
</span>
</div>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_8j2l6_52"
>
This is a real highlight
</span>
<svg
aria-hidden="true"
class="_nav-hint_8j2l6_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.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.877.877 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
/>
</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_8j2l6_17 _interactive_8j2l6_35"
data-kind="primary"
data-orientation="vertical"
data-radix-collection-item=""
role="menuitem"
tabindex="-1"
>
<div
class="_icon_8j2l6_43"
>
<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: 32px;"
>
T
</span>
</div>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_8j2l6_52"
>
This is a second real highlight
</span>
<svg
aria-hidden="true"
class="_nav-hint_8j2l6_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.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.877.877 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
/>
</svg>
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_dot"
>
<span
class="mx_NotificationBadge_count"
/>
</div>
</button>
</div>
</div>
`;
exports[`ThreadsActivityCentre should render the release announcement 1`] = `
<body>
<div
data-floating-ui-inert=""
>
<div
class="mx_ThreadsActivityCentre_container"
>
<button
aria-controls="floating-ui-8"
aria-describedby="floating-ui-8"
aria-expanded="true"
aria-haspopup="dialog"
aria-label="Threads"
aria-labelledby="floating-ui-10"
class="_icon-button_bh2qc_17 mx_ThreadsActivityCentreButton"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
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-2Zm3 7h10a.97.97 0 0 0 .712-.287A.967.967 0 0 0 18 9a.967.967 0 0 0-.288-.713A.968.968 0 0 0 17 8H7a.968.968 0 0 0-.713.287A.968.968 0 0 0 6 9c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 4h6c.283 0 .52-.096.713-.287A.968.968 0 0 0 14 13a.968.968 0 0 0-.287-.713A.968.968 0 0 0 13 12H7a.967.967 0 0 0-.713.287A.968.968 0 0 0 6 13c0 .283.096.52.287.713.192.191.43.287.713.287Z"
/>
</svg>
</div>
</button>
<span
data-floating-ui-focus-guard=""
data-type="outside"
role="button"
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;"
tabindex="0"
/>
<span
aria-owns="floating-ui-15"
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;"
/>
<span
data-floating-ui-focus-guard=""
data-type="outside"
role="button"
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;"
tabindex="0"
/>
</div>
</div>
<div
data-floating-ui-inert=""
data-floating-ui-portal=""
id="floating-ui-14"
>
<div
class="_tooltip_1pslb_17 _invisible_1pslb_30"
style="position: absolute; left: 0px; top: 0px; transform: translate(0px, 0px);"
tabindex="-1"
>
<svg
aria-hidden="true"
class="_arrow_1pslb_42"
height="10"
style="position: absolute; pointer-events: none; right: calc(100% - 0px); transform: rotate(90deg);"
viewBox="0 0 10 10"
width="10"
>
<path
d="M0,0 H10 L5,6 Q5,6 5,6 Z"
stroke="none"
/>
<clippath
id="floating-ui-16"
>
<rect
height="10"
width="10"
x="0"
y="0"
/>
</clippath>
</svg>
<span
id="floating-ui-10"
>
Threads
</span>
</div>
</div>
<div
data-floating-ui-portal=""
id="floating-ui-15"
>
<span
data-floating-ui-focus-guard=""
data-floating-ui-inert=""
data-type="inside"
role="button"
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;"
tabindex="0"
/>
<div
aria-describedby="floating-ui-7"
aria-labelledby="floating-ui-6"
class="_content_1oa1y_17"
id="floating-ui-8"
role="dialog"
style="position: absolute; left: 0px; top: 0px; transform: translate(0px, 0px);"
tabindex="-1"
>
<svg
aria-hidden="true"
class="_arrow_1oa1y_62"
height="20"
style="position: absolute; pointer-events: none; right: calc(100% - 0px); transform: rotate(90deg);"
viewBox="0 0 20 20"
width="20"
>
<path
d="M0,0 H20 L10,12 Q10,12 10,12 Z"
stroke="none"
/>
<clippath
id="floating-ui-17"
>
<rect
height="20"
width="20"
x="0"
y="0"
/>
</clippath>
</svg>
<h3
class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83 _header_1oa1y_46"
id="floating-ui-6"
>
Threads Activity Centre
</h3>
<span
class="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _description_1oa1y_52"
id="floating-ui-7"
>
Threads notifications have moved, find them here from now on.
</span>
<button
class="_button_i91xf_17 _button_1oa1y_57"
data-kind="secondary"
data-size="sm"
role="button"
tabindex="0"
>
OK
</button>
</div>
<span
data-floating-ui-focus-guard=""
data-floating-ui-inert=""
data-type="inside"
role="button"
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: fixed; white-space: nowrap; width: 1px; top: 0px; left: 0px;"
tabindex="0"
/>
</div>
</body>
`;
@@ -0,0 +1,165 @@
/*
* 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 { renderHook } from "@testing-library/react-hooks";
import {
MatrixClient,
MatrixEventEvent,
NotificationCountType,
PendingEventOrdering,
Room,
} from "matrix-js-sdk/src/matrix";
import { 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);
});
});
});