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-cli": "29.7.0",
"jest-environment-jsdom": "29.7.0", "jest-environment-jsdom": "29.7.0",
"jest-junit": "16.0.0", "jest-junit": "16.0.0",
"jest-styled-components": "7.2.0",
"jest-watch-typeahead": "2.2.2", "jest-watch-typeahead": "2.2.2",
"mmjstool": "github:mattermost/mattermost-utilities#73e61d2ede0ebf802492df4cfbac481d35efed54", "mmjstool": "github:mattermost/mattermost-utilities#73e61d2ede0ebf802492df4cfbac481d35efed54",
"nock": "13.2.8", "nock": "13.2.8",

View File

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

View File

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

View File

@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {mount, shallow} from 'enzyme';
import React from 'react'; import React from 'react';
import type {AppBinding} from '@mattermost/types/apps'; 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 {Permissions} from 'mattermost-redux/constants';
import {AppBindingLocations} from 'mattermost-redux/constants/apps'; 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 type {PluginComponent} from 'types/store/plugins';
import AppBar from './app_bar'; 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', () => { 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[] = [ const channelHeaderComponents: PluginComponent[] = [
{ {
id: 'the_component_id', id: 'the_component_id',
@ -134,65 +48,141 @@ describe('components/app_bar/app_bar', () => {
}, },
] as AppBinding[]; ] as AppBinding[];
test('should match snapshot on mount', async () => { const initialState = {
const wrapper = mount( views: {
<AppBar/>, rhs: {
); isSidebarOpen: true,
rhsState: 'plugin',
expect(wrapper).toMatchSnapshot(); pluggableId: 'the_rhs_plugin_component',
});
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',
}, },
} as any; },
plugins: {
const wrapper = shallow( components: {
<AppBar/>, AppBar: channelHeaderComponents,
); RightHandSidebarComponent: rhsComponents,
Product: [],
expect(wrapper.find('AppBarMarketplace').exists()).toEqual(false); } as {[componentName: string]: PluginComponent[]},
}); },
entities: {
test('should show marketplace if enabled and user has SYSCONSOLE_WRITE_PLUGINS permission', async () => { apps: {
mockState.entities.general = { main: {
config: { bindings: channelHeaderAppBindings,
DisableAppBar: 'false', } as {bindings: AppBinding[]},
FeatureFlagAppsEnabled: 'true', pluginEnabled: true,
EnableMarketplace: 'true',
PluginsEnabled: 'true',
}, },
} as any; general: {
config: {
mockState.entities.roles = { DisableAppBar: 'false',
roles: { FeatureFlagAppsEnabled: 'true',
system_user: {
permissions: [
Permissions.SYSCONSOLE_WRITE_PLUGINS,
],
}, },
}, },
} 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/>, <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 classNames from 'classnames';
import React, {useRef, useState} from 'react'; import React, {useRef, useState} from 'react';
import {Tooltip} from 'react-bootstrap'; import {Tooltip} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage, useIntl} from 'react-intl';
import OverlayTrigger from 'components/overlay_trigger'; import OverlayTrigger from 'components/overlay_trigger';
@ -21,6 +21,8 @@ type Props = {
}; };
const CopyButton: React.FC<Props> = (props: Props) => { const CopyButton: React.FC<Props> = (props: Props) => {
const intl = useIntl();
const [isCopied, setIsCopied] = useState(false); const [isCopied, setIsCopied] = useState(false);
const timerRef = useRef<NodeJS.Timeout | null>(null); const timerRef = useRef<NodeJS.Timeout | null>(null);
@ -74,16 +76,16 @@ const CopyButton: React.FC<Props> = (props: Props) => {
<span <span
className={spanClassName} className={spanClassName}
onClick={copyText} onClick={copyText}
aria-label={intl.formatMessage({id: getId(), defaultMessage: getDefaultMessage()})}
role='button'
> >
{!isCopied && {!isCopied &&
<i <i
role='button'
className='icon icon-content-copy' className='icon icon-content-copy'
/> />
} }
{isCopied && {isCopied &&
<i <i
role='button'
className='icon icon-check' className='icon icon-check'
/> />
} }

View File

@ -3,14 +3,10 @@
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import React from 'react'; 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 TeamUrl from 'components/create_team/components/team_url/team_url';
import {mountWithIntl} from 'tests/helpers/intl-test-helper'; import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import Constants from 'utils/constants'; import Constants from 'utils/constants';
jest.mock('images/logo.png', () => 'logo.png'); jest.mock('images/logo.png', () => 'logo.png');
@ -30,31 +26,20 @@ describe('/components/create_team/components/display_name', () => {
history: {push: jest.fn()}, 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', () => { test('should match snapshot', () => {
const wrapper = shallow(<TeamUrl {...defaultProps}/>); const wrapper = shallow(<TeamUrl {...defaultProps}/>);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
test('should return to display_name.jsx page', () => { test('should return to display_name.jsx page', async () => {
const wrapper = mountWithIntl(<TeamUrl {...defaultProps}/>); renderWithContext(<TeamUrl {...defaultProps}/>);
wrapper.find('a').simulate('click', { screen.getByText('Back to previous step').click();
preventDefault: () => jest.fn(),
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 () => { test('should successfully submit', async () => {
@ -65,81 +50,80 @@ describe('/components/create_team/components/display_name', () => {
const actions = {...defaultProps.actions, checkIfTeamExists}; const actions = {...defaultProps.actions, checkIfTeamExists};
const props = {...defaultProps, actions}; const props = {...defaultProps, actions};
const wrapper = mountWithIntl( renderWithContext(
<TeamUrl {...props}/>, <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.checkIfTeamExists).toHaveBeenCalledTimes(1);
expect(actions.createTeam).not.toHaveBeenCalled(); expect(actions.createTeam).not.toHaveBeenCalled();
await (wrapper.instance() as unknown as TeamUrl).submitNext({preventDefault: jest.fn()} as unknown as React.MouseEvent<Button, MouseEvent>); screen.getByText('Finish').click();
expect(actions.checkIfTeamExists).toHaveBeenCalledTimes(2);
expect(actions.createTeam).toHaveBeenCalledTimes(1); await waitFor(() => {
expect(actions.createTeam).toBeCalledWith({display_name: 'test-team', name: 'test-team', type: 'O'}); expect(actions.checkIfTeamExists).toHaveBeenCalledTimes(2);
expect(props.history.push).toHaveBeenCalledTimes(1); expect(actions.createTeam).toHaveBeenCalledTimes(1);
expect(props.history.push).toBeCalledWith('/test-team/channels/town-square'); 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', () => { test('should display isRequired error', () => {
const wrapper = mountWithIntl(<TeamUrl {...defaultProps}/>); renderWithContext(
(wrapper.find('.form-control').instance() as unknown as HTMLInputElement).value = ''; <TeamUrl {...defaultProps}/>,
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'
/>,
); );
userEvent.clear(screen.getByRole('textbox'));
screen.getByText('Finish').click();
expect(screen.getByText('This field is required')).toBeInTheDocument();
}); });
test('should display charLength error', () => { test('should display charLength error', () => {
const wrapper = mountWithIntl(<TeamUrl {...defaultProps}/>); 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';
wrapper.find('.form-control').simulate('change');
wrapper.find('button').simulate('click', {preventDefault: () => jest.fn()});
expect(wrapper.state('nameError')).toEqual(chatLengthError);
(wrapper.find('.form-control').instance() as unknown as HTMLInputElement).value = 'a'.repeat(Constants.MAX_TEAMNAME_LENGTH + 1); renderWithContext(
wrapper.find('.form-control').simulate('change'); <TeamUrl {...defaultProps}/>,
wrapper.find('button').simulate('click', {preventDefault: () => jest.fn()}); );
expect(wrapper.state('nameError')).toEqual(chatLengthError);
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', () => { test('should display teamUrl regex error', () => {
const wrapper = mountWithIntl(<TeamUrl {...defaultProps}/>); renderWithContext(
(wrapper.find('.form-control').instance() as unknown as HTMLInputElement).value = '!!wrongName1'; <TeamUrl {...defaultProps}/>,
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."
/>,
); );
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', () => { test('should display teamUrl taken error', () => {
const store = mockStore({ renderWithContext(
entities: { <TeamUrl {...defaultProps}/>,
general: { );
config: {},
license: {
Cloud: 'false',
},
},
users: {
currentUserId: 'currentUserId',
},
},
});
const wrapper = mountWithIntl(<Provider store={store}><TeamUrl {...defaultProps}/></Provider>); userEvent.type(screen.getByRole('textbox'), 'channel');
(wrapper.find('.form-control').instance() as unknown as HTMLInputElement).value = 'channel'; screen.getByText('Finish').click();
wrapper.find('.form-control').simulate('change');
wrapper.find('button').simulate('click', {preventDefault: () => jest.fn()}); expect(screen.getByText('Please try another.', {exact: false})).toBeInTheDocument();
expect((wrapper as any).find(TeamUrl).state('nameError').props.id).toEqual('create_team.team_url.taken');
}); });
}); });

View File

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

View File

@ -7,7 +7,6 @@ import React from 'react';
import type {GlobalState} from '@mattermost/types/store'; import type {GlobalState} from '@mattermost/types/store';
import type {DeepPartial} from '@mattermost/types/utilities'; import type {DeepPartial} from '@mattermost/types/utilities';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext, screen} from 'tests/react_testing_utils'; import {renderWithContext, screen} from 'tests/react_testing_utils';
import FileAttachment from './file_attachment'; import FileAttachment from './file_attachment';
@ -168,15 +167,12 @@ describe('FileAttachment', () => {
test('should blur file attachment link after click', () => { test('should blur file attachment link after click', () => {
const props = {...baseProps, compactDisplay: true}; const props = {...baseProps, compactDisplay: true};
const wrapper = mountWithIntl(<FileAttachment {...props}/>); renderWithContext(<FileAttachment {...props}/>);
const e = {
preventDefault: jest.fn(),
target: {blur: jest.fn()},
};
const a = wrapper.find('#file-attachment-link'); const link = screen.getByText(baseProps.fileInfo.name);
a.simulate('click', e); const blur = jest.spyOn(link, 'blur');
expect(e.target.blur).toHaveBeenCalled(); screen.getByText(baseProps.fileInfo.name).click();
expect(blur).toHaveBeenCalled();
}); });
describe('archived file', () => { 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>} overlay={<Tooltip id='file-name__tooltip'>{fileName}</Tooltip>}
> >
<a <a
id='file-attachment-link'
href='#' href='#'
onClick={handleImageClick} onClick={handleImageClick}
className='post-image__name' 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. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {mount, shallow} from 'enzyme';
import React from 'react'; import React from 'react';
import type {ComponentProps} from 'react'; import type {ComponentProps} from 'react';
import * as fileActions from 'mattermost-redux/actions/files'; import * as fileActions from 'mattermost-redux/actions/files';
import OverlayTrigger from 'components/overlay_trigger'; import {renderWithContext, screen} from 'tests/react_testing_utils';
import Tooltip from 'components/tooltip';
import {TestHelper} from 'utils/test_helper'; import {TestHelper} from 'utils/test_helper';
import * as Utils from 'utils/utils'; import * as Utils from 'utils/utils';
import type {GlobalState} from 'types/store';
import FilePreviewModalMainActions from './file_preview_modal_main_actions'; 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', () => { describe('components/file_preview_modal/file_preview_modal_main_actions/FilePreviewModalMainActions', () => {
let defaultProps: ComponentProps<typeof FilePreviewModalMainActions>; let defaultProps: ComponentProps<typeof FilePreviewModalMainActions>;
beforeEach(() => { beforeEach(() => {
@ -39,22 +26,6 @@ describe('components/file_preview_modal/file_preview_modal_main_actions/FilePrev
content: 'test content', content: 'test content',
canCopyContent: false, 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', () => { 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, enablePublicLink: false,
}; };
const wrapper = shallow(<FilePreviewModalMainActions {...props}/>); renderWithContext(
expect(wrapper).toMatchSnapshot(); <FilePreviewModalMainActions {...props}/>,
);
expect(screen.queryByLabelText('Get a public link')).not.toBeInTheDocument();
}); });
test('should match snapshot with public links enabled', () => { 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, enablePublicLink: true,
}; };
const wrapper = shallow(<FilePreviewModalMainActions {...props}/>); renderWithContext(
expect(wrapper).toMatchSnapshot(); <FilePreviewModalMainActions {...props}/>,
const overlayWrapper = wrapper.find(OverlayTrigger).first(); );
expect(overlayWrapper.prop('overlay').type).toEqual(Tooltip);
expect(overlayWrapper.prop('children')).toMatchSnapshot(); 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 = { const props = {
...defaultProps, ...defaultProps,
enablePublicLink: true, enablePublicLink: true,
showPublicLink: false, showPublicLink: false,
}; };
const wrapper = shallow(<FilePreviewModalMainActions {...props}/>); renderWithContext(
expect(wrapper).toMatchSnapshot(); <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 = { const props = {
...defaultProps, ...defaultProps,
canCopyContent: true, canCopyContent: true,
}; };
const wrapper = shallow(<FilePreviewModalMainActions {...props}/>); renderWithContext(
expect(wrapper).toMatchSnapshot(); <FilePreviewModalMainActions {...props}/>,
);
expect(screen.getByLabelText('Copy code')).toBeInTheDocument();
}); });
test('should call public link callback', () => { test('should call public link callback', () => {
@ -107,17 +87,22 @@ describe('components/file_preview_modal/file_preview_modal_main_actions/FilePrev
...defaultProps, ...defaultProps,
enablePublicLink: true, enablePublicLink: true,
}; };
const wrapper = shallow(<FilePreviewModalMainActions {...props}/>); renderWithContext(
expect(wrapper.find(OverlayTrigger)).toHaveLength(3); <FilePreviewModalMainActions {...props}/>,
const overlayWrapper = wrapper.find(OverlayTrigger).first().children('a'); );
expect(spy).toHaveBeenCalledTimes(0); expect(spy).toHaveBeenCalledTimes(0);
overlayWrapper.simulate('click');
screen.getByLabelText('Get a public link').click();
expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
}); });
test('should not get public api when public links is disabled', async () => { test('should not get public api when public links is disabled', async () => {
const spy = jest.spyOn(fileActions, 'getFilePublicLink'); const spy = jest.spyOn(fileActions, 'getFilePublicLink');
mount(<FilePreviewModalMainActions {...defaultProps}/>); renderWithContext(
<FilePreviewModalMainActions {...defaultProps}/>,
);
expect(spy).toHaveBeenCalledTimes(0); expect(spy).toHaveBeenCalledTimes(0);
}); });
@ -127,7 +112,9 @@ describe('components/file_preview_modal/file_preview_modal_main_actions/FilePrev
...defaultProps, ...defaultProps,
enablePublicLink: true, enablePublicLink: true,
}; };
mount(<FilePreviewModalMainActions {...props}/>); renderWithContext(
<FilePreviewModalMainActions {...props}/>,
);
expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
}); });
@ -137,9 +124,11 @@ describe('components/file_preview_modal/file_preview_modal_main_actions/FilePrev
...defaultProps, ...defaultProps,
canCopyContent: true, canCopyContent: true,
}; };
const wrapper = mount(<FilePreviewModalMainActions {...props}/>); renderWithContext(
<FilePreviewModalMainActions {...props}/>,
);
expect(spy).toHaveBeenCalledTimes(0); expect(spy).toHaveBeenCalledTimes(0);
wrapper.find('.icon-content-copy').simulate('click'); screen.getByLabelText('Copy code').click();
expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledTimes(1);
}); });
}); });

