From 22d72b6df8084afb569c2be5987d2d42143d0f65 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Mon, 22 Apr 2024 14:53:42 -0400 Subject: [PATCH] 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 --- webapp/channels/package.json | 1 - .../trial_banner/trial_banner.tsx | 15 +- .../__snapshots__/app_bar.test.tsx.snap | 352 +-- .../src/components/app_bar/app_bar.test.tsx | 274 +- .../channels/src/components/copy_button.tsx | 8 +- .../components/team_url/team_url.test.tsx | 138 +- .../emoji_picker/emoji_picker.test.tsx | 32 +- .../filename_overlay.test.tsx.snap | 1 - .../file_attachment/file_attachment.test.tsx | 14 +- .../file_attachment/filename_overlay.tsx | 1 - ...e_preview_modal_main_actions.test.tsx.snap | 337 --- .../file_preview_modal_main_actions.test.tsx | 91 +- .../file_preview_modal_main_actions.tsx | 55 +- .../src/components/invitation_modal/index.tsx | 2 + .../invitation_modal.test.tsx | 7 +- .../invitation_modal/invitation_modal.tsx | 8 +- .../invitation_modal/invite_view.test.tsx | 5 +- .../invitation_modal/invite_view.tsx | 10 +- .../new_channel_modal.test.tsx | 180 +- .../src/components/overlay_trigger.test.tsx | 66 +- .../src/components/overlay_trigger.tsx | 6 +- .../components/post_emoji/post_emoji.test.tsx | 8 +- .../post_profile_picture.test.tsx | 6 +- .../__snapshots__/quick_input.test.tsx.snap | 194 -- .../quick_input/quick_input.test.tsx | 38 +- .../components/quick_input/quick_input.tsx | 2 + .../__snapshots__/search_bar.test.tsx.snap | 4 + .../components/team_button.test.tsx | 44 +- .../team_sidebar/components/team_button.tsx | 13 +- .../src/components/toast/toast.test.tsx | 15 +- .../channel_header_plug.test.tsx.snap | 2349 +---------------- .../channel_header_plug.test.tsx | 22 +- webapp/channels/src/utils/utils.tsx | 16 +- webapp/package-lock.json | 20 - 34 files changed, 715 insertions(+), 3619 deletions(-) delete mode 100644 webapp/channels/src/components/file_preview_modal/file_preview_modal_main_actions/__snapshots__/file_preview_modal_main_actions.test.tsx.snap delete mode 100644 webapp/channels/src/components/quick_input/__snapshots__/quick_input.test.tsx.snap diff --git a/webapp/channels/package.json b/webapp/channels/package.json index 1f1d82d095..81c51a31c0 100644 --- a/webapp/channels/package.json +++ b/webapp/channels/package.json @@ -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", diff --git a/webapp/channels/src/components/admin_console/license_settings/trial_banner/trial_banner.tsx b/webapp/channels/src/components/admin_console/license_settings/trial_banner/trial_banner.tsx index 8cdad803e7..087fe00897 100644 --- a/webapp/channels/src/components/admin_console/license_settings/trial_banner/trial_banner.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/trial_banner/trial_banner.tsx @@ -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((state) => getBoolPreference(state, Preferences.UNIQUE, Unique.REQUEST_TRIAL_AFTER_SERVER_UPGRADE)); + const clickedUpgradeAndTrialBtn = useSelector((state) => getBoolPreference(state, Preferences.UNIQUE, Unique.CLICKED_UPGRADE_AND_TRIAL_BTN)); const userId = useSelector((state: GlobalState) => getCurrentUserId(state)); diff --git a/webapp/channels/src/components/app_bar/__snapshots__/app_bar.test.tsx.snap b/webapp/channels/src/components/app_bar/__snapshots__/app_bar.test.tsx.snap index 81019b31b0..8b045ddd9f 100644 --- a/webapp/channels/src/components/app_bar/__snapshots__/app_bar.test.tsx.snap +++ b/webapp/channels/src/components/app_bar/__snapshots__/app_bar.test.tsx.snap @@ -1,335 +1,79 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`components/app_bar/app_bar should match snapshot on mount 1`] = ` - +
- - - - Playbooks Tooltip - - - } - placement="left" - trigger={ - Array [ - "hover", - "focus", - ] - } +
- - - Playbooks Tooltip - - - } - placement="left" - trigger={ - Array [ - "hover", - "focus", - ] - } - > -
-
- fallback_component -
-
-
- - + fallback_component +
+

