Remove access to global state from some files (#26752)

* admin_console/license_settings/trial_banner

* invitation_modal and associated utils

* overlay trigger

* Change TrialBanner to not use makeGetCategory

* Address feedback

* Fixing unit tests D:

* Address further feedback

* Fix one last test
This commit is contained in:
Harrison Healey 2024-04-22 14:53:42 -04:00 committed by GitHub
parent 7429ddaf04
commit 22d72b6df8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 715 additions and 3619 deletions

View File

@ -161,7 +161,6 @@
"jest-cli": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-junit": "16.0.0",
"jest-styled-components": "7.2.0",
"jest-watch-typeahead": "2.2.2",
"mmjstool": "github:mattermost/mattermost-utilities#73e61d2ede0ebf802492df4cfbac481d35efed54",
"nock": "13.2.8",

View File

@ -6,14 +6,10 @@ import type {ReactNode} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import type {PreferenceType} from '@mattermost/types/preferences';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences';
import {getBool as getBoolPreference} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import store from 'stores/redux_store';
import AlertBanner from 'components/alert_banner';
import withOpenStartTrialFormModal from 'components/common/hocs/cloud/with_open_start_trial_form_modal';
import type {TelemetryProps} from 'components/common/hooks/useOpenPricingModal';
@ -93,14 +89,9 @@ const TrialBanner = ({
let gettingTrialErrorMsg;
const {formatMessage} = useIntl();
const state = store.getState();
const getCategory = makeGetCategory();
const preferences = getCategory(state, Preferences.UNIQUE);
const restartedAfterUpgradePrefValue = preferences.find((pref: PreferenceType) => pref.name === Unique.REQUEST_TRIAL_AFTER_SERVER_UPGRADE);
const clickedUpgradeAndStartTrialBtn = preferences.find((pref: PreferenceType) => pref.name === Unique.CLICKED_UPGRADE_AND_TRIAL_BTN);
const restartedAfterUpgradePrefs = restartedAfterUpgradePrefValue?.value === 'true';
const clickedUpgradeAndTrialBtn = clickedUpgradeAndStartTrialBtn?.value === 'true';
const restartedAfterUpgradePrefs = useSelector<GlobalState>((state) => getBoolPreference(state, Preferences.UNIQUE, Unique.REQUEST_TRIAL_AFTER_SERVER_UPGRADE));
const clickedUpgradeAndTrialBtn = useSelector<GlobalState>((state) => getBoolPreference(state, Preferences.UNIQUE, Unique.CLICKED_UPGRADE_AND_TRIAL_BTN));
const userId = useSelector((state: GlobalState) => getCurrentUserId(state));

View File

@ -1,335 +1,79 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/app_bar/app_bar should match snapshot on mount 1`] = `
<AppBar>
<DocumentFragment>
<div
className="app-bar"
class="app-bar"
>
<div
className="app-bar__top"
class="app-bar__top"
>
<AppBarPluginComponent
component={
Object {
"action": [MockFunction],
"icon": "fallback_component",
"id": "the_component_id",
"pluginId": "playbooks",
"tooltipText": "Playbooks Tooltip",
}
}
key="the_component_id"
<div
class="app-bar__icon"
id="app-bar-icon-playbooks"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
bsClass="tooltip"
id="pluginTooltip-app-bar-icon-playbooks"
placement="right"
>
<span>
Playbooks Tooltip
</span>
</Tooltip>
}
placement="left"
trigger={
Array [
"hover",
"focus",
]
}
<div
class="app-bar__old-icon app-bar__icon-inner app-bar__icon-inner--centered"
role="button"
tabindex="0"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<OverlayWrapper
bsClass="tooltip"
id="pluginTooltip-app-bar-icon-playbooks"
intl={null}
placement="right"
>
<span>
Playbooks Tooltip
</span>
</OverlayWrapper>
}
placement="left"
trigger={
Array [
"hover",
"focus",
]
}
>
<div
className="app-bar__icon"
id="app-bar-icon-playbooks"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<div
className="app-bar__old-icon app-bar__icon-inner app-bar__icon-inner--centered"
role="button"
tabIndex={0}
>
fallback_component
</div>
</div>
</OverlayTrigger>
</OverlayTrigger>
</AppBarPluginComponent>
fallback_component
</div>
</div>
<hr
className="app-bar__divider"
key="divider"
class="app-bar__divider"
/>
<AppBarBinding
binding={
Object {
"app_id": "com.mattermost.zendesk",
"label": "Create Subscription",
}
}
key="com.mattermost.zendesk_Create Subscription"
<div
aria-label="Create Subscription"
class="app-bar__icon"
id="app-bar-icon-com.mattermost.zendesk"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
bsClass="tooltip"
id="tooltip-app-bar-icon-com.mattermost.zendesk"
placement="right"
>
<span>
Create Subscription
</span>
</Tooltip>
}
placement="left"
trigger={
Array [
"hover",
"focus",
]
}
<div
class="app-bar__icon-inner"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<OverlayWrapper
bsClass="tooltip"
id="tooltip-app-bar-icon-com.mattermost.zendesk"
intl={null}
placement="right"
>
<span>
Create Subscription
</span>
</OverlayWrapper>
}
placement="left"
trigger={
Array [
"hover",
"focus",
]
}
>
<div
aria-label="Create Subscription"
className="app-bar__icon"
id="app-bar-icon-com.mattermost.zendesk"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<div
className="app-bar__icon-inner"
>
<img />
</div>
</div>
</OverlayTrigger>
</OverlayTrigger>
</AppBarBinding>
<img />
</div>
</div>
</div>
</div>
</AppBar>
</DocumentFragment>
`;
exports[`components/app_bar/app_bar should match snapshot on mount when App Bar is disabled 1`] = `
<AppBar>
<DocumentFragment>
<div
className="app-bar"
class="app-bar"
>
<div
className="app-bar__top"
class="app-bar__top"
>
<AppBarPluginComponent
component={
Object {
"action": [MockFunction],
"icon": "fallback_component",
"id": "the_component_id",
"pluginId": "playbooks",
"tooltipText": "Playbooks Tooltip",
}
}
key="the_component_id"
<div
class="app-bar__icon"
id="app-bar-icon-playbooks"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
bsClass="tooltip"
id="pluginTooltip-app-bar-icon-playbooks"
placement="right"
>
<span>
Playbooks Tooltip
</span>
</Tooltip>
}
placement="left"
trigger={
Array [
"hover",
"focus",
]
}
<div
class="app-bar__old-icon app-bar__icon-inner app-bar__icon-inner--centered"
role="button"
tabindex="0"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<OverlayWrapper
bsClass="tooltip"
id="pluginTooltip-app-bar-icon-playbooks"
intl={null}
placement="right"
>
<span>
Playbooks Tooltip
</span>
</OverlayWrapper>
}
placement="left"
trigger={
Array [
"hover",
"focus",
]
}
>
<div
className="app-bar__icon"
id="app-bar-icon-playbooks"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<div
className="app-bar__old-icon app-bar__icon-inner app-bar__icon-inner--centered"
role="button"
tabIndex={0}
>
fallback_component
</div>
</div>
</OverlayTrigger>
</OverlayTrigger>
</AppBarPluginComponent>
fallback_component
</div>
</div>
<hr
className="app-bar__divider"
key="divider"
class="app-bar__divider"
/>
<AppBarBinding
binding={
Object {
"app_id": "com.mattermost.zendesk",
"label": "Create Subscription",
}
}
key="com.mattermost.zendesk_Create Subscription"
<div
aria-label="Create Subscription"
class="app-bar__icon"
id="app-bar-icon-com.mattermost.zendesk"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
bsClass="tooltip"
id="tooltip-app-bar-icon-com.mattermost.zendesk"
placement="right"
>
<span>
Create Subscription
</span>
</Tooltip>
}
placement="left"
trigger={
Array [
"hover",
"focus",
]
}
<div
class="app-bar__icon-inner"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<OverlayWrapper
bsClass="tooltip"
id="tooltip-app-bar-icon-com.mattermost.zendesk"
intl={null}
placement="right"
>
<span>
Create Subscription
</span>
</OverlayWrapper>
}
placement="left"
trigger={
Array [
"hover",
"focus",
]
}
>
<div
aria-label="Create Subscription"
className="app-bar__icon"
id="app-bar-icon-com.mattermost.zendesk"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<div
className="app-bar__icon-inner"
>
<img />
</div>
</div>
</OverlayTrigger>
</OverlayTrigger>
</AppBarBinding>
<img />
</div>
</div>
</div>
</div>
</AppBar>
</DocumentFragment>
`;

View File

@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {mount, shallow} from 'enzyme';
import React from 'react';
import type {AppBinding} from '@mattermost/types/apps';
@ -9,100 +8,15 @@ import type {AppBinding} from '@mattermost/types/apps';
import {Permissions} from 'mattermost-redux/constants';
import {AppBindingLocations} from 'mattermost-redux/constants/apps';
import type {GlobalState} from 'types/store';
import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import type {PluginComponent} from 'types/store/plugins';
import AppBar from './app_bar';
import 'jest-styled-components';
const mockDispatch = jest.fn();
let mockState: GlobalState;
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux') as typeof import('react-redux'),
useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState),
useDispatch: () => mockDispatch,
}));
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom') as typeof import('react-router-dom'),
useLocation: () => {
return {
pathname: '',
};
},
}));
describe('components/app_bar/app_bar', () => {
beforeEach(() => {
mockState = {
views: {
rhs: {
isSidebarOpen: true,
rhsState: 'plugin',
pluggableId: 'the_rhs_plugin_component',
},
},
plugins: {
components: {
AppBar: channelHeaderComponents,
RightHandSidebarComponent: rhsComponents,
Product: [],
} as {[componentName: string]: PluginComponent[]},
},
entities: {
apps: {
main: {
bindings: channelHeaderAppBindings,
} as {bindings: AppBinding[]},
pluginEnabled: true,
},
general: {
config: {
DisableAppBar: 'false',
FeatureFlagAppsEnabled: 'true',
} as any,
},
channels: {
currentChannelId: 'currentchannel',
channels: {
currentchannel: {
id: 'currentchannel',
},
} as any,
myMembers: {
currentchannel: {
id: 'memberid',
},
} as any,
},
teams: {
currentTeamId: 'currentteam',
},
preferences: {
myPreferences: {
},
} as any,
users: {
currentUserId: 'user1',
profiles: {
user1: {
roles: 'system_user',
},
},
} as any,
roles: {
roles: {
system_user: {
permissions: [],
},
},
} as any,
},
} as GlobalState;
});
const channelHeaderComponents: PluginComponent[] = [
{
id: 'the_component_id',
@ -134,65 +48,141 @@ describe('components/app_bar/app_bar', () => {
},
] as AppBinding[];
test('should match snapshot on mount', async () => {
const wrapper = mount(
<AppBar/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot on mount when App Bar is disabled', async () => {
mockState.entities.general.config.DisableAppBar = 'false';
const wrapper = mount(
<AppBar/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should not show marketplace if disabled or user does not have SYSCONSOLE_WRITE_PLUGINS permission', async () => {
mockState.entities.general = {
config: {
DisableAppBar: 'true',
FeatureFlagAppsEnabled: 'true',
EnableMarketplace: 'true',
PluginsEnabled: 'true',
const initialState = {
views: {
rhs: {
isSidebarOpen: true,
rhsState: 'plugin',
pluggableId: 'the_rhs_plugin_component',
},
} as any;
const wrapper = shallow(
<AppBar/>,
);
expect(wrapper.find('AppBarMarketplace').exists()).toEqual(false);
});
test('should show marketplace if enabled and user has SYSCONSOLE_WRITE_PLUGINS permission', async () => {
mockState.entities.general = {
config: {
DisableAppBar: 'false',
FeatureFlagAppsEnabled: 'true',
EnableMarketplace: 'true',
PluginsEnabled: 'true',
},
plugins: {
components: {
AppBar: channelHeaderComponents,
RightHandSidebarComponent: rhsComponents,
Product: [],
} as {[componentName: string]: PluginComponent[]},
},
entities: {
apps: {
main: {
bindings: channelHeaderAppBindings,
} as {bindings: AppBinding[]},
pluginEnabled: true,
},
} as any;
mockState.entities.roles = {
roles: {
system_user: {
permissions: [
Permissions.SYSCONSOLE_WRITE_PLUGINS,
],
general: {
config: {
DisableAppBar: 'false',
FeatureFlagAppsEnabled: 'true',
},
},
} as any;
channels: {
currentChannelId: 'currentchannel',
channels: {
currentchannel: TestHelper.getChannelMock({
id: 'currentchannel',
}),
},
myMembers: {
currentchannel: TestHelper.getChannelMembershipMock({
channel_id: 'currentchannel',
user_id: 'user1',
}),
},
},
teams: {
currentTeamId: 'currentteam',
},
users: {
currentUserId: 'user1',
profiles: {
user1: TestHelper.getUserMock({
roles: 'system_user',
}),
},
},
},
};
const wrapper = shallow(
test('should match snapshot on mount', () => {
const testState = initialState;
const {asFragment} = renderWithContext(
<AppBar/>,
testState,
);
expect(wrapper.find('AppBarMarketplace').exists()).toEqual(true);
expect(asFragment()).toMatchSnapshot();
});
test('should match snapshot on mount when App Bar is disabled', () => {
const testState = mergeObjects(initialState, {
entities: {
general: {
config: {
DisableAppbar: 'false',
},
},
},
});
const {asFragment} = renderWithContext(
<AppBar/>,
testState,
);
expect(asFragment()).toMatchSnapshot();
});
test('should not show marketplace if disabled or user does not have SYSCONSOLE_WRITE_PLUGINS permission', () => {
const testState = mergeObjects(initialState, {
entities: {
general: {
config: {
DisableAppBar: 'true',
FeatureFlagAppsEnabled: 'true',
EnableMarketplace: 'true',
PluginsEnabled: 'true',
},
},
},
});
renderWithContext(
<AppBar/>,
testState,
);
expect(screen.queryByLabelText('App Marketplace')).not.toBeInTheDocument();
});
test('should show marketplace if enabled and user has SYSCONSOLE_WRITE_PLUGINS permission', () => {
const testState = mergeObjects(initialState, {
entities: {
general: {
config: {
DisableAppBar: 'false',
FeatureFlagAppsEnabled: 'true',
EnableMarketplace: 'true',
PluginsEnabled: 'true',
},
},
roles: {
roles: {
system_user: {
permissions: [
Permissions.SYSCONSOLE_WRITE_PLUGINS,
],
},
},
},
},
});
renderWithContext(
<AppBar/>,
testState,
);
expect(screen.queryByLabelText('App Marketplace')).toBeInTheDocument();
});
});

View File

@ -4,7 +4,7 @@
import classNames from 'classnames';
import React, {useRef, useState} from 'react';
import {Tooltip} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import {FormattedMessage, useIntl} from 'react-intl';
import OverlayTrigger from 'components/overlay_trigger';
@ -21,6 +21,8 @@ type Props = {
};
const CopyButton: React.FC<Props> = (props: Props) => {
const intl = useIntl();
const [isCopied, setIsCopied] = useState(false);
const timerRef = useRef<NodeJS.Timeout | null>(null);
@ -74,16 +76,16 @@ const CopyButton: React.FC<Props> = (props: Props) => {
<span
className={spanClassName}
onClick={copyText}
aria-label={intl.formatMessage({id: getId(), defaultMessage: getDefaultMessage()})}
role='button'
>
{!isCopied &&
<i
role='button'
className='icon icon-content-copy'
/>
}
{isCopied &&
<i
role='button'
className='icon icon-check'
/>
}

View File

@ -3,14 +3,10 @@
import {shallow} from 'enzyme';
import React from 'react';
import type {Button} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
import {Provider} from 'react-redux';
import TeamUrl from 'components/create_team/components/team_url/team_url';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import Constants from 'utils/constants';
jest.mock('images/logo.png', () => 'logo.png');
@ -30,31 +26,20 @@ describe('/components/create_team/components/display_name', () => {
history: {push: jest.fn()},
};
const chatLengthError = (
<FormattedMessage
id='create_team.team_url.charLength'
defaultMessage='Name must be {min} or more characters up to a maximum of {max}'
values={{
min: Constants.MIN_TEAMNAME_LENGTH,
max: Constants.MAX_TEAMNAME_LENGTH,
}}
/>
);
test('should match snapshot', () => {
const wrapper = shallow(<TeamUrl {...defaultProps}/>);
expect(wrapper).toMatchSnapshot();
});
test('should return to display_name.jsx page', () => {
const wrapper = mountWithIntl(<TeamUrl {...defaultProps}/>);
test('should return to display_name.jsx page', async () => {
renderWithContext(<TeamUrl {...defaultProps}/>);
wrapper.find('a').simulate('click', {
preventDefault: () => jest.fn(),
screen.getByText('Back to previous step').click();
expect(defaultProps.updateParent).toHaveBeenCalledWith({
...defaultProps.state,
wizard: 'display_name',
});
expect(wrapper.prop('state').wizard).toBe('display_name');
expect(wrapper.prop('updateParent')).toHaveBeenCalled();
});
test('should successfully submit', async () => {
@ -65,81 +50,80 @@ describe('/components/create_team/components/display_name', () => {
const actions = {...defaultProps.actions, checkIfTeamExists};
const props = {...defaultProps, actions};
const wrapper = mountWithIntl(
renderWithContext(
<TeamUrl {...props}/>,
);
await (wrapper.instance() as unknown as TeamUrl).submitNext({preventDefault: jest.fn()} as unknown as React.MouseEvent<Button, MouseEvent>);
screen.getByText('Finish').click();
await waitFor(() => {
expect(screen.getByText('This URL is taken or unavailable. Please try another.')).toBeInTheDocument();
});
expect(actions.checkIfTeamExists).toHaveBeenCalledTimes(1);
expect(actions.createTeam).not.toHaveBeenCalled();
await (wrapper.instance() as unknown as TeamUrl).submitNext({preventDefault: jest.fn()} as unknown as React.MouseEvent<Button, MouseEvent>);
expect(actions.checkIfTeamExists).toHaveBeenCalledTimes(2);
expect(actions.createTeam).toHaveBeenCalledTimes(1);
expect(actions.createTeam).toBeCalledWith({display_name: 'test-team', name: 'test-team', type: 'O'});
expect(props.history.push).toHaveBeenCalledTimes(1);
expect(props.history.push).toBeCalledWith('/test-team/channels/town-square');
screen.getByText('Finish').click();
await waitFor(() => {
expect(actions.checkIfTeamExists).toHaveBeenCalledTimes(2);
expect(actions.createTeam).toHaveBeenCalledTimes(1);
expect(actions.createTeam).toBeCalledWith({display_name: 'test-team', name: 'test-team', type: 'O'});
expect(props.history.push).toHaveBeenCalledTimes(1);
expect(props.history.push).toBeCalledWith('/test-team/channels/town-square');
});
});
test('should display isRequired error', () => {
const wrapper = mountWithIntl(<TeamUrl {...defaultProps}/>);
(wrapper.find('.form-control').instance() as unknown as HTMLInputElement).value = '';
wrapper.find('.form-control').simulate('change');
wrapper.find('button').simulate('click', {preventDefault: () => jest.fn()});
expect(wrapper.state('nameError')).toEqual(
<FormattedMessage
id='create_team.team_url.required'
defaultMessage='This field is required'
/>,
renderWithContext(
<TeamUrl {...defaultProps}/>,
);
userEvent.clear(screen.getByRole('textbox'));
screen.getByText('Finish').click();
expect(screen.getByText('This field is required')).toBeInTheDocument();
});
test('should display charLength error', () => {
const wrapper = mountWithIntl(<TeamUrl {...defaultProps}/>);
(wrapper.find('.form-control').instance() as unknown as HTMLInputElement).value = 'a';
wrapper.find('.form-control').simulate('change');
wrapper.find('button').simulate('click', {preventDefault: () => jest.fn()});
expect(wrapper.state('nameError')).toEqual(chatLengthError);
const lengthError = `Name must be ${Constants.MIN_TEAMNAME_LENGTH} or more characters up to a maximum of ${Constants.MAX_TEAMNAME_LENGTH}`;
(wrapper.find('.form-control').instance() as unknown as HTMLInputElement).value = 'a'.repeat(Constants.MAX_TEAMNAME_LENGTH + 1);
wrapper.find('.form-control').simulate('change');
wrapper.find('button').simulate('click', {preventDefault: () => jest.fn()});
expect(wrapper.state('nameError')).toEqual(chatLengthError);
renderWithContext(
<TeamUrl {...defaultProps}/>,
);
expect(screen.queryByText(lengthError)).not.toBeInTheDocument();
userEvent.type(screen.getByRole('textbox'), 'a');
screen.getByText('Finish').click();
expect(screen.getByText(lengthError)).toBeInTheDocument();
userEvent.type(screen.getByRole('textbox'), 'a'.repeat(Constants.MAX_TEAMNAME_LENGTH + 1));
screen.getByText('Finish').click();
expect(screen.getByText(lengthError)).toBeInTheDocument();
});
test('should display teamUrl regex error', () => {
const wrapper = mountWithIntl(<TeamUrl {...defaultProps}/>);
(wrapper.find('.form-control').instance() as unknown as HTMLInputElement).value = '!!wrongName1';
wrapper.find('.form-control').simulate('change');
wrapper.find('button').simulate('click', {preventDefault: () => jest.fn()});
expect(wrapper.state('nameError')).toEqual(
<FormattedMessage
id='create_team.team_url.regex'
defaultMessage="Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash."
/>,
renderWithContext(
<TeamUrl {...defaultProps}/>,
);
userEvent.type(screen.getByRole('textbox'), '!!wrongName1');
screen.getByText('Finish').click();
expect(screen.getByText("Use only lower case letters, numbers and dashes. Must start with a letter and can't end in a dash.")).toBeInTheDocument();
});
test('should display teamUrl taken error', () => {
const store = mockStore({
entities: {
general: {
config: {},
license: {
Cloud: 'false',
},
},
users: {
currentUserId: 'currentUserId',
},
},
});
renderWithContext(
<TeamUrl {...defaultProps}/>,
);
const wrapper = mountWithIntl(<Provider store={store}><TeamUrl {...defaultProps}/></Provider>);
(wrapper.find('.form-control').instance() as unknown as HTMLInputElement).value = 'channel';
wrapper.find('.form-control').simulate('change');
wrapper.find('button').simulate('click', {preventDefault: () => jest.fn()});
expect((wrapper as any).find(TeamUrl).state('nameError').props.id).toEqual('create_team.team_url.taken');
userEvent.type(screen.getByRole('textbox'), 'channel');
screen.getByText('Finish').click();
expect(screen.getByText('Please try another.', {exact: false})).toBeInTheDocument();
});
});

View File

@ -2,11 +2,10 @@
// See LICENSE.txt for license information.
import React from 'react';
import {IntlProvider} from 'react-intl';
import type {SystemEmoji} from '@mattermost/types/emojis';
import {render, screen} from 'tests/react_testing_utils';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import EmojiMap from 'utils/emoji_map';
import EmojiPicker from './emoji_picker';
@ -19,11 +18,6 @@ jest.mock('components/emoji_picker/components/emoji_picker_preview', () => ({emo
));
describe('components/emoji_picker/EmojiPicker', () => {
const intlProviderProps = {
defaultLocale: 'en',
locale: 'en',
};
const baseProps = {
filter: '',
visible: true,
@ -45,20 +39,16 @@ describe('components/emoji_picker/EmojiPicker', () => {
};
test('should match snapshot', () => {
const {asFragment} = render(
<IntlProvider {...intlProviderProps}>
<EmojiPicker {...baseProps}/>
</IntlProvider>,
const {asFragment} = renderWithContext(
<EmojiPicker {...baseProps}/>,
);
expect(asFragment()).toMatchSnapshot();
});
test('Recent category should not exist if there are no recent emojis', () => {
render(
<IntlProvider {...intlProviderProps}>
<EmojiPicker {...baseProps}/>
</IntlProvider>,
renderWithContext(
<EmojiPicker {...baseProps}/>,
);
expect(screen.queryByLabelText('emoji_picker.recent')).toBeNull();
@ -70,10 +60,8 @@ describe('components/emoji_picker/EmojiPicker', () => {
recentEmojis: ['smile'],
};
render(
<IntlProvider {...intlProviderProps}>
<EmojiPicker {...props}/>
</IntlProvider>,
renderWithContext(
<EmojiPicker {...props}/>,
);
expect(screen.queryByLabelText('emoji_picker.recent')).not.toBeNull();
@ -85,10 +73,8 @@ describe('components/emoji_picker/EmojiPicker', () => {
filter: 'wave',
};
render(
<IntlProvider {...intlProviderProps}>
<EmojiPicker {...props}/>
</IntlProvider>,
renderWithContext(
<EmojiPicker {...props}/>,
);
expect(screen.queryByText('Preview for wave emoji')).not.toBeNull();

View File

@ -22,7 +22,6 @@ exports[`components/file_attachment/FilenameOverlay should match snapshot, compa
<a
className="post-image__name"
href="#"
id="file-attachment-link"
onClick={[MockFunction]}
rel="noopener noreferrer"
>

