Add customisation point to disable space creation (#7766)

* mock matchMedia in jest setup

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use UIComponent.CreateSpaces in space panel

Signed-off-by: Kerry Archibald <kerrya@element.io>

* lint

Signed-off-by: Kerry Archibald <kerrya@element.io>

* hide add space in spacecontextmenu

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use UIComponent customistations in space oom view add space button

Signed-off-by: Kerry Archibald <kerrya@element.io>

* copyright

Signed-off-by: Kerry Archibald <kerrya@element.io>
pull/21833/head
Kerry 2022-02-09 17:21:40 +01:00 committed by GitHub
parent 6e8edbb418
commit 818fddd72c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 890 additions and 48 deletions

View File

@ -363,6 +363,8 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISp
const SpaceLandingAddButton = ({ space }) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
const canCreateRoom = shouldShowComponent(UIComponent.CreateRooms);
const canCreateSpace = shouldShowComponent(UIComponent.CreateSpaces);
let contextMenu;
if (menuDisplayed) {
@ -376,7 +378,7 @@ const SpaceLandingAddButton = ({ space }) => {
compact
>
<IconizedContextMenuOptionList first>
<IconizedContextMenuOption
{ canCreateRoom && <IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomList_iconPlus"
onClick={async (e) => {
@ -388,7 +390,7 @@ const SpaceLandingAddButton = ({ space }) => {
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
}
}}
/>
/> }
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomList_iconAddExistingRoom"
@ -399,18 +401,20 @@ const SpaceLandingAddButton = ({ space }) => {
showAddExistingRooms(space);
}}
/>
<IconizedContextMenuOption
label={_t("Add space")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
showCreateNewSubspace(space);
}}
>
<BetaPill />
</IconizedContextMenuOption>
{ canCreateSpace &&
<IconizedContextMenuOption
label={_t("Add space")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
showCreateNewSubspace(space);
}}
>
<BetaPill />
</IconizedContextMenuOption>
}
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
}
@ -449,10 +453,11 @@ const SpaceLanding = ({ space }: { space: Room }) => {
);
}
const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
const hasAddRoomPermissions = myMembership === "join" &&
space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
let addRoomButton;
if (canAddRooms) {
if (hasAddRoomPermissions) {
addRoomButton = <SpaceLandingAddButton space={space} />;
}

View File

@ -36,6 +36,8 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import { BetaPill } from "../beta/BetaCard";
import SettingsStore from "../../../settings/SettingsStore";
import { Action } from "../../../dispatcher/actions";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
interface IProps extends IContextMenuProps {
space: Room;
@ -58,6 +60,7 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
inviteOption = (
<IconizedContextMenuOption
data-test-id='invite-option'
className="mx_SpacePanel_contextMenu_inviteButton"
iconClassName="mx_SpacePanel_iconInvite"
label={_t("Invite")}
@ -79,6 +82,7 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
settingsOption = (
<IconizedContextMenuOption
data-test-id='settings-option'
iconClassName="mx_SpacePanel_iconSettings"
label={_t("Settings")}
onClick={onSettingsClick}
@ -95,6 +99,7 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
leaveOption = (
<IconizedContextMenuOption
data-test-id='leave-option'
iconClassName="mx_SpacePanel_iconLeave"
className="mx_IconizedContextMenu_option_red"
label={_t("Leave space")}
@ -126,10 +131,12 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
);
}
const canAddRooms = space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
const hasPermissionToAddSpaceChild = space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
const canAddRooms = hasPermissionToAddSpaceChild && shouldShowComponent(UIComponent.CreateRooms);
const canAddSubSpaces = hasPermissionToAddSpaceChild && shouldShowComponent(UIComponent.CreateSpaces);
let newRoomSection: JSX.Element;
if (space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
if (canAddRooms || canAddSubSpaces) {
const onNewRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
@ -147,21 +154,27 @@ const SpaceContextMenu = ({ space, hideHeader, onFinished, ...props }: IProps) =
};
newRoomSection = <>
<div className="mx_SpacePanel_contextMenu_separatorLabel">
<div data-test-id='add-to-space-header' className="mx_SpacePanel_contextMenu_separatorLabel">
{ _t("Add") }
</div>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Room")}
onClick={onNewRoomClick}
/>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Space")}
onClick={onNewSubspaceClick}
>
<BetaPill />
</IconizedContextMenuOption>
{ canAddRooms &&
<IconizedContextMenuOption
data-test-id='new-room-option'
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Room")}
onClick={onNewRoomClick}
/>
}
{ canAddSubSpaces &&
<IconizedContextMenuOption
data-test-id='new-subspace-option'
iconClassName="mx_SpacePanel_iconPlus"
label={_t("Space")}
onClick={onNewSubspaceClick}
>
<BetaPill />
</IconizedContextMenuOption>
}
</>;
}

View File

@ -191,6 +191,8 @@ const DmAuxButton = ({ tabIndex, dispatcher = defaultDispatcher }: IAuxButtonPro
title={_t("Start chat")}
/>;
}
return null;
};
const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {

View File

@ -70,6 +70,8 @@ import { ActionPayload } from "../../../dispatcher/payloads";
import { Action } from "../../../dispatcher/actions";
import { NotificationState } from "../../../stores/notifications/NotificationState";
import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => {
const invites = useEventEmitterState<Room[]>(SpaceStore.instance, UPDATE_INVITED_SPACES, () => {
@ -235,6 +237,7 @@ const CreateSpaceButton = ({
role="treeitem"
>
<SpaceButton
data-test-id='create-space-button'
className={classNames("mx_SpaceButton_new", {
mx_SpaceButton_newCancel: menuDisplayed,
})}
@ -316,7 +319,11 @@ const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({
</Draggable>
)) }
{ children }
<CreateSpaceButton isPanelCollapsed={isPanelCollapsed} setPanelCollapsed={setPanelCollapsed} />
{
shouldShowComponent(UIComponent.CreateSpaces) &&
<CreateSpaceButton isPanelCollapsed={isPanelCollapsed} setPanelCollapsed={setPanelCollapsed} />
}
</IndicatorScrollbar>;
});

View File

@ -38,4 +38,5 @@ export enum UIFeature {
export enum UIComponent {
InviteUsers = "UIComponent.sendInvites",
CreateRooms = "UIComponent.roomCreation",
CreateSpaces = "UIComponent.spaceCreation",
}

View File

@ -0,0 +1,222 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { mount } from 'enzyme';
import { Room } from 'matrix-js-sdk';
import { mocked } from 'jest-mock';
import { act } from 'react-dom/test-utils';
import '../../../skinned-sdk';
import SpaceContextMenu from '../../../../src/components/views/context_menus/SpaceContextMenu';
import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
import { findByTestId } from '../../../utils/test-utils';
import {
leaveSpace,
shouldShowSpaceSettings,
showCreateNewRoom,
showCreateNewSubspace,
showSpaceInvite,
showSpaceSettings,
} from '../../../../src/utils/space';
import { shouldShowComponent } from '../../../../src/customisations/helpers/UIComponents';
import { UIComponent } from '../../../../src/settings/UIFeature';
jest.mock('../../../../src/customisations/helpers/UIComponents', () => ({
shouldShowComponent: jest.fn(),
}));
jest.mock('../../../../src/utils/space', () => ({
leaveSpace: jest.fn(),
shouldShowSpaceSettings: jest.fn(),
showCreateNewRoom: jest.fn(),
showCreateNewSubspace: jest.fn(),
showSpaceInvite: jest.fn(),
showSpacePreferences: jest.fn(),
showSpaceSettings: jest.fn(),
}));
jest.mock('../../../../src/stores/spaces/SpaceStore', () => ({
spacesEnabled: true,
}));
describe('<SpaceContextMenu />', () => {
const userId = '@test:server';
const mockClient = {
getUserId: jest.fn().mockReturnValue(userId),
};
const makeMockSpace = (props = {}) => ({
name: 'test space',
getJoinRule: jest.fn(),
canInvite: jest.fn(),
currentState: {
maySendStateEvent: jest.fn(),
},
client: mockClient,
getMyMembership: jest.fn(),
...props,
}) as unknown as Room;
const defaultProps = {
space: makeMockSpace(),
onFinished: jest.fn(),
};
const getComponent = (props = {}) =>
mount(<SpaceContextMenu {...defaultProps} {...props} />,
{
wrappingComponent: MatrixClientContext.Provider,
wrappingComponentProps: {
value: mockClient,
},
});
beforeEach(() => {
jest.resetAllMocks();
mockClient.getUserId.mockReturnValue(userId);
});
it('renders menu correctly', () => {
const component = getComponent();
expect(component).toMatchSnapshot();
});
it('renders invite option when space is public', () => {
const space = makeMockSpace({
getJoinRule: jest.fn().mockReturnValue('public'),
});
const component = getComponent({ space });
expect(findByTestId(component, 'invite-option').length).toBeTruthy();
});
it('renders invite option when user is has invite rights for space', () => {
const space = makeMockSpace({
canInvite: jest.fn().mockReturnValue(true),
});
const component = getComponent({ space });
expect(space.canInvite).toHaveBeenCalledWith(userId);
expect(findByTestId(component, 'invite-option').length).toBeTruthy();
});
it('opens invite dialog when invite option is clicked', () => {
const space = makeMockSpace({
getJoinRule: jest.fn().mockReturnValue('public'),
});
const onFinished = jest.fn();
const component = getComponent({ space, onFinished });
act(() => {
findByTestId(component, 'invite-option').at(0).simulate('click');
});
expect(showSpaceInvite).toHaveBeenCalledWith(space);
expect(onFinished).toHaveBeenCalled();
});
it('renders space settings option when user has rights', () => {
mocked(shouldShowSpaceSettings).mockReturnValue(true);
const component = getComponent();
expect(shouldShowSpaceSettings).toHaveBeenCalledWith(defaultProps.space);
expect(findByTestId(component, 'settings-option').length).toBeTruthy();
});
it('opens space settings when space settings option is clicked', () => {
mocked(shouldShowSpaceSettings).mockReturnValue(true);
const onFinished = jest.fn();
const component = getComponent({ onFinished });
act(() => {
findByTestId(component, 'settings-option').at(0).simulate('click');
});
expect(showSpaceSettings).toHaveBeenCalledWith(defaultProps.space);
expect(onFinished).toHaveBeenCalled();
});
it('renders leave option when user does not have rights to see space settings', () => {
const component = getComponent();
expect(findByTestId(component, 'leave-option').length).toBeTruthy();
});
it('leaves space when leave option is clicked', () => {
const onFinished = jest.fn();
const component = getComponent({ onFinished });
act(() => {
findByTestId(component, 'leave-option').at(0).simulate('click');
});
expect(leaveSpace).toHaveBeenCalledWith(defaultProps.space);
expect(onFinished).toHaveBeenCalled();
});
describe('add children section', () => {
const space = makeMockSpace();
beforeEach(() => {
// set space to allow adding children to space
mocked(space.currentState.maySendStateEvent).mockReturnValue(true);
mocked(shouldShowComponent).mockReturnValue(true);
});
it('does not render section when user does not have permission to add children', () => {
mocked(space.currentState.maySendStateEvent).mockReturnValue(false);
const component = getComponent({ space });
expect(findByTestId(component, 'add-to-space-header').length).toBeFalsy();
expect(findByTestId(component, 'new-room-option').length).toBeFalsy();
expect(findByTestId(component, 'new-subspace-option').length).toBeFalsy();
});
it('does not render section when UIComponent customisations disable room and space creation', () => {
mocked(shouldShowComponent).mockReturnValue(false);
const component = getComponent({ space });
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.CreateRooms);
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.CreateSpaces);
expect(findByTestId(component, 'add-to-space-header').length).toBeFalsy();
expect(findByTestId(component, 'new-room-option').length).toBeFalsy();
expect(findByTestId(component, 'new-subspace-option').length).toBeFalsy();
});
it('renders section with add room button when UIComponent customisation allows CreateRoom', () => {
// only allow CreateRoom
mocked(shouldShowComponent).mockImplementation(feature => feature === UIComponent.CreateRooms);
const component = getComponent({ space });
expect(findByTestId(component, 'add-to-space-header').length).toBeTruthy();
expect(findByTestId(component, 'new-room-option').length).toBeTruthy();
expect(findByTestId(component, 'new-subspace-option').length).toBeFalsy();
});
it('renders section with add space button when UIComponent customisation allows CreateSpace', () => {
// only allow CreateSpaces
mocked(shouldShowComponent).mockImplementation(feature => feature === UIComponent.CreateSpaces);
const component = getComponent({ space });
expect(findByTestId(component, 'add-to-space-header').length).toBeTruthy();
expect(findByTestId(component, 'new-room-option').length).toBeFalsy();
expect(findByTestId(component, 'new-subspace-option').length).toBeTruthy();
});
it('opens create room dialog on add room button click', () => {
const onFinished = jest.fn();
const component = getComponent({ space, onFinished });
act(() => {
findByTestId(component, 'new-room-option').at(0).simulate('click');
});
expect(showCreateNewRoom).toHaveBeenCalledWith(space);
expect(onFinished).toHaveBeenCalled();
});
it('opens create space dialog on add space button click', () => {
const onFinished = jest.fn();
const component = getComponent({ space, onFinished });
act(() => {
findByTestId(component, 'new-subspace-option').at(0).simulate('click');
});
expect(showCreateNewSubspace).toHaveBeenCalledWith(space);
expect(onFinished).toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,500 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SpaceContextMenu /> renders menu correctly 1`] = `
<SpaceContextMenu
onFinished={[MockFunction]}
space={
Object {
"canInvite": [MockFunction] {
"calls": Array [
Array [
"@test:server",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
},
"client": Object {
"getUserId": [MockFunction] {
"calls": Array [
Array [],
],
"results": Array [
Object {
"type": "return",
"value": "@test:server",
},
],
},
},
"currentState": Object {
"maySendStateEvent": [MockFunction] {
"calls": Array [
Array [
"m.space.child",
"@test:server",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
},
},
"getJoinRule": [MockFunction] {
"calls": Array [
Array [],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
},
"getMyMembership": [MockFunction],
"name": "test space",
}
}
>
<IconizedContextMenu
className="mx_SpacePanel_contextMenu"
compact={true}
onFinished={[MockFunction]}
>
<ContextMenu
chevronFace="none"
hasBackground={true}
managed={true}
onFinished={[MockFunction]}
>
<Portal
containerInfo={
<div
id="mx_ContextualMenu_Container"
>
<div
class="mx_ContextualMenu_wrapper"
>
<div
class="mx_ContextualMenu_background"
/>
<div
class="mx_ContextualMenu"
role="menu"
>
<div
class="mx_IconizedContextMenu mx_SpacePanel_contextMenu mx_IconizedContextMenu_compact"
>
<div
class="mx_SpacePanel_contextMenu_header"
>
test space
</div>
<div
class="mx_IconizedContextMenu_optionList"
>
<div
aria-label="Space home"
class="mx_AccessibleButton mx_IconizedContextMenu_item focus-visible"
data-focus-visible-added=""
role="menuitem"
tabindex="0"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconHome"
/>
<span
class="mx_IconizedContextMenu_label"
>
Space home
</span>
</div>
<div
aria-label="Explore rooms"
class="mx_AccessibleButton mx_IconizedContextMenu_item"
role="menuitem"
tabindex="-1"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconExplore"
/>
<span
class="mx_IconizedContextMenu_label"
>
Explore rooms
</span>
</div>
<div
aria-label="Preferences"
class="mx_AccessibleButton mx_IconizedContextMenu_item"
role="menuitem"
tabindex="-1"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconPreferences"
/>
<span
class="mx_IconizedContextMenu_label"
>
Preferences
</span>
</div>
<div
aria-label="Leave space"
class="mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
data-test-id="leave-option"
role="menuitem"
tabindex="-1"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconLeave"
/>
<span
class="mx_IconizedContextMenu_label"
>
Leave space
</span>
</div>
</div>
</div>
</div>
</div>
</div>
}
>
<RovingTabIndexProvider
handleHomeEnd={true}
handleUpDown={true}
onKeyDown={[Function]}
>
<div
className="mx_ContextualMenu_wrapper"
onClick={[Function]}
onContextMenu={[Function]}
onKeyDown={[Function]}
style={
Object {
"bottom": undefined,
"right": undefined,
}
}
>
<div
className="mx_ContextualMenu_background"
onClick={[Function]}
onContextMenu={[Function]}
style={Object {}}
/>
<div
className="mx_ContextualMenu"
role="menu"
style={Object {}}
>
<div
className="mx_IconizedContextMenu mx_SpacePanel_contextMenu mx_IconizedContextMenu_compact"
>
<div
className="mx_SpacePanel_contextMenu_header"
>
test space
</div>
<IconizedContextMenuOptionList
first={true}
>
<div
className="mx_IconizedContextMenu_optionList"
>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconHome"
label="Space home"
onClick={[Function]}
>
<MenuItem
className="mx_IconizedContextMenu_item"
label="Space home"
onClick={[Function]}
>
<RovingAccessibleButton
aria-label="Space home"
className="mx_IconizedContextMenu_item"
onClick={[Function]}
role="menuitem"
>
<AccessibleButton
aria-label="Space home"
className="mx_IconizedContextMenu_item"
element="div"
inputRef={
Object {
"current": <div
aria-label="Space home"
class="mx_AccessibleButton mx_IconizedContextMenu_item focus-visible"
data-focus-visible-added=""
role="menuitem"
tabindex="0"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconHome"
/>
<span
class="mx_IconizedContextMenu_label"
>
Space home
</span>
</div>,
}
}
onClick={[Function]}
onFocus={[Function]}
role="menuitem"
tabIndex={0}
>
<div
aria-label="Space home"
className="mx_AccessibleButton mx_IconizedContextMenu_item"
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="menuitem"
tabIndex={0}
>
<span
className="mx_IconizedContextMenu_icon mx_SpacePanel_iconHome"
/>
<span
className="mx_IconizedContextMenu_label"
>
Space home
</span>
</div>
</AccessibleButton>
</RovingAccessibleButton>
</MenuItem>
</IconizedContextMenuOption>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconExplore"
label="Explore rooms"
onClick={[Function]}
>
<MenuItem
className="mx_IconizedContextMenu_item"
label="Explore rooms"
onClick={[Function]}
>
<RovingAccessibleButton
aria-label="Explore rooms"
className="mx_IconizedContextMenu_item"
onClick={[Function]}
role="menuitem"
>
<AccessibleButton
aria-label="Explore rooms"
className="mx_IconizedContextMenu_item"
element="div"
inputRef={
Object {
"current": <div
aria-label="Explore rooms"
class="mx_AccessibleButton mx_IconizedContextMenu_item"
role="menuitem"
tabindex="-1"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconExplore"
/>
<span
class="mx_IconizedContextMenu_label"
>
Explore rooms
</span>
</div>,
}
}
onClick={[Function]}
onFocus={[Function]}
role="menuitem"
tabIndex={-1}
>
<div
aria-label="Explore rooms"
className="mx_AccessibleButton mx_IconizedContextMenu_item"
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="menuitem"
tabIndex={-1}
>
<span
className="mx_IconizedContextMenu_icon mx_SpacePanel_iconExplore"
/>
<span
className="mx_IconizedContextMenu_label"
>
Explore rooms
</span>
</div>
</AccessibleButton>
</RovingAccessibleButton>
</MenuItem>
</IconizedContextMenuOption>
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPreferences"
label="Preferences"
onClick={[Function]}
>
<MenuItem
className="mx_IconizedContextMenu_item"
label="Preferences"
onClick={[Function]}
>
<RovingAccessibleButton
aria-label="Preferences"
className="mx_IconizedContextMenu_item"
onClick={[Function]}
role="menuitem"
>
<AccessibleButton
aria-label="Preferences"
className="mx_IconizedContextMenu_item"
element="div"
inputRef={
Object {
"current": <div
aria-label="Preferences"
class="mx_AccessibleButton mx_IconizedContextMenu_item"
role="menuitem"
tabindex="-1"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconPreferences"
/>
<span
class="mx_IconizedContextMenu_label"
>
Preferences
</span>
</div>,
}
}
onClick={[Function]}
onFocus={[Function]}
role="menuitem"
tabIndex={-1}
>
<div
aria-label="Preferences"
className="mx_AccessibleButton mx_IconizedContextMenu_item"
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="menuitem"
tabIndex={-1}
>
<span
className="mx_IconizedContextMenu_icon mx_SpacePanel_iconPreferences"
/>
<span
className="mx_IconizedContextMenu_label"
>
Preferences
</span>
</div>
</AccessibleButton>
</RovingAccessibleButton>
</MenuItem>
</IconizedContextMenuOption>
<IconizedContextMenuOption
className="mx_IconizedContextMenu_option_red"
data-test-id="leave-option"
iconClassName="mx_SpacePanel_iconLeave"
label="Leave space"
onClick={[Function]}
>
<MenuItem
className="mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
data-test-id="leave-option"
label="Leave space"
onClick={[Function]}
>
<RovingAccessibleButton
aria-label="Leave space"
className="mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
data-test-id="leave-option"
onClick={[Function]}
role="menuitem"
>
<AccessibleButton
aria-label="Leave space"
className="mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
data-test-id="leave-option"
element="div"
inputRef={
Object {
"current": <div
aria-label="Leave space"
class="mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
data-test-id="leave-option"
role="menuitem"
tabindex="-1"
>
<span
class="mx_IconizedContextMenu_icon mx_SpacePanel_iconLeave"
/>
<span
class="mx_IconizedContextMenu_label"
>
Leave space
</span>
</div>,
}
}
onClick={[Function]}
onFocus={[Function]}
role="menuitem"
tabIndex={-1}
>
<div
aria-label="Leave space"
className="mx_AccessibleButton mx_IconizedContextMenu_option_red mx_IconizedContextMenu_item"
data-test-id="leave-option"
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="menuitem"
tabIndex={-1}
>
<span
className="mx_IconizedContextMenu_icon mx_SpacePanel_iconLeave"
/>
<span
className="mx_IconizedContextMenu_label"
>
Leave space
</span>
</div>
</AccessibleButton>
</RovingAccessibleButton>
</MenuItem>
</IconizedContextMenuOption>
</div>
</IconizedContextMenuOptionList>
</div>
</div>
</div>
</RovingTabIndexProvider>
</Portal>
</ContextMenu>
</IconizedContextMenu>
</SpaceContextMenu>
`;

View File

@ -23,22 +23,6 @@ import _ThemeChoicePanel from '../../../../src/components/views/settings/ThemeCh
const ThemeChoicePanel = TestUtils.wrapInMatrixClientContext(_ThemeChoicePanel);
// Avoid errors about global.matchMedia. See:
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // Deprecated
removeListener: jest.fn(), // Deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
// Fake random strings to give a predictable snapshot
jest.mock(
'matrix-js-sdk/src/randomstring',

View File

@ -0,0 +1,92 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { mount } from 'enzyme';
import { mocked } from 'jest-mock';
import { MatrixClient } from 'matrix-js-sdk';
import { act } from "react-dom/test-utils";
import '../../../skinned-sdk';
import SpacePanel from '../../../../src/components/views/spaces/SpacePanel';
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
import { SpaceKey } from '../../../../src/stores/spaces';
import { findByTestId } from '../../../utils/test-utils';
import { shouldShowComponent } from '../../../../src/customisations/helpers/UIComponents';
import { UIComponent } from '../../../../src/settings/UIFeature';
jest.mock('../../../../src/stores/spaces/SpaceStore', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const EventEmitter = require("events");
class MockSpaceStore extends EventEmitter {
invitedSpaces = [];
enabledMetaSpaces = [];
spacePanelSpaces = [];
activeSpace: SpaceKey = '!space1';
}
return {
instance: new MockSpaceStore(),
};
});
jest.mock('../../../../src/customisations/helpers/UIComponents', () => ({
shouldShowComponent: jest.fn(),
}));
describe('<SpacePanel />', () => {
const defaultProps = {};
const getComponent = (props = {}) =>
mount(<SpacePanel {...defaultProps} {...props} />);
const mockClient = {
getUserId: jest.fn().mockReturnValue('@test:test'),
isGuest: jest.fn(),
getAccountData: jest.fn(),
} as unknown as MatrixClient;
beforeAll(() => {
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient);
});
beforeEach(() => {
mocked(shouldShowComponent).mockClear().mockReturnValue(true);
});
describe('create new space button', () => {
it('renders create space button when UIComponent.CreateSpaces component should be shown', () => {
const component = getComponent();
expect(findByTestId(component, 'create-space-button').length).toBeTruthy();
});
it('does not render create space button when UIComponent.CreateSpaces component should not be shown', () => {
mocked(shouldShowComponent).mockReturnValue(false);
const component = getComponent();
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.CreateSpaces);
expect(findByTestId(component, 'create-space-button').length).toBeFalsy();
});
it('opens context menu on create space button click', async () => {
const component = getComponent();
await act(async () => {
findByTestId(component, 'create-space-button').at(0).simulate('click');
component.setProps({});
});
expect(component.find('SpaceCreateMenu').length).toBeTruthy();
});
});
});

View File

@ -22,3 +22,16 @@ configure({ adapter: new Adapter() });
// maplibre requires a createObjectURL mock
global.URL.createObjectURL = jest.fn();
// matchMedia is not included in jsdom
const mockMatchMedia = jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // Deprecated
removeListener: jest.fn(), // Deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
}));
global.matchMedia = mockMatchMedia;

View File

@ -18,6 +18,7 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { EventEmitter } from "events";
import { ReactWrapper } from "enzyme";
import { AsyncStoreWithClient } from "../../src/stores/AsyncStoreWithClient";
import { mkEvent, mkStubRoom } from "../test-utils";
@ -82,3 +83,5 @@ export const mkSpace = (
};
export const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise(r => e.once(k, r));
export const findByTestId = (component: ReactWrapper, id: string) => component.find(`[data-test-id="${id}"]`);