View File

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

View File

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

View File

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

View File

@ -17,8 +17,6 @@ import {isEmail} from 'mattermost-redux/utils/helpers';
import {trackEvent} from 'actions/telemetry_actions'; import {trackEvent} from 'actions/telemetry_actions';
import {getRoleForTrackFlow} from 'utils/utils';
import {InviteType} from './invite_as'; import {InviteType} from './invite_as';
import InviteView, {initializeInviteState} from './invite_view'; import InviteView, {initializeInviteState} from './invite_view';
import type {InviteState} from './invite_view'; import type {InviteState} from './invite_view';
@ -73,6 +71,7 @@ export type Props = {
channelToInvite?: Channel; channelToInvite?: Channel;
initialValue?: string; initialValue?: string;
inviteAsGuest?: boolean; inviteAsGuest?: boolean;
roleForTrackFlow: {started_by_role: string};
} }
export const View = { export const View = {
@ -162,12 +161,11 @@ export class InvitationModal extends React.PureComponent<Props, State> {
if (!this.props.currentTeam) { if (!this.props.currentTeam) {
return; return;
} }
const roleForTrackFlow = getRoleForTrackFlow();
const inviteAs = this.state.invite.inviteType; const inviteAs = this.state.invite.inviteType;
if (inviteAs === InviteType.MEMBER && this.props.isCloud) { 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 users: UserProfile[] = [];
const emails: string[] = []; 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 deepFreeze from 'mattermost-redux/utils/deep_freeze';
import store from 'stores/redux_store';
import {mountWithThemedIntl} from 'tests/helpers/themed-intl-test-helper'; import {mountWithThemedIntl} from 'tests/helpers/themed-intl-test-helper';
import mockStore from 'tests/test_store';
import {SelfHostedProducts} from 'utils/constants'; import {SelfHostedProducts} from 'utils/constants';
import {TestHelper as TH} from 'utils/test_helper'; import {TestHelper as TH} from 'utils/test_helper';
import {generateId} from 'utils/utils'; import {generateId} from 'utils/utils';
@ -120,7 +119,7 @@ describe('InviteView', () => {
}, },
}; };
store.getState = () => (state); const store = mockStore(state);
beforeEach(() => { beforeEach(() => {
props = defaultProps; props = defaultProps;

View File

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

View File

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

View File

@ -5,7 +5,10 @@ import {mount} from 'enzyme';
import React from 'react'; import React from 'react';
import {OverlayTrigger as BaseOverlayTrigger} from 'react-bootstrap'; // eslint-disable-line no-restricted-imports import {OverlayTrigger as BaseOverlayTrigger} from 'react-bootstrap'; // eslint-disable-line no-restricted-imports
import {FormattedMessage, IntlProvider} from 'react-intl'; 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 {mountWithIntl} from 'tests/helpers/intl-test-helper';
import OverlayTrigger from './overlay_trigger'; import OverlayTrigger from './overlay_trigger';
@ -13,6 +16,8 @@ import OverlayTrigger from './overlay_trigger';
describe('OverlayTrigger', () => { describe('OverlayTrigger', () => {
const testId = 'test.value'; const testId = 'test.value';
let store: Store;
const intlProviderProps = { const intlProviderProps = {
defaultLocale: 'en', defaultLocale: 'en',
locale: 'en', locale: 'en',
@ -33,6 +38,7 @@ describe('OverlayTrigger', () => {
let originalConsoleError: () => void; let originalConsoleError: () => void;
beforeEach(() => { beforeEach(() => {
store = testConfigureStore();
originalConsoleError = console.error; originalConsoleError = console.error;
console.error = jest.fn(); console.error = jest.fn();
}); });
@ -43,11 +49,13 @@ describe('OverlayTrigger', () => {
test('base OverlayTrigger should fail to pass intl to overlay', () => { test('base OverlayTrigger should fail to pass intl to overlay', () => {
const wrapper = mount( const wrapper = mount(
<IntlProvider {...intlProviderProps}> <ReduxProvider store={store}>
<BaseOverlayTrigger {...baseProps}> <IntlProvider {...intlProviderProps}>
<span/> <BaseOverlayTrigger {...baseProps}>
</BaseOverlayTrigger> <span/>
</IntlProvider>, </BaseOverlayTrigger>
</IntlProvider>
</ReduxProvider>,
); );
// console.error will have been called by FormattedMessage because its intl context is missing // 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', () => { test('custom OverlayTrigger should pass intl to overlay', () => {
const wrapper = mount( const wrapper = mount(
<IntlProvider {...intlProviderProps}> <ReduxProvider store={store}>
<OverlayTrigger {...baseProps}> <IntlProvider {...intlProviderProps}>
<span/> <OverlayTrigger {...baseProps}>
</OverlayTrigger> <span/>
</IntlProvider>, </OverlayTrigger>
</IntlProvider>
</ReduxProvider>,
); );
const overlay = mount(wrapper.find(BaseOverlayTrigger).prop('overlay')); const overlay = mount(wrapper.find(BaseOverlayTrigger).prop('overlay'));
@ -79,11 +89,13 @@ describe('OverlayTrigger', () => {
}; };
const wrapper = mountWithIntl( const wrapper = mountWithIntl(
<IntlProvider {...intlProviderProps}> <ReduxProvider store={store}>
<OverlayTrigger {...props}> <IntlProvider {...intlProviderProps}>
<span/> <OverlayTrigger {...props}>
</OverlayTrigger> <span/>
</IntlProvider>, </OverlayTrigger>
</IntlProvider>
</ReduxProvider>,
); );
expect(ref.current).toBe(wrapper.find(BaseOverlayTrigger).instance()); expect(ref.current).toBe(wrapper.find(BaseOverlayTrigger).instance());
@ -104,11 +116,13 @@ describe('OverlayTrigger', () => {
}; };
const wrapper = mount( const wrapper = mount(
<IntlProvider {...intlProviderProps}> <ReduxProvider store={store}>
<OverlayTrigger {...props}> <IntlProvider {...intlProviderProps}>
<span/> <OverlayTrigger {...props}>
</OverlayTrigger> <span/>
</IntlProvider>, </OverlayTrigger>
</IntlProvider>
</ReduxProvider>,
); );
// Dive into the react-bootstrap internals to find our overlay // Dive into the react-bootstrap internals to find our overlay
@ -143,11 +157,13 @@ describe('OverlayTrigger', () => {
}; };
const wrapper = mount( const wrapper = mount(
<IntlProvider {...intlProviderProps}> <ReduxProvider store={store}>
<OverlayTrigger {...props}> <IntlProvider {...intlProviderProps}>
<span/> <OverlayTrigger {...props}>
</OverlayTrigger> <span/>
</IntlProvider>, </OverlayTrigger>
</IntlProvider>
</ReduxProvider>,
); );
// Dive into the react-bootstrap internals to find our overlay // 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 type {OverlayTriggerProps} from 'react-bootstrap';
import {IntlContext} from 'react-intl'; import {IntlContext} from 'react-intl';
import type {IntlShape} from 'react-intl'; import type {IntlShape} from 'react-intl';
import {Provider} from 'react-redux'; import {Provider, useStore} from 'react-redux';
import store from 'stores/redux_store';
export type BaseOverlayTrigger = OriginalOverlayTrigger & { export type BaseOverlayTrigger = OriginalOverlayTrigger & {
hide: () => void; hide: () => void;
@ -25,6 +23,8 @@ type Props = OverlayTriggerProps & {
const OverlayTrigger = React.forwardRef((props: Props, ref?: React.Ref<OriginalOverlayTrigger>) => { const OverlayTrigger = React.forwardRef((props: Props, ref?: React.Ref<OriginalOverlayTrigger>) => {
const {overlay, disabled, ...otherProps} = props; 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 // 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. // that context itself, so we have to manually forward the react-intl context to this component's child.
const OverlayWrapper = ({intl, ...overlayProps}: {intl: IntlShape}) => ( const OverlayWrapper = ({intl, ...overlayProps}: {intl: IntlShape}) => (

View File

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

View File

@ -4,7 +4,7 @@
import React from 'react'; import React from 'react';
import type {ComponentProps} 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 {TestHelper} from 'utils/test_helper';
import PostProfilePicture from './post_profile_picture'; import PostProfilePicture from './post_profile_picture';
@ -31,7 +31,7 @@ describe('components/PostProfilePicture', () => {
test('no status and post icon override specified, default props', () => { test('no status and post icon override specified, default props', () => {
const props: Props = baseProps; const props: Props = baseProps;
render( renderWithContext(
<PostProfilePicture {...props}/>, <PostProfilePicture {...props}/>,
); );
@ -47,7 +47,7 @@ describe('components/PostProfilePicture', () => {
status: 'away', status: 'away',
postIconOverrideURL: 'http://example.com/image.png', postIconOverrideURL: 'http://example.com/image.png',
}; };
render( renderWithContext(
<PostProfilePicture {...props}/>, <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. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {mount} from 'enzyme';
import React from 'react'; import React from 'react';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import {QuickInput} from './quick_input'; import {QuickInput} from './quick_input';
describe('components/QuickInput', () => { describe('components/QuickInput', () => {
@ -14,16 +15,16 @@ describe('components/QuickInput', () => {
['when value undefined', {clearable: true, onClear: () => {}}], ['when value undefined', {clearable: true, onClear: () => {}}],
['when value empty', {value: '', clearable: true, onClear: () => {}}], ['when value empty', {value: '', clearable: true, onClear: () => {}}],
])('should not render clear button', (description, props) => { ])('should not render clear button', (description, props) => {
const wrapper = mount( renderWithContext(
<QuickInput {...props}/>, <QuickInput {...props}/>,
); );
expect(wrapper.find('.input-clear').exists()).toBe(false); expect(screen.queryByTestId('input-clear')).not.toBeInTheDocument();
}); });
describe('should render clear button', () => { describe('should render clear button', () => {
test('with default tooltip text', () => { test('with default tooltip text', () => {
const wrapper = mount( renderWithContext(
<QuickInput <QuickInput
value='mock' value='mock'
clearable={true} 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', () => { test('with customized tooltip text', () => {
const wrapper = mount( renderWithContext(
<QuickInput <QuickInput
value='mock' value='mock'
clearable={true} 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', () => { test('with customized tooltip component', () => {
const wrapper = mount( renderWithContext(
<QuickInput <QuickInput
value='mock' value='mock'
clearable={true} 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/>; return <div/>;
} }
} }
const wrapper = mount( const {rerender} = renderWithContext(
<QuickInput <QuickInput
value='mock' value='mock'
clearable={true} clearable={true}
@ -80,11 +81,20 @@ describe('components/QuickInput', () => {
/>, />,
); );
wrapper.setProps({onClear: () => wrapper.setProps({value: ''})}); expect(screen.queryByTestId('input-clear')).toBeInTheDocument();
expect(wrapper.find('.input-clear').exists()).toBe(true);
wrapper.find('.input-clear').simulate('mousedown'); userEvent.click(screen.getByTestId('input-clear'));
expect(wrapper.find('.input-clear').exists()).toBe(false);
rerender(
<QuickInput
value=''
clearable={true}
onClear={() => {}}
inputComponent={MockComp}
/>,
);
expect(screen.queryByTestId('input-clear')).not.toBeInTheDocument();
expect(focusFn).toBeCalled(); expect(focusFn).toBeCalled();
}); });
}); });

View File

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

View File

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

View File

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

View File

@ -89,7 +89,10 @@ export default function TeamButton({
teamClass = 'unread'; teamClass = 'unread';
badge = ( badge = (
<span className={'unread-badge'}/> <span
data-testid={'team-badge-' + teamId}
className={'unread-badge'}
/>
); );
} else if (isNotCreateTeamButton) { } else if (isNotCreateTeamButton) {
teamClass = ''; teamClass = '';
@ -115,7 +118,12 @@ export default function TeamButton({
}); });
badge = ( 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> </Draggable>
) : ( ) : (
<div <div
data-testid={'team-container-' + teamId}
className={`team-container ${teamClass}`} className={`team-container ${teamClass}`}
> >
{teamButton} {teamButton}

View File

@ -2,10 +2,9 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import type {ReactWrapper} from 'enzyme';
import React from 'react'; 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 * as Utils from 'utils/utils';
import Toast from './toast'; import Toast from './toast';
@ -45,12 +44,14 @@ describe('components/Toast', () => {
test('should dismiss', () => { test('should dismiss', () => {
defaultProps.onDismiss = jest.fn(); defaultProps.onDismiss = jest.fn();
const wrapper: ReactWrapper<any, any, React.Component> = mountWithIntl(<Toast {... defaultProps}><span>{'child'}</span></Toast>); renderWithContext(
const toast = wrapper.find(Toast).instance(); <Toast {... defaultProps}>
<span>{'child'}</span>
</Toast>,
);
screen.getByTestId('dismissToast').click();
if (toast instanceof Toast) {
toast.handleDismiss();
}
expect(defaultProps.onDismiss).toHaveBeenCalledTimes(1); 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 type {Theme} from 'mattermost-redux/selectors/entities/preferences';
import ChannelHeaderPlug from 'plugins/channel_header_plug/channel_header_plug'; 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'; import type {PluginComponent} from 'types/store/plugins';
@ -22,8 +22,8 @@ describe('plugins/ChannelHeaderPlug', () => {
tooltipText: 'some tooltip text', tooltipText: 'some tooltip text',
} as PluginComponent; } as PluginComponent;
test('should match snapshot with no extended component', () => { test('should not render anything with no extended component', () => {
const wrapper = mountWithIntl( const {asFragment} = renderWithContext(
<ChannelHeaderPlug <ChannelHeaderPlug
components={[]} components={[]}
channel={{} as Channel} channel={{} as Channel}
@ -40,11 +40,11 @@ describe('plugins/ChannelHeaderPlug', () => {
shouldShowAppBar={false} shouldShowAppBar={false}
/>, />,
); );
expect(wrapper).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
test('should match snapshot with one extended component', () => { test('should match snapshot with one extended component', () => {
const wrapper = mountWithIntl( const {asFragment} = renderWithContext(
<ChannelHeaderPlug <ChannelHeaderPlug
components={[testPlug]} components={[testPlug]}
channel={{} as Channel} channel={{} as Channel}
@ -61,11 +61,11 @@ describe('plugins/ChannelHeaderPlug', () => {
shouldShowAppBar={false} shouldShowAppBar={false}
/>, />,
); );
expect(wrapper).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
test('should match snapshot with six extended components', () => { test('should match snapshot with six extended components', () => {
const wrapper = mountWithIntl( const {asFragment} = renderWithContext(
<ChannelHeaderPlug <ChannelHeaderPlug
components={[ components={[
testPlug, testPlug,
@ -98,11 +98,11 @@ describe('plugins/ChannelHeaderPlug', () => {
shouldShowAppBar={false} shouldShowAppBar={false}
/>, />,
); );
expect(wrapper).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });
test('should match snapshot when the App Bar is visible', () => { test('should not render anything when the App Bar is visible', () => {
const wrapper = mountWithIntl( const {asFragment} = renderWithContext(
<ChannelHeaderPlug <ChannelHeaderPlug
components={[ components={[
testPlug, testPlug,
@ -124,6 +124,6 @@ describe('plugins/ChannelHeaderPlug', () => {
shouldShowAppBar={true} 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 {getTeamByName as getTeamByNameAction} from 'mattermost-redux/actions/teams';
import {Client4} from 'mattermost-redux/client'; import {Client4} from 'mattermost-redux/client';
import {Preferences, General} from 'mattermost-redux/constants'; import {Preferences, General} from 'mattermost-redux/constants';
import {createSelector} from 'mattermost-redux/selectors/create_selector';
import { import {
getChannel, getChannel,
getChannelsNameMapInTeam, getChannelsNameMapInTeam,
@ -1630,8 +1631,7 @@ const TrackFlowRoles: Record<string, string> = {
su: General.SYSTEM_USER_ROLE, su: General.SYSTEM_USER_ROLE,
}; };
export function getTrackFlowRole() { export function getTrackFlowRole(state: GlobalState) {
const state = store.getState();
let trackFlowRole = 'su'; let trackFlowRole = 'su';
if (isFirstAdmin(state)) { if (isFirstAdmin(state)) {
@ -1643,11 +1643,15 @@ export function getTrackFlowRole() {
return trackFlowRole; return trackFlowRole;
} }
export function getRoleForTrackFlow() { export const getRoleForTrackFlow = createSelector(
const startedByRole = TrackFlowRoles[getTrackFlowRole()]; 'getRoleForTrackFlow',
getTrackFlowRole,
(trackFlowRole) => {
const startedByRole = TrackFlowRoles[trackFlowRole];
return {started_by_role: startedByRole}; return {started_by_role: startedByRole};
} },
);
export function getSbr() { export function getSbr() {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);

View File

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