View File

@ -7,7 +7,6 @@ import React from 'react';
import type {GlobalState} from '@mattermost/types/store';
import type {DeepPartial} from '@mattermost/types/utilities';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import FileAttachment from './file_attachment';
@ -168,15 +167,12 @@ describe('FileAttachment', () => {
test('should blur file attachment link after click', () => {
const props = {...baseProps, compactDisplay: true};
const wrapper = mountWithIntl(<FileAttachment {...props}/>);
const e = {
preventDefault: jest.fn(),
target: {blur: jest.fn()},
};
renderWithContext(<FileAttachment {...props}/>);
const a = wrapper.find('#file-attachment-link');
a.simulate('click', e);
expect(e.target.blur).toHaveBeenCalled();
const link = screen.getByText(baseProps.fileInfo.name);
const blur = jest.spyOn(link, 'blur');
screen.getByText(baseProps.fileInfo.name).click();
expect(blur).toHaveBeenCalled();
});
describe('archived file', () => {

View File

@ -71,7 +71,6 @@ export default class FilenameOverlay extends React.PureComponent<Props> {
overlay={<Tooltip id='file-name__tooltip'>{fileName}</Tooltip>}
>
<a
id='file-attachment-link'
href='#'
onClick={handleImageClick}
className='post-image__name'

View File

@ -1,337 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/file_preview_modal/file_preview_modal_main_actions/FilePreviewModalMainActions should match snapshot for external image with public links enabled 1`] = `
<div
className="file-preview-modal-main-actions__actions"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
key="download"
overlay={
<Tooltip
id="download-icon-tooltip"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Download"
id="view_image_popover.download"
/>
</Tooltip>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<ExternalLink
className="file-preview-modal-main-actions__action-item"
download="img.png"
href="http://example.com/img.png"
location="file_preview_modal_main_actions"
>
<i
className="icon icon-download-outline"
/>
</ExternalLink>
</OverlayTrigger>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
key="publicLink"
overlay={
<Tooltip
id="close-icon-tooltip"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Close"
id="full_screen_modal.close"
/>
</Tooltip>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<button
className="file-preview-modal-main-actions__action-item"
onClick={[MockFunction]}
>
<i
className="icon icon-close"
/>
</button>
</OverlayTrigger>
</div>
`;
exports[`components/file_preview_modal/file_preview_modal_main_actions/FilePreviewModalMainActions should match snapshot when copy content is enabled 1`] = `
<div
className="file-preview-modal-main-actions__actions"
>
<CopyButton
afterCopyText="Copied"
className="file-preview-modal-main-actions__action-item"
content="test content"
placement="bottom"
/>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
key="download"
overlay={
<Tooltip
id="download-icon-tooltip"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Download"
id="view_image_popover.download"
/>
</Tooltip>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<ExternalLink
className="file-preview-modal-main-actions__action-item"
download="img.png"
href="http://example.com/img.png"
location="file_preview_modal_main_actions"
>
<i
className="icon icon-download-outline"
/>
</ExternalLink>
</OverlayTrigger>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
key="publicLink"
overlay={
<Tooltip
id="close-icon-tooltip"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Close"
id="full_screen_modal.close"
/>
</Tooltip>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<button
className="file-preview-modal-main-actions__action-item"
onClick={[MockFunction]}
>
<i
className="icon icon-close"
/>
</button>
</OverlayTrigger>
</div>
`;
exports[`components/file_preview_modal/file_preview_modal_main_actions/FilePreviewModalMainActions should match snapshot with public links disabled 1`] = `
<div
className="file-preview-modal-main-actions__actions"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
key="download"
overlay={
<Tooltip
id="download-icon-tooltip"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Download"
id="view_image_popover.download"
/>
</Tooltip>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<ExternalLink
className="file-preview-modal-main-actions__action-item"
download="img.png"
href="http://example.com/img.png"
location="file_preview_modal_main_actions"
>
<i
className="icon icon-download-outline"
/>
</ExternalLink>
</OverlayTrigger>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
key="publicLink"
overlay={
<Tooltip
id="close-icon-tooltip"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Close"
id="full_screen_modal.close"
/>
</Tooltip>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<button
className="file-preview-modal-main-actions__action-item"
onClick={[MockFunction]}
>
<i
className="icon icon-close"
/>
</button>
</OverlayTrigger>
</div>
`;
exports[`components/file_preview_modal/file_preview_modal_main_actions/FilePreviewModalMainActions should match snapshot with public links enabled 1`] = `
<div
className="file-preview-modal-main-actions__actions"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
key="filePreviewPublicLink"
onExit={[Function]}
overlay={
<Tooltip
id="link-variant-icon-tooltip"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Get a public link"
id="view_image_popover.publicLink"
/>
</Tooltip>
}
placement="bottom"
shouldUpdatePosition={true}
trigger={
Array [
"hover",
"focus",
]
}
>
<a
className="file-preview-modal-main-actions__action-item"
href="#"
onClick={[Function]}
>
<i
className="icon icon-link-variant"
/>
</a>
</OverlayTrigger>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
key="download"
overlay={
<Tooltip
id="download-icon-tooltip"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Download"
id="view_image_popover.download"
/>
</Tooltip>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<ExternalLink
className="file-preview-modal-main-actions__action-item"
download="img.png"
href="http://example.com/img.png"
location="file_preview_modal_main_actions"
>
<i
className="icon icon-download-outline"
/>
</ExternalLink>
</OverlayTrigger>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
key="publicLink"
overlay={
<Tooltip
id="close-icon-tooltip"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Close"
id="full_screen_modal.close"
/>
</Tooltip>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<button
className="file-preview-modal-main-actions__action-item"
onClick={[MockFunction]}
>
<i
className="icon icon-close"
/>
</button>
</OverlayTrigger>
</div>
`;
exports[`components/file_preview_modal/file_preview_modal_main_actions/FilePreviewModalMainActions should match snapshot with public links enabled 2`] = `
<a
className="file-preview-modal-main-actions__action-item"
href="#"
onClick={[Function]}
>
<i
className="icon icon-link-variant"
/>
</a>
`;

View File

@ -1,30 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {mount, shallow} from 'enzyme';
import React from 'react';
import type {ComponentProps} from 'react';
import * as fileActions from 'mattermost-redux/actions/files';
import OverlayTrigger from 'components/overlay_trigger';
import Tooltip from 'components/tooltip';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import * as Utils from 'utils/utils';
import type {GlobalState} from 'types/store';
import FilePreviewModalMainActions from './file_preview_modal_main_actions';
const mockDispatch = jest.fn();
let mockState: GlobalState;
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux') as typeof import('react-redux'),
useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState),
useDispatch: () => mockDispatch,
}));
describe('components/file_preview_modal/file_preview_modal_main_actions/FilePreviewModalMainActions', () => {
let defaultProps: ComponentProps<typeof FilePreviewModalMainActions>;
beforeEach(() => {
@ -39,22 +26,6 @@ describe('components/file_preview_modal/file_preview_modal_main_actions/FilePrev
content: 'test content',
canCopyContent: false,
};
mockState = {
entities: {
general: {config: {}},
users: {profiles: {}},
channels: {channels: {}},
preferences: {
myPreferences: {
},
},
files: {
filePublicLink: {link: 'http://example.com/img.png'},
},
},
} as GlobalState;
});
test('should match snapshot with public links disabled', () => {
@ -63,8 +34,11 @@ describe('components/file_preview_modal/file_preview_modal_main_actions/FilePrev
enablePublicLink: false,
};
const wrapper = shallow(<FilePreviewModalMainActions {...props}/>);
expect(wrapper).toMatchSnapshot();
renderWithContext(
<FilePreviewModalMainActions {...props}/>,
);
expect(screen.queryByLabelText('Get a public link')).not.toBeInTheDocument();
});
test('should match snapshot with public links enabled', () => {
@ -73,32 +47,38 @@ describe('components/file_preview_modal/file_preview_modal_main_actions/FilePrev
enablePublicLink: true,
};
const wrapper = shallow(<FilePreviewModalMainActions {...props}/>);
expect(wrapper).toMatchSnapshot();
const overlayWrapper = wrapper.find(OverlayTrigger).first();
expect(overlayWrapper.prop('overlay').type).toEqual(Tooltip);
expect(overlayWrapper.prop('children')).toMatchSnapshot();
renderWithContext(
<FilePreviewModalMainActions {...props}/>,
);
expect(screen.queryByLabelText('Get a public link')).toBeInTheDocument();
});
test('should match snapshot for external image with public links enabled', () => {
test('should not show public link button for external image with public links enabled', () => {
const props = {
...defaultProps,
enablePublicLink: true,
showPublicLink: false,
};
const wrapper = shallow(<FilePreviewModalMainActions {...props}/>);
expect(wrapper).toMatchSnapshot();
renderWithContext(
<FilePreviewModalMainActions {...props}/>,
);
expect(screen.queryByLabelText('Get a public link')).not.toBeInTheDocument();
});
test('should match snapshot when copy content is enabled', () => {
test('should show copy button when copy content is enabled', () => {
const props = {
...defaultProps,
canCopyContent: true,
};
const wrapper = shallow(<FilePreviewModalMainActions {...props}/>);
expect(wrapper).toMatchSnapshot();
renderWithContext(
<FilePreviewModalMainActions {...props}/>,
);
expect(screen.getByLabelText('Copy code')).toBeInTheDocument();
});
test('should call public link callback', () => {
@ -107,17 +87,22 @@ describe('components/file_preview_modal/file_preview_modal_main_actions/FilePrev
...defaultProps,
enablePublicLink: true,
};
const wrapper = shallow(<FilePreviewModalMainActions {...props}/>);
expect(wrapper.find(OverlayTrigger)).toHaveLength(3);
const overlayWrapper = wrapper.find(OverlayTrigger).first().children('a');
renderWithContext(
<FilePreviewModalMainActions {...props}/>,
);
expect(spy).toHaveBeenCalledTimes(0);
overlayWrapper.simulate('click');
screen.getByLabelText('Get a public link').click();
expect(spy).toHaveBeenCalledTimes(1);
});
test('should not get public api when public links is disabled', async () => {
const spy = jest.spyOn(fileActions, 'getFilePublicLink');
mount(<FilePreviewModalMainActions {...defaultProps}/>);
renderWithContext(
<FilePreviewModalMainActions {...defaultProps}/>,
);
expect(spy).toHaveBeenCalledTimes(0);
});
@ -127,7 +112,9 @@ describe('components/file_preview_modal/file_preview_modal_main_actions/FilePrev
...defaultProps,
enablePublicLink: true,
};
mount(<FilePreviewModalMainActions {...props}/>);
renderWithContext(
<FilePreviewModalMainActions {...props}/>,
);
expect(spy).toHaveBeenCalledTimes(1);
});
@ -137,9 +124,11 @@ describe('components/file_preview_modal/file_preview_modal_main_actions/FilePrev
...defaultProps,
canCopyContent: true,
};
const wrapper = mount(<FilePreviewModalMainActions {...props}/>);
renderWithContext(
<FilePreviewModalMainActions {...props}/>,
);
expect(spy).toHaveBeenCalledTimes(0);
wrapper.find('.icon-content-copy').simulate('click');
screen.getByLabelText('Copy code').click();
expect(spy).toHaveBeenCalledTimes(1);
});
});

View File

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {memo, useEffect, useState} from 'react';
import {FormattedMessage} from 'react-intl';
import {useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import type {FileInfo} from '@mattermost/types/files';
@ -25,10 +25,6 @@ import type {LinkInfo} from '../types';
import './file_preview_modal_main_actions.scss';
interface DownloadLinkProps {
download?: string;
}
interface Props {
usedInside?: 'Header' | 'Footer';
showOnlyClose?: boolean;
@ -45,6 +41,8 @@ interface Props {
}
const FilePreviewModalMainActions: React.FC<Props> = (props: Props) => {
const intl = useIntl();
const tooltipPlacement = props.usedInside === 'Header' ? 'bottom' : 'top';
const selectedFilePublicLink = useSelector((state: GlobalState) => selectFilePublicLink(state)?.link);
const dispatch = useDispatch();
@ -60,6 +58,10 @@ const FilePreviewModalMainActions: React.FC<Props> = (props: Props) => {
setPublicLinkCopied(true);
};
const closeMessage = intl.formatMessage({
id: 'full_screen_modal.close',
defaultMessage: 'Close',
});
const closeButton = (
<OverlayTrigger
delayShow={Constants.OVERLAY_TIME_DELAY}
@ -67,34 +69,31 @@ const FilePreviewModalMainActions: React.FC<Props> = (props: Props) => {
placement={tooltipPlacement}
overlay={
<Tooltip id='close-icon-tooltip'>
<FormattedMessage
id='full_screen_modal.close'
defaultMessage='Close'
/>
{closeMessage}
</Tooltip>
}
>
<button
className='file-preview-modal-main-actions__action-item'
onClick={props.handleModalClose}
aria-label={closeMessage}
>
<i className='icon icon-close'/>
</button>
</OverlayTrigger>
);
let publicTooltipMessage = (
<FormattedMessage
id='view_image_popover.publicLink'
defaultMessage='Get a public link'
/>
);
let publicTooltipMessage;
if (publicLinkCopied) {
publicTooltipMessage = (
<FormattedMessage
id='file_preview_modal_main_actions.public_link-copied'
defaultMessage='Public link copied'
/>
);
publicTooltipMessage = intl.formatMessage({
id: 'file_preview_modal_main_actions.public_link-copied',
defaultMessage: 'Public link copied',
});
} else {
publicTooltipMessage = intl.formatMessage({
id: 'view_image_popover.publicLink',
defaultMessage: 'Get a public link',
});
}
const publicLink = (
<OverlayTrigger
@ -113,13 +112,17 @@ const FilePreviewModalMainActions: React.FC<Props> = (props: Props) => {
href='#'
className='file-preview-modal-main-actions__action-item'
onClick={copyPublicLink}
aria-label={publicTooltipMessage}
>
<i className='icon icon-link-variant'/>
</a>
</OverlayTrigger>
);
const downloadLinkProps: DownloadLinkProps = {};
downloadLinkProps.download = props.filename;
const downloadMessage = intl.formatMessage({
id: 'view_image_popover.download',
defaultMessage: 'Download',
});
const download = (
<OverlayTrigger
delayShow={Constants.OVERLAY_TIME_DELAY}
@ -127,10 +130,7 @@ const FilePreviewModalMainActions: React.FC<Props> = (props: Props) => {
placement={tooltipPlacement}
overlay={
<Tooltip id='download-icon-tooltip'>
<FormattedMessage
id='view_image_popover.download'
defaultMessage='Download'
/>
{downloadMessage}
</Tooltip>
}
>
@ -139,6 +139,7 @@ const FilePreviewModalMainActions: React.FC<Props> = (props: Props) => {
className='file-preview-modal-main-actions__action-item'
location='file_preview_modal_main_actions'
download={props.filename}
aria-label={downloadMessage}
>
<i className='icon icon-download-outline'/>
</ExternalLink>

View File

@ -28,6 +28,7 @@ import {
import {makeAsyncComponent} from 'components/async_load';
import {Constants} from 'utils/constants';
import {getRoleForTrackFlow} from 'utils/utils';
import type {GlobalState} from 'types/store';
@ -86,6 +87,7 @@ export function mapStateToProps(state: GlobalState, props: OwnProps) {
isAdmin: isAdmin(getCurrentUser(state).roles),
currentChannel,
townSquareDisplayName,
roleForTrackFlow: getRoleForTrackFlow(state),
};
}

View File

@ -7,11 +7,11 @@ import {Provider} from 'react-redux';
import type {Team} from '@mattermost/types/teams';
import {General} from 'mattermost-redux/constants';
import deepFreeze from 'mattermost-redux/utils/deep_freeze';
import store from 'stores/redux_store';
import {mountWithThemedIntl} from 'tests/helpers/themed-intl-test-helper';
import mockStore from 'tests/test_store';
import {SelfHostedProducts} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import {generateId} from 'utils/utils';
@ -47,6 +47,7 @@ const defaultProps: Props = deepFreeze({
intl: {} as IntlShape,
townSquareDisplayName: '',
onExited: jest.fn(),
roleForTrackFlow: {started_by_role: General.SYSTEM_USER_ROLE},
});
let props = defaultProps;
@ -107,7 +108,7 @@ describe('InvitationModal', () => {
},
};
store.getState = () => (state);
const store = mockStore(state);
beforeEach(() => {
props = defaultProps;

View File

@ -17,8 +17,6 @@ import {isEmail} from 'mattermost-redux/utils/helpers';
import {trackEvent} from 'actions/telemetry_actions';
import {getRoleForTrackFlow} from 'utils/utils';
import {InviteType} from './invite_as';
import InviteView, {initializeInviteState} from './invite_view';
import type {InviteState} from './invite_view';
@ -73,6 +71,7 @@ export type Props = {
channelToInvite?: Channel;
initialValue?: string;
inviteAsGuest?: boolean;
roleForTrackFlow: {started_by_role: string};
}
export const View = {
@ -162,12 +161,11 @@ export class InvitationModal extends React.PureComponent<Props, State> {
if (!this.props.currentTeam) {
return;
}
const roleForTrackFlow = getRoleForTrackFlow();
const inviteAs = this.state.invite.inviteType;
if (inviteAs === InviteType.MEMBER && this.props.isCloud) {
trackEvent('cloud_invite_users', 'click_send_invitations', {num_invitations: this.state.invite.usersEmails.length, ...roleForTrackFlow});
trackEvent('cloud_invite_users', 'click_send_invitations', {num_invitations: this.state.invite.usersEmails.length, ...this.props.roleForTrackFlow});
}
trackEvent('invite_users', 'click_invite', roleForTrackFlow);
trackEvent('invite_users', 'click_invite', this.props.roleForTrackFlow);
const users: UserProfile[] = [];
const emails: string[] = [];

View File

@ -9,9 +9,8 @@ import type {Team} from '@mattermost/types/teams';
import deepFreeze from 'mattermost-redux/utils/deep_freeze';
import store from 'stores/redux_store';
import {mountWithThemedIntl} from 'tests/helpers/themed-intl-test-helper';
import mockStore from 'tests/test_store';
import {SelfHostedProducts} from 'utils/constants';
import {TestHelper as TH} from 'utils/test_helper';
import {generateId} from 'utils/utils';
@ -120,7 +119,7 @@ describe('InviteView', () => {
},
};
store.getState = () => (state);
const store = mockStore(state);
beforeEach(() => {
props = defaultProps;

View File

@ -5,6 +5,7 @@ import classNames from 'classnames';
import React, {useEffect, useMemo} from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage, useIntl} from 'react-intl';
import {useSelector} from 'react-redux';
import type {Channel} from '@mattermost/types/channels';
import type {Team} from '@mattermost/types/teams';
@ -76,6 +77,9 @@ export type Props = InviteState & {
}
export default function InviteView(props: Props) {
const trackFlowRole = useSelector(getTrackFlowRole);
const roleForTrackFlow = useSelector(getRoleForTrackFlow);
useEffect(() => {
if (!props.currentTeam.invite_id) {
props.regenerateTeamInviteId(props.currentTeam.id);
@ -85,11 +89,11 @@ export default function InviteView(props: Props) {
const {formatMessage} = useIntl();
const inviteURL = useMemo(() => {
return `${getSiteURL()}/signup_user_complete/?id=${props.currentTeam.invite_id}&md=link&sbr=${getTrackFlowRole()}`;
}, [props.currentTeam.invite_id]);
return `${getSiteURL()}/signup_user_complete/?id=${props.currentTeam.invite_id}&md=link&sbr=${trackFlowRole}`;
}, [props.currentTeam.invite_id, trackFlowRole]);
const copyText = useCopyText({
trackCallback: () => trackEvent(getAnalyticsCategory(props.isAdmin), 'click_copy_invite_link', {...getRoleForTrackFlow(), ...getSourceForTrackFlow()}),
trackCallback: () => trackEvent(getAnalyticsCategory(props.isAdmin), 'click_copy_invite_link', {...roleForTrackFlow, ...getSourceForTrackFlow()}),
text: inviteURL,
});

View File

@ -4,11 +4,12 @@
import React from 'react';
import {act} from 'react-dom/test-utils';
import type {DeepPartial} from '@mattermost/types/utilities';
import {createChannel} from 'mattermost-redux/actions/channels';
import Permissions from 'mattermost-redux/constants/permissions';
import {
render,
renderWithContext,
screen,
userEvent,
@ -23,97 +24,89 @@ import NewChannelModal from './new_channel_modal';
jest.mock('mattermost-redux/actions/channels');
const mockDispatch = jest.fn();
let mockState: GlobalState;
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux') as typeof import('react-redux'),
useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState),
useDispatch: () => mockDispatch,
}));
describe('components/new_channel_modal', () => {
beforeEach(() => {
mockState = {
entities: {
general: {
config: {},
},
const initialState: DeepPartial<GlobalState> = {
entities: {
general: {
config: {},
},
channels: {
currentChannelId: 'current_channel_id',
channels: {
currentChannelId: 'current_channel_id',
channels: {
current_channel_id: {
id: 'current_channel_id',
display_name: 'Current channel',
name: 'current_channel',
},
},
roles: {
current_channel_id: [
'channel_user',
'channel_admin',
],
},
},
teams: {
currentTeamId: 'current_team_id',
myMembers: {
current_team_id: {
roles: 'team_user team_admin',
},
},
teams: {
current_team_id: {
id: 'current_team_id',
description: 'Curent team description',
name: 'current-team',
},
},
},
preferences: {
myPreferences: {},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {roles: 'system_admin system_user'},
current_channel_id: {
id: 'current_channel_id',
display_name: 'Current channel',
name: 'current_channel',
},
},
roles: {
roles: {
channel_admin: {
permissions: [],
},
channel_user: {
permissions: [],
},
team_admin: {
permissions: [],
},
team_user: {
permissions: [
Permissions.CREATE_PRIVATE_CHANNEL,
],
},
system_admin: {
permissions: [
Permissions.CREATE_PUBLIC_CHANNEL,
],
},
system_user: {
permissions: [],
},
current_channel_id: new Set([
'channel_user',
'channel_admin',
]),
},
},
teams: {
currentTeamId: 'current_team_id',
myMembers: {
current_team_id: {
roles: 'team_user team_admin',
},
},
teams: {
current_team_id: {
id: 'current_team_id',
description: 'Curent team description',
name: 'current-team',
},
},
},
plugins: {
plugins: {focalboard: {id: suitePluginIds.focalboard}},
preferences: {
myPreferences: {},
},
} as unknown as GlobalState;
});
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {roles: 'system_admin system_user'},
},
},
roles: {
roles: {
channel_admin: {
permissions: [],
},
channel_user: {
permissions: [],
},
team_admin: {
permissions: [],
},
team_user: {
permissions: [
Permissions.CREATE_PRIVATE_CHANNEL,
],
},
system_admin: {
permissions: [
Permissions.CREATE_PUBLIC_CHANNEL,
],
},
system_user: {
permissions: [],
},
},
},
},
plugins: {
plugins: {focalboard: {id: suitePluginIds.focalboard}},
},
};
test('should match component state with given props', () => {
render(<NewChannelModal/>);
renderWithContext(
<NewChannelModal/>,
initialState,
);
const heading = screen.getByRole('heading');
expect(heading).toBeInTheDocument();
@ -172,8 +165,9 @@ describe('components/new_channel_modal', () => {
test('should handle display name change', () => {
const value = 'Channel name';
render(
renderWithContext(
<NewChannelModal/>,
initialState,
);
// Change display name
@ -196,8 +190,9 @@ describe('components/new_channel_modal', () => {
const url = 'channel-name-new';
render(
renderWithContext(
<NewChannelModal/>,
initialState,
);
// Change display name
@ -235,8 +230,9 @@ describe('components/new_channel_modal', () => {
});
test('should handle type changes', () => {
render(
renderWithContext(
<NewChannelModal/>,
initialState,
);
// Change type to private
@ -261,8 +257,9 @@ describe('components/new_channel_modal', () => {
test('should handle purpose changes', () => {
const value = 'Purpose';
render(
renderWithContext(
<NewChannelModal/>,
initialState,
);
// Change purpose
@ -277,8 +274,9 @@ describe('components/new_channel_modal', () => {
});
test('should enable confirm button when having valid display name, url and type', () => {
render(
renderWithContext(
<NewChannelModal/>,
initialState,
);
// Confirm button should be disabled
@ -304,8 +302,9 @@ describe('components/new_channel_modal', () => {
});
test('should disable confirm button when display name in error', () => {
render(
renderWithContext(
<NewChannelModal/>,
initialState,
);
// Change display name
@ -333,8 +332,9 @@ describe('components/new_channel_modal', () => {
});
test('should disable confirm button when url in error', () => {
render(
renderWithContext(
<NewChannelModal/>,
initialState,
);
// Change display name
@ -369,8 +369,9 @@ describe('components/new_channel_modal', () => {
});
test('should disable confirm button when server error', async () => {
render(
renderWithContext(
<NewChannelModal/>,
initialState,
);
// Confirm button should be disabled
@ -406,6 +407,7 @@ describe('components/new_channel_modal', () => {
renderWithContext(
<NewChannelModal/>,
initialState,
);
// Confirm button should be disabled

View File

@ -5,7 +5,10 @@ import {mount} from 'enzyme';
import React from 'react';
import {OverlayTrigger as BaseOverlayTrigger} from 'react-bootstrap'; // eslint-disable-line no-restricted-imports
import {FormattedMessage, IntlProvider} from 'react-intl';
import {Provider as ReduxProvider} from 'react-redux';
import type {Store} from 'redux';
import testConfigureStore from 'packages/mattermost-redux/test/test_store';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import OverlayTrigger from './overlay_trigger';
@ -13,6 +16,8 @@ import OverlayTrigger from './overlay_trigger';
describe('OverlayTrigger', () => {
const testId = 'test.value';
let store: Store;
const intlProviderProps = {
defaultLocale: 'en',
locale: 'en',
@ -33,6 +38,7 @@ describe('OverlayTrigger', () => {
let originalConsoleError: () => void;
beforeEach(() => {
store = testConfigureStore();
originalConsoleError = console.error;
console.error = jest.fn();
});
@ -43,11 +49,13 @@ describe('OverlayTrigger', () => {
test('base OverlayTrigger should fail to pass intl to overlay', () => {
const wrapper = mount(
<IntlProvider {...intlProviderProps}>
<BaseOverlayTrigger {...baseProps}>
<span/>
</BaseOverlayTrigger>
</IntlProvider>,
<ReduxProvider store={store}>
<IntlProvider {...intlProviderProps}>
<BaseOverlayTrigger {...baseProps}>
<span/>
</BaseOverlayTrigger>
</IntlProvider>
</ReduxProvider>,
);
// console.error will have been called by FormattedMessage because its intl context is missing
@ -58,11 +66,13 @@ describe('OverlayTrigger', () => {
test('custom OverlayTrigger should pass intl to overlay', () => {
const wrapper = mount(
<IntlProvider {...intlProviderProps}>
<OverlayTrigger {...baseProps}>
<span/>
</OverlayTrigger>
</IntlProvider>,
<ReduxProvider store={store}>
<IntlProvider {...intlProviderProps}>
<OverlayTrigger {...baseProps}>
<span/>
</OverlayTrigger>
</IntlProvider>
</ReduxProvider>,
);
const overlay = mount(wrapper.find(BaseOverlayTrigger).prop('overlay'));
@ -79,11 +89,13 @@ describe('OverlayTrigger', () => {
};
const wrapper = mountWithIntl(
<IntlProvider {...intlProviderProps}>
<OverlayTrigger {...props}>
<span/>
</OverlayTrigger>
</IntlProvider>,
<ReduxProvider store={store}>
<IntlProvider {...intlProviderProps}>
<OverlayTrigger {...props}>
<span/>
</OverlayTrigger>
</IntlProvider>
</ReduxProvider>,
);
expect(ref.current).toBe(wrapper.find(BaseOverlayTrigger).instance());
@ -104,11 +116,13 @@ describe('OverlayTrigger', () => {
};
const wrapper = mount(
<IntlProvider {...intlProviderProps}>
<OverlayTrigger {...props}>
<span/>
</OverlayTrigger>
</IntlProvider>,
<ReduxProvider store={store}>
<IntlProvider {...intlProviderProps}>
<OverlayTrigger {...props}>
<span/>
</OverlayTrigger>
</IntlProvider>
</ReduxProvider>,
);
// Dive into the react-bootstrap internals to find our overlay
@ -143,11 +157,13 @@ describe('OverlayTrigger', () => {
};
const wrapper = mount(
<IntlProvider {...intlProviderProps}>
<OverlayTrigger {...props}>
<span/>
</OverlayTrigger>
</IntlProvider>,
<ReduxProvider store={store}>
<IntlProvider {...intlProviderProps}>
<OverlayTrigger {...props}>
<span/>
</OverlayTrigger>
</IntlProvider>
</ReduxProvider>,
);
// Dive into the react-bootstrap internals to find our overlay

View File

@ -6,9 +6,7 @@ import {OverlayTrigger as OriginalOverlayTrigger} from 'react-bootstrap'; // esl
import type {OverlayTriggerProps} from 'react-bootstrap';
import {IntlContext} from 'react-intl';
import type {IntlShape} from 'react-intl';
import {Provider} from 'react-redux';
import store from 'stores/redux_store';
import {Provider, useStore} from 'react-redux';
export type BaseOverlayTrigger = OriginalOverlayTrigger & {
hide: () => void;
@ -25,6 +23,8 @@ type Props = OverlayTriggerProps & {
const OverlayTrigger = React.forwardRef((props: Props, ref?: React.Ref<OriginalOverlayTrigger>) => {
const {overlay, disabled, ...otherProps} = props;
const store = useStore();
// The overlay is rendered outside of the regular React context, and our version react-bootstrap can't forward
// that context itself, so we have to manually forward the react-intl context to this component's child.
const OverlayWrapper = ({intl, ...overlayProps}: {intl: IntlShape}) => (

View File

@ -3,7 +3,7 @@
import React from 'react';
import {render, screen} from 'tests/react_testing_utils';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import PostEmoji from './post_emoji';
@ -14,14 +14,14 @@ describe('PostEmoji', () => {
};
test('should render image when imageUrl is provided', () => {
render(<PostEmoji {...baseProps}/>);
renderWithContext(<PostEmoji {...baseProps}/>);
expect(screen.queryByTestId('postEmoji.:' + baseProps.name + ':')).toBeInTheDocument();
expect(screen.queryByTestId('postEmoji.:' + baseProps.name + ':')).toHaveStyle(`backgroundImage: url(${baseProps.imageUrl})}`);
});
test('should render shortcode text within span when imageUrl is provided', () => {
render(<PostEmoji {...baseProps}/>);
renderWithContext(<PostEmoji {...baseProps}/>);
expect(screen.queryByTestId('postEmoji.:' + baseProps.name + ':')).toHaveTextContent(`:${baseProps.name}:`);
});
@ -32,7 +32,7 @@ describe('PostEmoji', () => {
imageUrl: '',
};
render(<PostEmoji {...props}/>);
renderWithContext(<PostEmoji {...props}/>);
expect(screen.queryByTestId('postEmoji.:' + baseProps.name + ':')).not.toBeInTheDocument();
expect(screen.getByText(`:${props.name}:`)).toBeInTheDocument();

View File

@ -4,7 +4,7 @@
import React from 'react';
import type {ComponentProps} from 'react';
import {render, screen} from 'tests/react_testing_utils';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import PostProfilePicture from './post_profile_picture';
@ -31,7 +31,7 @@ describe('components/PostProfilePicture', () => {
test('no status and post icon override specified, default props', () => {
const props: Props = baseProps;
render(
renderWithContext(
<PostProfilePicture {...props}/>,
);
@ -47,7 +47,7 @@ describe('components/PostProfilePicture', () => {
status: 'away',
postIconOverrideURL: 'http://example.com/image.png',
};
render(
renderWithContext(
<PostProfilePicture {...props}/>,
);

View File

@ -1,194 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/QuickInput should render clear button with customized tooltip component 1`] = `
<div
className="input-clear visible"
onMouseDown={[Function]}
onTouchEnd={[Function]}
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
id="InputClearTooltip"
>
<span>
Custom
</span>
</Tooltip>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<OverlayWrapper
id="InputClearTooltip"
intl={null}
>
<span>
Custom
</span>
</OverlayWrapper>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<span
aria-hidden="true"
className="input-clear-x"
onBlur={[Function]}
onClick={null}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<i
className="icon icon-close-circle"
/>
</span>
</OverlayTrigger>
</OverlayTrigger>
</div>
`;
exports[`components/QuickInput should render clear button with customized tooltip text 1`] = `
<div
className="input-clear visible"
onMouseDown={[Function]}
onTouchEnd={[Function]}
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
id="InputClearTooltip"
>
Custom
</Tooltip>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<OverlayWrapper
id="InputClearTooltip"
intl={null}
>
Custom
</OverlayWrapper>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<span
aria-hidden="true"
className="input-clear-x"
onBlur={[Function]}
onClick={null}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<i
className="icon icon-close-circle"
/>
</span>
</OverlayTrigger>
</OverlayTrigger>
</div>
`;
exports[`components/QuickInput should render clear button with default tooltip text 1`] = `
<div
className="input-clear visible"
onMouseDown={[Function]}
onTouchEnd={[Function]}
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
id="InputClearTooltip"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Clear"
id="input.clear"
/>
</Tooltip>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<OverlayWrapper
id="InputClearTooltip"
intl={null}
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Clear"
id="input.clear"
/>
</OverlayWrapper>
}
placement="bottom"
trigger={
Array [
"hover",
"focus",
]
}
>
<span
aria-hidden="true"
className="input-clear-x"
onBlur={[Function]}
onClick={null}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<i
className="icon icon-close-circle"
/>
</span>
</OverlayTrigger>
</OverlayTrigger>
</div>
`;

View File

@ -1,9 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {mount} from 'enzyme';
import React from 'react';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import {QuickInput} from './quick_input';
describe('components/QuickInput', () => {
@ -14,16 +15,16 @@ describe('components/QuickInput', () => {
['when value undefined', {clearable: true, onClear: () => {}}],
['when value empty', {value: '', clearable: true, onClear: () => {}}],
])('should not render clear button', (description, props) => {
const wrapper = mount(
renderWithContext(
<QuickInput {...props}/>,
);
expect(wrapper.find('.input-clear').exists()).toBe(false);
expect(screen.queryByTestId('input-clear')).not.toBeInTheDocument();
});
describe('should render clear button', () => {
test('with default tooltip text', () => {
const wrapper = mount(
renderWithContext(
<QuickInput
value='mock'
clearable={true}
@ -31,11 +32,11 @@ describe('components/QuickInput', () => {
/>,
);
expect(wrapper.find('.input-clear')).toMatchSnapshot();
expect(screen.queryByTestId('input-clear')).toBeInTheDocument();
});
test('with customized tooltip text', () => {
const wrapper = mount(
renderWithContext(
<QuickInput
value='mock'
clearable={true}
@ -44,11 +45,11 @@ describe('components/QuickInput', () => {
/>,
);
expect(wrapper.find('.input-clear')).toMatchSnapshot();
expect(screen.queryByTestId('input-clear')).toBeInTheDocument();
});
test('with customized tooltip component', () => {
const wrapper = mount(
renderWithContext(
<QuickInput
value='mock'
clearable={true}
@ -59,7 +60,7 @@ describe('components/QuickInput', () => {
/>,
);
expect(wrapper.find('.input-clear')).toMatchSnapshot();
expect(screen.queryByTestId('input-clear')).toBeInTheDocument();
});
});
@ -71,7 +72,7 @@ describe('components/QuickInput', () => {
return <div/>;
}
}
const wrapper = mount(
const {rerender} = renderWithContext(
<QuickInput
value='mock'
clearable={true}
@ -80,11 +81,20 @@ describe('components/QuickInput', () => {
/>,
);
wrapper.setProps({onClear: () => wrapper.setProps({value: ''})});
expect(wrapper.find('.input-clear').exists()).toBe(true);
expect(screen.queryByTestId('input-clear')).toBeInTheDocument();
wrapper.find('.input-clear').simulate('mousedown');
expect(wrapper.find('.input-clear').exists()).toBe(false);
userEvent.click(screen.getByTestId('input-clear'));
rerender(
<QuickInput
value=''
clearable={true}
onClear={() => {}}
inputComponent={MockComp}
/>,
);
expect(screen.queryByTestId('input-clear')).not.toBeInTheDocument();
expect(focusFn).toBeCalled();
});
});

View File

@ -200,9 +200,11 @@ export class QuickInput extends React.PureComponent<Props> {
{inputElement}
{showClearButton &&
<div
data-testid='input-clear'
className={classNames(clearClassName, 'input-clear visible')}
onMouseDown={this.onClear}
onTouchEnd={this.onClear}
role='button'
>
<OverlayTrigger
delayShow={Constants.OVERLAY_TIME_DELAY}

View File

@ -45,6 +45,8 @@ exports[`components/search_bar/SearchBar should match snapshot with search 1`] =
/>
<div
class="input-clear visible"
data-testid="input-clear"
role="button"
>
<span
aria-hidden="true"
@ -115,6 +117,8 @@ exports[`components/search_bar/SearchBar should match snapshot with search, with
/>
<div
class="input-clear visible"
data-testid="input-clear"
role="button"
>
<span
aria-hidden="true"

View File

@ -1,10 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {screen} from '@testing-library/react';
import React from 'react';
import {BrowserRouter as Router} from 'react-router-dom';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext} from 'tests/react_testing_utils';
import TeamButton from './team_button';
@ -35,14 +35,12 @@ describe('components/TeamSidebar/TeamButton', () => {
unread: true,
};
const wrapper = mountWithIntl(
<Router>
<TeamButton {...props}/>
</Router>,
renderWithContext(
<TeamButton {...props}/>,
);
expect(wrapper.find('.unread-badge').exists()).toBe(true);
expect(wrapper.find('.team-container.unread').exists()).toBe(true);
expect(screen.queryByTestId('team-badge-')).toBeInTheDocument();
expect(screen.getByTestId('team-container-')).toHaveClass('unread');
});
it('should hide unread badge and set no class when unread in a product', () => {
@ -53,14 +51,12 @@ describe('components/TeamSidebar/TeamButton', () => {
isInProduct: true,
};
const wrapper = mountWithIntl(
<Router>
<TeamButton {...props}/>
</Router>,
renderWithContext(
<TeamButton {...props}/>,
);
expect(wrapper.find('.unread-badge').exists()).toBe(false);
expect(wrapper.find('.team-container.unread').exists()).toBe(false);
expect(screen.queryByTestId('team-badge-')).not.toBeInTheDocument();
expect(screen.getByTestId('team-container-')).not.toHaveClass('unread');
});
it('should show mentions badge and set class when mentions in channels', () => {
@ -71,14 +67,12 @@ describe('components/TeamSidebar/TeamButton', () => {
mentions: 1,
};
const wrapper = mountWithIntl(
<Router>
<TeamButton {...props}/>
</Router>,
renderWithContext(
<TeamButton {...props}/>,
);
expect(wrapper.find('.badge.badge-max-number').exists()).toBe(true);
expect(wrapper.find('.team-container.unread').exists()).toBe(true);
expect(screen.queryByTestId('team-badge-')).toHaveClass('badge-max-number');
expect(screen.getByTestId('team-container-')).toHaveClass('unread');
});
it('should hide mentions badge and set no class when mentions in product', () => {
@ -90,13 +84,11 @@ describe('components/TeamSidebar/TeamButton', () => {
isInProduct: true,
};
const wrapper = mountWithIntl(
<Router>
<TeamButton {...props}/>
</Router>,
renderWithContext(
<TeamButton {...props}/>,
);
expect(wrapper.find('.badge.badge-max-number').exists()).toBe(false);
expect(wrapper.find('.team-container.unread').exists()).toBe(false);
expect(screen.queryByTestId('team-badge-')).not.toBeInTheDocument();
expect(screen.getByTestId('team-container-')).not.toHaveClass('unread');
});
});

View File

@ -89,7 +89,10 @@ export default function TeamButton({
teamClass = 'unread';
badge = (
<span className={'unread-badge'}/>
<span
data-testid={'team-badge-' + teamId}
className={'unread-badge'}
/>
);
} else if (isNotCreateTeamButton) {
teamClass = '';
@ -115,7 +118,12 @@ export default function TeamButton({
});
badge = (
<span className={classNames('badge badge-max-number pull-right small', {urgent: otherProps.hasUrgent})}>{mentions > 99 ? '99+' : mentions}</span>
<span
data-testid={'team-badge-' + teamId}
className={classNames('badge badge-max-number pull-right small', {urgent: otherProps.hasUrgent})}
>
{mentions > 99 ? '99+' : mentions}
</span>
);
}
}
@ -206,6 +214,7 @@ export default function TeamButton({
</Draggable>
) : (
<div
data-testid={'team-container-' + teamId}
className={`team-container ${teamClass}`}
>
{teamButton}

View File

@ -2,10 +2,9 @@
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import type {ReactWrapper} from 'enzyme';
import React from 'react';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import * as Utils from 'utils/utils';
import Toast from './toast';
@ -45,12 +44,14 @@ describe('components/Toast', () => {
test('should dismiss', () => {
defaultProps.onDismiss = jest.fn();
const wrapper: ReactWrapper<any, any, React.Component> = mountWithIntl(<Toast {... defaultProps}><span>{'child'}</span></Toast>);
const toast = wrapper.find(Toast).instance();
renderWithContext(
<Toast {... defaultProps}>
<span>{'child'}</span>
</Toast>,
);
screen.getByTestId('dismissToast').click();
if (toast instanceof Toast) {
toast.handleDismiss();
}
expect(defaultProps.onDismiss).toHaveBeenCalledTimes(1);
});

View File

@ -8,7 +8,7 @@ import type {Channel, ChannelMembership} from '@mattermost/types/channels';
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
import ChannelHeaderPlug from 'plugins/channel_header_plug/channel_header_plug';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext} from 'tests/react_testing_utils';
import type {PluginComponent} from 'types/store/plugins';
@ -22,8 +22,8 @@ describe('plugins/ChannelHeaderPlug', () => {
tooltipText: 'some tooltip text',
} as PluginComponent;
test('should match snapshot with no extended component', () => {
const wrapper = mountWithIntl(
test('should not render anything with no extended component', () => {
const {asFragment} = renderWithContext(
<ChannelHeaderPlug
components={[]}
channel={{} as Channel}
@ -40,11 +40,11 @@ describe('plugins/ChannelHeaderPlug', () => {
shouldShowAppBar={false}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(asFragment()).toMatchSnapshot();
});
test('should match snapshot with one extended component', () => {
const wrapper = mountWithIntl(
const {asFragment} = renderWithContext(
<ChannelHeaderPlug
components={[testPlug]}
channel={{} as Channel}
@ -61,11 +61,11 @@ describe('plugins/ChannelHeaderPlug', () => {
shouldShowAppBar={false}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(asFragment()).toMatchSnapshot();
});
test('should match snapshot with six extended components', () => {
const wrapper = mountWithIntl(
const {asFragment} = renderWithContext(
<ChannelHeaderPlug
components={[
testPlug,
@ -98,11 +98,11 @@ describe('plugins/ChannelHeaderPlug', () => {
shouldShowAppBar={false}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(asFragment()).toMatchSnapshot();
});
test('should match snapshot when the App Bar is visible', () => {
const wrapper = mountWithIntl(
test('should not render anything when the App Bar is visible', () => {
const {asFragment} = renderWithContext(
<ChannelHeaderPlug
components={[
testPlug,
@ -124,6 +124,6 @@ describe('plugins/ChannelHeaderPlug', () => {
shouldShowAppBar={true}
/>,
);
expect(wrapper).toMatchSnapshot();
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -31,6 +31,7 @@ import {getPost as getPostAction} from 'mattermost-redux/actions/posts';
import {getTeamByName as getTeamByNameAction} from 'mattermost-redux/actions/teams';
import {Client4} from 'mattermost-redux/client';
import {Preferences, General} from 'mattermost-redux/constants';
import {createSelector} from 'mattermost-redux/selectors/create_selector';
import {
getChannel,
getChannelsNameMapInTeam,
@ -1630,8 +1631,7 @@ const TrackFlowRoles: Record<string, string> = {
su: General.SYSTEM_USER_ROLE,
};
export function getTrackFlowRole() {
const state = store.getState();
export function getTrackFlowRole(state: GlobalState) {
let trackFlowRole = 'su';
if (isFirstAdmin(state)) {
@ -1643,11 +1643,15 @@ export function getTrackFlowRole() {
return trackFlowRole;
}
export function getRoleForTrackFlow() {
const startedByRole = TrackFlowRoles[getTrackFlowRole()];
export const getRoleForTrackFlow = createSelector(
'getRoleForTrackFlow',
getTrackFlowRole,
(trackFlowRole) => {
const startedByRole = TrackFlowRoles[trackFlowRole];
return {started_by_role: startedByRole};
}
return {started_by_role: startedByRole};
},
);
export function getSbr() {
const params = new URLSearchParams(window.location.search);

View File

@ -210,7 +210,6 @@
"jest-cli": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-junit": "16.0.0",
"jest-styled-components": "7.2.0",
"jest-watch-typeahead": "2.2.2",
"mmjstool": "github:mattermost/mattermost-utilities#73e61d2ede0ebf802492df4cfbac481d35efed54",
"nock": "13.2.8",
@ -250,11 +249,6 @@
"node": ">=0.10.0"
}
},
"node_modules/@adobe/css-tools": {
"version": "4.3.1",
"dev": true,
"license": "MIT"
},
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
"dev": true,
@ -15904,20 +15898,6 @@
"node": ">=10"
}
},
"node_modules/jest-styled-components": {
"version": "7.2.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@adobe/css-tools": "^4.0.1"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"styled-components": ">= 5"
}
},
"node_modules/jest-util": {
"version": "28.1.3",
"dev": true,