- - - - Create Subscription - - - } - placement="left" - trigger={ - Array [ - "hover", - "focus", - ] - } +
- - - Create Subscription - - - } - placement="left" - trigger={ - Array [ - "hover", - "focus", - ] - } - > -
-
- -
-
-
- - + +
+
-
+ `; exports[`components/app_bar/app_bar should match snapshot on mount when App Bar is disabled 1`] = ` - +
- - - - Playbooks Tooltip - - - } - placement="left" - trigger={ - Array [ - "hover", - "focus", - ] - } +
- - - Playbooks Tooltip - - - } - placement="left" - trigger={ - Array [ - "hover", - "focus", - ] - } - > -
-
- fallback_component -
-
-
- - + fallback_component +
+

- - - - Create Subscription - - - } - placement="left" - trigger={ - Array [ - "hover", - "focus", - ] - } +
- - - Create Subscription - - - } - placement="left" - trigger={ - Array [ - "hover", - "focus", - ] - } - > -
-
- -
-
-
- - + +
+
-
+ `; diff --git a/webapp/channels/src/components/app_bar/app_bar.test.tsx b/webapp/channels/src/components/app_bar/app_bar.test.tsx index 012f5a5298..3225145392 100644 --- a/webapp/channels/src/components/app_bar/app_bar.test.tsx +++ b/webapp/channels/src/components/app_bar/app_bar.test.tsx @@ -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( - , - ); - - expect(wrapper).toMatchSnapshot(); - }); - - test('should match snapshot on mount when App Bar is disabled', async () => { - mockState.entities.general.config.DisableAppBar = 'false'; - - const wrapper = mount( - , - ); - - 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( - , - ); - - 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( , + 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( + , + 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( + , + 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( + , + testState, + ); + + expect(screen.queryByLabelText('App Marketplace')).toBeInTheDocument(); }); }); diff --git a/webapp/channels/src/components/copy_button.tsx b/webapp/channels/src/components/copy_button.tsx index ed47078b9b..a4816becbf 100644 --- a/webapp/channels/src/components/copy_button.tsx +++ b/webapp/channels/src/components/copy_button.tsx @@ -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) => { + const intl = useIntl(); + const [isCopied, setIsCopied] = useState(false); const timerRef = useRef(null); @@ -74,16 +76,16 @@ const CopyButton: React.FC = (props: Props) => { {!isCopied && } {isCopied && } diff --git a/webapp/channels/src/components/create_team/components/team_url/team_url.test.tsx b/webapp/channels/src/components/create_team/components/team_url/team_url.test.tsx index 544a4a35ae..c0b569588a 100644 --- a/webapp/channels/src/components/create_team/components/team_url/team_url.test.tsx +++ b/webapp/channels/src/components/create_team/components/team_url/team_url.test.tsx @@ -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 = ( - - ); - test('should match snapshot', () => { const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); - test('should return to display_name.jsx page', () => { - const wrapper = mountWithIntl(); + test('should return to display_name.jsx page', async () => { + renderWithContext(); - 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( , ); - await (wrapper.instance() as unknown as TeamUrl).submitNext({preventDefault: jest.fn()} as unknown as React.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); - 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(); - (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( - , + renderWithContext( + , ); + + 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(); - (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( + , + ); + + 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(); - (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( - , + renderWithContext( + , ); + + 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( + , + ); - const wrapper = mountWithIntl(); - (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(); }); }); diff --git a/webapp/channels/src/components/emoji_picker/emoji_picker.test.tsx b/webapp/channels/src/components/emoji_picker/emoji_picker.test.tsx index 92effa827f..911d1945fc 100644 --- a/webapp/channels/src/components/emoji_picker/emoji_picker.test.tsx +++ b/webapp/channels/src/components/emoji_picker/emoji_picker.test.tsx @@ -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( - - - , + const {asFragment} = renderWithContext( + , ); expect(asFragment()).toMatchSnapshot(); }); test('Recent category should not exist if there are no recent emojis', () => { - render( - - - , + renderWithContext( + , ); expect(screen.queryByLabelText('emoji_picker.recent')).toBeNull(); @@ -70,10 +60,8 @@ describe('components/emoji_picker/EmojiPicker', () => { recentEmojis: ['smile'], }; - render( - - - , + renderWithContext( + , ); expect(screen.queryByLabelText('emoji_picker.recent')).not.toBeNull(); @@ -85,10 +73,8 @@ describe('components/emoji_picker/EmojiPicker', () => { filter: 'wave', }; - render( - - - , + renderWithContext( + , ); expect(screen.queryByText('Preview for wave emoji')).not.toBeNull(); diff --git a/webapp/channels/src/components/file_attachment/__snapshots__/filename_overlay.test.tsx.snap b/webapp/channels/src/components/file_attachment/__snapshots__/filename_overlay.test.tsx.snap index 91e95b1bb8..130aaf801e 100644 --- a/webapp/channels/src/components/file_attachment/__snapshots__/filename_overlay.test.tsx.snap +++ b/webapp/channels/src/components/file_attachment/__snapshots__/filename_overlay.test.tsx.snap @@ -22,7 +22,6 @@ exports[`components/file_attachment/FilenameOverlay should match snapshot, compa diff --git a/webapp/channels/src/components/file_attachment/file_attachment.test.tsx b/webapp/channels/src/components/file_attachment/file_attachment.test.tsx index 6eb6d4d113..eeefd89a2e 100644 --- a/webapp/channels/src/components/file_attachment/file_attachment.test.tsx +++ b/webapp/channels/src/components/file_attachment/file_attachment.test.tsx @@ -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(); - const e = { - preventDefault: jest.fn(), - target: {blur: jest.fn()}, - }; + renderWithContext(); - 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', () => { diff --git a/webapp/channels/src/components/file_attachment/filename_overlay.tsx b/webapp/channels/src/components/file_attachment/filename_overlay.tsx index 19d0e60b29..c613c5200d 100644 --- a/webapp/channels/src/components/file_attachment/filename_overlay.tsx +++ b/webapp/channels/src/components/file_attachment/filename_overlay.tsx @@ -71,7 +71,6 @@ export default class FilenameOverlay extends React.PureComponent { overlay={{fileName}} > - - - - } - placement="bottom" - trigger={ - Array [ - "hover", - "focus", - ] - } - > - - - - - - - - } - placement="bottom" - trigger={ - Array [ - "hover", - "focus", - ] - } - > - - - -`; - -exports[`components/file_preview_modal/file_preview_modal_main_actions/FilePreviewModalMainActions should match snapshot when copy content is enabled 1`] = ` -
- - - - - } - placement="bottom" - trigger={ - Array [ - "hover", - "focus", - ] - } - > - - - - - - - - } - placement="bottom" - trigger={ - Array [ - "hover", - "focus", - ] - } - > - - -
-`; - -exports[`components/file_preview_modal/file_preview_modal_main_actions/FilePreviewModalMainActions should match snapshot with public links disabled 1`] = ` -
- - - - } - placement="bottom" - trigger={ - Array [ - "hover", - "focus", - ] - } - > - - - - - - - - } - placement="bottom" - trigger={ - Array [ - "hover", - "focus", - ] - } - > - - -
-`; - -exports[`components/file_preview_modal/file_preview_modal_main_actions/FilePreviewModalMainActions should match snapshot with public links enabled 1`] = ` -
- - - - } - placement="bottom" - shouldUpdatePosition={true} - trigger={ - Array [ - "hover", - "focus", - ] - } - > - - - - - - - - } - placement="bottom" - trigger={ - Array [ - "hover", - "focus", - ] - } - > - - - - - - - - } - placement="bottom" - trigger={ - Array [ - "hover", - "focus", - ] - } - > - - -
-`; - -exports[`components/file_preview_modal/file_preview_modal_main_actions/FilePreviewModalMainActions should match snapshot with public links enabled 2`] = ` - - - -`; diff --git a/webapp/channels/src/components/file_preview_modal/file_preview_modal_main_actions/file_preview_modal_main_actions.test.tsx b/webapp/channels/src/components/file_preview_modal/file_preview_modal_main_actions/file_preview_modal_main_actions.test.tsx index 9be459a122..ebd5a63e77 100644 --- a/webapp/channels/src/components/file_preview_modal/file_preview_modal_main_actions/file_preview_modal_main_actions.test.tsx +++ b/webapp/channels/src/components/file_preview_modal/file_preview_modal_main_actions/file_preview_modal_main_actions.test.tsx @@ -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; 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(); - expect(wrapper).toMatchSnapshot(); + renderWithContext( + , + ); + + 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(); - expect(wrapper).toMatchSnapshot(); - const overlayWrapper = wrapper.find(OverlayTrigger).first(); - expect(overlayWrapper.prop('overlay').type).toEqual(Tooltip); - expect(overlayWrapper.prop('children')).toMatchSnapshot(); + renderWithContext( + , + ); + + 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(); - expect(wrapper).toMatchSnapshot(); + renderWithContext( + , + ); + + 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(); - expect(wrapper).toMatchSnapshot(); + renderWithContext( + , + ); + + 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(); - expect(wrapper.find(OverlayTrigger)).toHaveLength(3); - const overlayWrapper = wrapper.find(OverlayTrigger).first().children('a'); + renderWithContext( + , + ); + 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(); + renderWithContext( + , + ); expect(spy).toHaveBeenCalledTimes(0); }); @@ -127,7 +112,9 @@ describe('components/file_preview_modal/file_preview_modal_main_actions/FilePrev ...defaultProps, enablePublicLink: true, }; - mount(); + renderWithContext( + , + ); 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(); + renderWithContext( + , + ); expect(spy).toHaveBeenCalledTimes(0); - wrapper.find('.icon-content-copy').simulate('click'); + screen.getByLabelText('Copy code').click(); expect(spy).toHaveBeenCalledTimes(1); }); }); diff --git a/webapp/channels/src/components/file_preview_modal/file_preview_modal_main_actions/file_preview_modal_main_actions.tsx b/webapp/channels/src/components/file_preview_modal/file_preview_modal_main_actions/file_preview_modal_main_actions.tsx index c8f50e13aa..66886b1f76 100644 --- a/webapp/channels/src/components/file_preview_modal/file_preview_modal_main_actions/file_preview_modal_main_actions.tsx +++ b/webapp/channels/src/components/file_preview_modal/file_preview_modal_main_actions/file_preview_modal_main_actions.tsx @@ -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) => { + 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) => { setPublicLinkCopied(true); }; + const closeMessage = intl.formatMessage({ + id: 'full_screen_modal.close', + defaultMessage: 'Close', + }); const closeButton = ( = (props: Props) => { placement={tooltipPlacement} overlay={ - + {closeMessage} } > ); - let publicTooltipMessage = ( - - ); + + let publicTooltipMessage; if (publicLinkCopied) { - publicTooltipMessage = ( - - ); + 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 = ( = (props: Props) => { href='#' className='file-preview-modal-main-actions__action-item' onClick={copyPublicLink} + aria-label={publicTooltipMessage} > ); - const downloadLinkProps: DownloadLinkProps = {}; - downloadLinkProps.download = props.filename; + + const downloadMessage = intl.formatMessage({ + id: 'view_image_popover.download', + defaultMessage: 'Download', + }); const download = ( = (props: Props) => { placement={tooltipPlacement} overlay={ - + {downloadMessage} } > @@ -139,6 +139,7 @@ const FilePreviewModalMainActions: React.FC = (props: Props) => { className='file-preview-modal-main-actions__action-item' location='file_preview_modal_main_actions' download={props.filename} + aria-label={downloadMessage} > diff --git a/webapp/channels/src/components/invitation_modal/index.tsx b/webapp/channels/src/components/invitation_modal/index.tsx index 25e00fa274..778379a9f8 100644 --- a/webapp/channels/src/components/invitation_modal/index.tsx +++ b/webapp/channels/src/components/invitation_modal/index.tsx @@ -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), }; } diff --git a/webapp/channels/src/components/invitation_modal/invitation_modal.test.tsx b/webapp/channels/src/components/invitation_modal/invitation_modal.test.tsx index 205277dfa2..ba8bc1654f 100644 --- a/webapp/channels/src/components/invitation_modal/invitation_modal.test.tsx +++ b/webapp/channels/src/components/invitation_modal/invitation_modal.test.tsx @@ -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; diff --git a/webapp/channels/src/components/invitation_modal/invitation_modal.tsx b/webapp/channels/src/components/invitation_modal/invitation_modal.tsx index 0ce4ae2bb4..1277f428f0 100644 --- a/webapp/channels/src/components/invitation_modal/invitation_modal.tsx +++ b/webapp/channels/src/components/invitation_modal/invitation_modal.tsx @@ -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 { 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[] = []; diff --git a/webapp/channels/src/components/invitation_modal/invite_view.test.tsx b/webapp/channels/src/components/invitation_modal/invite_view.test.tsx index 8c347bfd3f..03b0565a8d 100644 --- a/webapp/channels/src/components/invitation_modal/invite_view.test.tsx +++ b/webapp/channels/src/components/invitation_modal/invite_view.test.tsx @@ -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; diff --git a/webapp/channels/src/components/invitation_modal/invite_view.tsx b/webapp/channels/src/components/invitation_modal/invite_view.tsx index 3f1b2d6408..4cbbfebfa9 100644 --- a/webapp/channels/src/components/invitation_modal/invite_view.tsx +++ b/webapp/channels/src/components/invitation_modal/invite_view.tsx @@ -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, }); diff --git a/webapp/channels/src/components/new_channel_modal/new_channel_modal.test.tsx b/webapp/channels/src/components/new_channel_modal/new_channel_modal.test.tsx index 91330fc406..0b09c1699e 100644 --- a/webapp/channels/src/components/new_channel_modal/new_channel_modal.test.tsx +++ b/webapp/channels/src/components/new_channel_modal/new_channel_modal.test.tsx @@ -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 = { + 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(); + renderWithContext( + , + 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( , + initialState, ); // Change display name @@ -196,8 +190,9 @@ describe('components/new_channel_modal', () => { const url = 'channel-name-new'; - render( + renderWithContext( , + initialState, ); // Change display name @@ -235,8 +230,9 @@ describe('components/new_channel_modal', () => { }); test('should handle type changes', () => { - render( + renderWithContext( , + initialState, ); // Change type to private @@ -261,8 +257,9 @@ describe('components/new_channel_modal', () => { test('should handle purpose changes', () => { const value = 'Purpose'; - render( + renderWithContext( , + 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( , + 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( , + initialState, ); // Change display name @@ -333,8 +332,9 @@ describe('components/new_channel_modal', () => { }); test('should disable confirm button when url in error', () => { - render( + renderWithContext( , + initialState, ); // Change display name @@ -369,8 +369,9 @@ describe('components/new_channel_modal', () => { }); test('should disable confirm button when server error', async () => { - render( + renderWithContext( , + initialState, ); // Confirm button should be disabled @@ -406,6 +407,7 @@ describe('components/new_channel_modal', () => { renderWithContext( , + initialState, ); // Confirm button should be disabled diff --git a/webapp/channels/src/components/overlay_trigger.test.tsx b/webapp/channels/src/components/overlay_trigger.test.tsx index a989667aec..2f7899ff07 100644 --- a/webapp/channels/src/components/overlay_trigger.test.tsx +++ b/webapp/channels/src/components/overlay_trigger.test.tsx @@ -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( - - - - - , + + + + + + + , ); // 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( - - - - - , + + + + + + + , ); const overlay = mount(wrapper.find(BaseOverlayTrigger).prop('overlay')); @@ -79,11 +89,13 @@ describe('OverlayTrigger', () => { }; const wrapper = mountWithIntl( - - - - - , + + + + + + + , ); expect(ref.current).toBe(wrapper.find(BaseOverlayTrigger).instance()); @@ -104,11 +116,13 @@ describe('OverlayTrigger', () => { }; const wrapper = mount( - - - - - , + + + + + + + , ); // Dive into the react-bootstrap internals to find our overlay @@ -143,11 +157,13 @@ describe('OverlayTrigger', () => { }; const wrapper = mount( - - - - - , + + + + + + + , ); // Dive into the react-bootstrap internals to find our overlay diff --git a/webapp/channels/src/components/overlay_trigger.tsx b/webapp/channels/src/components/overlay_trigger.tsx index b6a743536c..8ac6782048 100644 --- a/webapp/channels/src/components/overlay_trigger.tsx +++ b/webapp/channels/src/components/overlay_trigger.tsx @@ -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) => { 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}) => ( diff --git a/webapp/channels/src/components/post_emoji/post_emoji.test.tsx b/webapp/channels/src/components/post_emoji/post_emoji.test.tsx index 404cc84773..a8a4b24b60 100644 --- a/webapp/channels/src/components/post_emoji/post_emoji.test.tsx +++ b/webapp/channels/src/components/post_emoji/post_emoji.test.tsx @@ -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(); + renderWithContext(); 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(); + renderWithContext(); expect(screen.queryByTestId('postEmoji.:' + baseProps.name + ':')).toHaveTextContent(`:${baseProps.name}:`); }); @@ -32,7 +32,7 @@ describe('PostEmoji', () => { imageUrl: '', }; - render(); + renderWithContext(); expect(screen.queryByTestId('postEmoji.:' + baseProps.name + ':')).not.toBeInTheDocument(); expect(screen.getByText(`:${props.name}:`)).toBeInTheDocument(); diff --git a/webapp/channels/src/components/post_profile_picture/post_profile_picture.test.tsx b/webapp/channels/src/components/post_profile_picture/post_profile_picture.test.tsx index 2c9d6c9db5..dab1165707 100644 --- a/webapp/channels/src/components/post_profile_picture/post_profile_picture.test.tsx +++ b/webapp/channels/src/components/post_profile_picture/post_profile_picture.test.tsx @@ -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( , ); @@ -47,7 +47,7 @@ describe('components/PostProfilePicture', () => { status: 'away', postIconOverrideURL: 'http://example.com/image.png', }; - render( + renderWithContext( , ); diff --git a/webapp/channels/src/components/quick_input/__snapshots__/quick_input.test.tsx.snap b/webapp/channels/src/components/quick_input/__snapshots__/quick_input.test.tsx.snap deleted file mode 100644 index 45cd24a8f3..0000000000 --- a/webapp/channels/src/components/quick_input/__snapshots__/quick_input.test.tsx.snap +++ /dev/null @@ -1,194 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/QuickInput should render clear button with customized tooltip component 1`] = ` -
- - - Custom - - - } - placement="bottom" - trigger={ - Array [ - "hover", - "focus", - ] - } - > - - - Custom - - - } - placement="bottom" - trigger={ - Array [ - "hover", - "focus", - ] - } - > - - - -
-`; - -exports[`components/QuickInput should render clear button with customized tooltip text 1`] = ` -
- - Custom - - } - placement="bottom" - trigger={ - Array [ - "hover", - "focus", - ] - } - > - - Custom - - } - placement="bottom" - trigger={ - Array [ - "hover", - "focus", - ] - } - > - - - -
-`; - -exports[`components/QuickInput should render clear button with default tooltip text 1`] = ` -
- - - - } - placement="bottom" - trigger={ - Array [ - "hover", - "focus", - ] - } - > - - - - } - placement="bottom" - trigger={ - Array [ - "hover", - "focus", - ] - } - > - - - -
-`; diff --git a/webapp/channels/src/components/quick_input/quick_input.test.tsx b/webapp/channels/src/components/quick_input/quick_input.test.tsx index bce31ac42d..7e5f274b49 100644 --- a/webapp/channels/src/components/quick_input/quick_input.test.tsx +++ b/webapp/channels/src/components/quick_input/quick_input.test.tsx @@ -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( , ); - 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( { />, ); - expect(wrapper.find('.input-clear')).toMatchSnapshot(); + expect(screen.queryByTestId('input-clear')).toBeInTheDocument(); }); test('with customized tooltip text', () => { - const wrapper = mount( + renderWithContext( { />, ); - expect(wrapper.find('.input-clear')).toMatchSnapshot(); + expect(screen.queryByTestId('input-clear')).toBeInTheDocument(); }); test('with customized tooltip component', () => { - const wrapper = mount( + renderWithContext( { />, ); - expect(wrapper.find('.input-clear')).toMatchSnapshot(); + expect(screen.queryByTestId('input-clear')).toBeInTheDocument(); }); }); @@ -71,7 +72,7 @@ describe('components/QuickInput', () => { return
; } } - const wrapper = mount( + const {rerender} = renderWithContext( { />, ); - 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( + {}} + inputComponent={MockComp} + />, + ); + + expect(screen.queryByTestId('input-clear')).not.toBeInTheDocument(); expect(focusFn).toBeCalled(); }); }); diff --git a/webapp/channels/src/components/quick_input/quick_input.tsx b/webapp/channels/src/components/quick_input/quick_input.tsx index 8bc94b6544..36fffe78e5 100644 --- a/webapp/channels/src/components/quick_input/quick_input.tsx +++ b/webapp/channels/src/components/quick_input/quick_input.tsx @@ -200,9 +200,11 @@ export class QuickInput extends React.PureComponent { {inputElement} {showClearButton &&
+
+ - - -
- - +
+
+ - - -
- - +
+
+ - - -
- - + +
+ - - -
-
- + +
+ - - -
-
- + +
+ - - -
-
- + +
+ - - -
-
- + +
+ - - -
-
- + +
+ - - -
-
- + +
+ - - -
-
- + +
+ - - -
-
- + +
+ - - -
-
- + +
+ - - -
-
- + + + `; + +exports[`plugins/ChannelHeaderPlug should not render anything when the App Bar is visible 1`] = ``; + +exports[`plugins/ChannelHeaderPlug should not render anything with no extended component 1`] = ``; diff --git a/webapp/channels/src/plugins/channel_header_plug/channel_header_plug.test.tsx b/webapp/channels/src/plugins/channel_header_plug/channel_header_plug.test.tsx index 5e42cbda61..1c96874c9b 100644 --- a/webapp/channels/src/plugins/channel_header_plug/channel_header_plug.test.tsx +++ b/webapp/channels/src/plugins/channel_header_plug/channel_header_plug.test.tsx @@ -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( { shouldShowAppBar={false} />, ); - expect(wrapper).toMatchSnapshot(); + expect(asFragment()).toMatchSnapshot(); }); test('should match snapshot with one extended component', () => { - const wrapper = mountWithIntl( + const {asFragment} = renderWithContext( { shouldShowAppBar={false} />, ); - expect(wrapper).toMatchSnapshot(); + expect(asFragment()).toMatchSnapshot(); }); test('should match snapshot with six extended components', () => { - const wrapper = mountWithIntl( + const {asFragment} = renderWithContext( { 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( { shouldShowAppBar={true} />, ); - expect(wrapper).toMatchSnapshot(); + expect(asFragment()).toMatchSnapshot(); }); }); diff --git a/webapp/channels/src/utils/utils.tsx b/webapp/channels/src/utils/utils.tsx index 4bb6080930..0dc90735de 100644 --- a/webapp/channels/src/utils/utils.tsx +++ b/webapp/channels/src/utils/utils.tsx @@ -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 = { 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); diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 631d84c42b..a0b8abdfb2 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -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,