* Remove stale max-len disablements * Remove stale camelCase & naming-convention disablements * Remove stale ban-ts-comment disablements * Remove stale no-var disablements * Remove stale no-empty-property disablements * Remove stale react rule disablements * Remove stale no-constant-condition disablements * Remove stale no-unused-vars disablements * Remove stale disablements for disabled rules * fixup camelcase * Remove dead code * Tidy code * Tweak oxlint config * Use oxlint to apply jsx/tsx extension consistently * Fix import * Fix imports * Rename affected snapshots * Update more imports * Enable restriction ruleset * Make code comply with new rules * Make code comply with react/button-has-type * Make code comply with typescript/non-nullable-type-assertion-style * Comply with node/no-process-env * Comply with unicorn/prefer-node-protocol * Comply with unicorn/import-style * Comply with unicorn/no-process-exit * Comply with no-proto * Comply with node/handle-callback-err * Comply with import/no-commonjs * Comply with node/no-path-concat * Comply with unicorn/no-length-as-slice-end * Comply with unicorn/no-document-cookie * Comply with unicorn/prefer-module * Comply with typescript/prefer-literal-enum-member * Comply with jsx-a11y/anchor-ambiguous-text * Tweak oxlint config * Fix resolves * Iterate * Iterate * Iterate * Iterate * Iterate * Iterate * Iterate
214 lines
8.3 KiB
TypeScript
214 lines
8.3 KiB
TypeScript
/*
|
|
Copyright 2026 Element Creations Ltd.
|
|
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, waitFor, within } 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 { TestSDKContext } from "../../../TestSDKContext.ts";
|
|
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";
|
|
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
|
|
import { Action } from "../../../../../src/dispatcher/actions";
|
|
|
|
// 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,
|
|
}
|
|
|
|
/* oxlint-disable typescript/prefer-literal-enum-member */
|
|
enum DragDirection {
|
|
LEFT = Keys.ARROW_LEFT,
|
|
UP = Keys.ARROW_UP,
|
|
RIGHT = Keys.ARROW_RIGHT,
|
|
DOWN = Keys.ARROW_DOWN,
|
|
}
|
|
/* oxlint-enable typescript/prefer-literal-enum-member */
|
|
|
|
// 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
|
|
(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", () => {
|
|
const EventEmitter = jest.requireActual("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();
|
|
start = jest.fn();
|
|
}
|
|
return 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"),
|
|
getClientWellKnown: jest.fn(),
|
|
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),
|
|
getAuthMetadata: jest.fn().mockRejectedValue(new Error("Legacy auth")),
|
|
} as unknown as MatrixClient;
|
|
const sdkContext = new TestSDKContext();
|
|
const SpacePanel = wrapInSdkContext(wrapInMatrixClientContext(UnwrappedSpacePanel), sdkContext);
|
|
|
|
beforeAll(() => {
|
|
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
|
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient);
|
|
sdkContext._client = mockClient;
|
|
});
|
|
|
|
beforeEach(() => {
|
|
sdkContext.spaceStore.enabledMetaSpaces.push(MetaSpace.Home, 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);
|
|
});
|
|
render(<SpacePanel />);
|
|
|
|
// Inspect the order of the rendered MetaSpaces, excluding the "Create a space" button.
|
|
const tree = screen.getByRole("tree", { name: "Spaces" });
|
|
const spaceButtons = within(tree)
|
|
.getAllByRole("treeitem")
|
|
.filter((el) => within(el).queryByRole("button", { name: "Create a space" }) === null);
|
|
|
|
const metaSpaceLabels = Array.from(spaceButtons).map((li) =>
|
|
within(li)
|
|
.getByRole("button", { name: /^(?!Options$).*/ }) // filter out the 'options' buttons within the buttons
|
|
.getAttribute("aria-label"),
|
|
);
|
|
expect(metaSpaceLabels).toEqual(["Home", "Other rooms", "Conferences"]);
|
|
|
|
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 () => {
|
|
(sdkContext.spaceStore.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(sdkContext.spaceStore.moveRootSpace).toHaveBeenCalledWith(0, 1);
|
|
});
|
|
|
|
it("should be able to open the user menu via dispatcher", async () => {
|
|
const { baseElement } = render(<SpacePanel />);
|
|
defaultDispatcher.dispatch({ action: Action.ToggleUserMenu });
|
|
await waitFor(() => {
|
|
// Menu exists outside the component due to Portals, so select it manually.
|
|
expect(baseElement.querySelector("div[aria-label='User menu']")).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|