From 535d93ee985948c971bfec94c2cff75f37c8c479 Mon Sep 17 00:00:00 2001
From: Harrison Healey
Date: Tue, 7 Oct 2025 11:11:12 -0400
Subject: [PATCH] MM-45255 Update web app to React 18 (#33858)
* Dependencies: Remove unused dependency on @mattermost/dynamic-virtualized-list
* Update Components package to React 18 and new version of RTL
* Upgrade React, React Redux, RTL, and associated libraries
I had to upgrade React Redux for the new version of React, and that
brought with it new versions of associated packges (Redux, Reselect,
Redux Thunk). A few other libraries needed to be updated or have their
explicit dependencies overridden for the new version of Redux as well.
To note for future dependency upgrades, redux-mock-store is deprecated,
and redux-batched-actions and redux-persist are inactive.
For RTL:
1. `@testing-library/react-hooks` has been rolled into
`@testing-library/react`, and its interface has changed.
2. I had to make some changes to get TS to use the types for the new
methods added to `expect`.
* Dependencies: Fix dom-accessibility-api patch from #33553
* Tests: Fix tests that use jest.spyOn with react-redux
* Functional: Remove usage of defaultProps on function components
* Tests: Remove usage of react-dom/test-utils
* Functional: Rename conflicting context prop on Apps components
* Tests: Always await on userEvent calls
* Functional: Patch react-overlays to fix pressing escape in unit tests
I did something similar in React Bootstrap a few weeks ago.
See https://github.com/mattermost/react-bootstrap/pull/5
* Tests: Prevent tests from fetching from real URLs
* Tests: Update snapshots changed by upgrading react-redux and styled-components
* Functional: Stop passing deprecated pure parameter to connect
* Tests: Change how we intercept console errors in tests to preserve stack traces
* Tests: Fix incorrect usage of act in Enzyme tests
These tests are a mix of:
1. Not calling act when performing something that will update the DOM (like clicking on a button or invoking a method) which either caused warnings or failed snapshots as changes weren't visible.
2. Calling act in weird ways (such as wrapping mount in an async act) which caused Enzyme to not function
Some of these changes just silence warnings, but most of them are required to make the test even run
* Tests: Fix incorrect usage of act in RTL tests
* Tests: Fix a few minor issues in tests
* Functional: Add note for why we're not using ReactDOM.createRoot
* Functional: Fix focus trap infinite recursion in test
* Types: Replace removed React.SFC
* Types: Fix type of functions in FormattedMessage values prop
* Functional: Fix DropdownInputHybrid placeholder
* Types: Patch type definitions of react-batched-actions
* Types: Fix mattermost-redux build failing due to type check in Redux Dev Tools
* Dependencies: Add type definitions for react-is
* Types: Update types around ReactNode and ReactElement
React's gotten more strict with these, so we need to be more careful with what
we return from a render method. In some of these places, we also misused some
types, so hopefully I've sorted those out.
* Types: Explicitly added types to all instances of useCallback
* Types/Tests: Update typing of Redux actions and hooks
useDispatch is sort of stricter now, but it doesn't seem to rely on the global type of `Dispatch` any more, so I ended up having to add an extra overload to make that work.
* Tests: Update new tests in useChannelSystemPolicies for new version of RTL
These were added on master after I updated RTL on the branch
* Tests: Update action used to test initial store state
* Functional: Remove remnants of code for hot reloading Redux store
* Types/Tests: Update typing around React Router
* Types/Functional: Update typing involving the FormattedMessage values prop
There's a couple functional changes to ensure that the value passed is either a valid string or Date
* Types: Misc fixes
* Functional: Don't pass unused props to ChannelHeader
* Functional: Ensure plugin setting text is rendered before passing to Setting component
The previous version might've allowed MessageDescriptors to be passed unformatted
into the Setting component (which would then be rendered in the DOM). As best as
I can tell, we never actually did that, so this shouldn't change anything in practice.
* Tests: Make tests for identifyElementRegion more reliable
* Tests: Update recent tests for new version of RTL
* E2E: Make editLastPostWithNewMessage more reliable
* Downgrade React to 18.2.0 and manually dedupe React versions
18.2.0 and 18.3.0 are nearly identical to one another, except 18.3.x includes
warnings when using any API that will be removed in React 19. I don't want to
flood the console and test logs with warnings for things we're not addressing
for the time being.
Also, they didn't export act from React itself until 18.3.1 for some reason
(despite the old import path printing a warning), so I needed to revert the
changes to its import path.
To get this all to work, for some reason, I had to manually delete all the
entries for `react` and `react-dom` from the lockfile to get NPM to use a
single version of React and ReactDOM everywhere. I did discover `npm dedupe`
in the process, but that didn't solve this problem where I was trying to
consistently downgrade everything.
* Revert changes to notice file build tool to speed up CI
* Add explicit version of types/scheduler
The version of `@types/react` that we use says it works with any version of
`@types/scheduler` which causes NPM to install a newer version of it which
is missing a file of types that it needs.
* Update new test to await on userEvent
* Fix Playwright test that relied on autogenerated class name
* Tests: Disable test for identifyElementRegion
* Functional: Change DynamicVirtualizedList ListItem to use useLayoutEffect
In a previous commit, I changed the RHS to use the monorepo
DynamicVirtualizedList instead of the old version that lived in its own repo.
That caused the RHS to no longer scroll to the bottom on initial mount or be
able to render additional pages (even though the posts are loaded). This seems
like it has to do with the improved size calculation that Zubair made because
that's the main difference in the monorepo version of that component.
For some reason I don't entirely understand, changing to useLayoutEffect seems
to fix that. I think that's because the old measurement code is written as a
class component, and useLayoutEffect fires at the same time as
componentDidMount/componentDidUpdate.
* Types: Revert some type changes to reduce test log output
* Functional: Fix places where useSelector returned new results when called with the same arguments
I wasn't planning on fixing this now, but I think the increased length of the warning logs in the tests are causing
the GitHub action for the unit tests to abort as it reaches around 10000 lines long.
* Tests: Fix place where mocked selector returned new results when called with the same arguments
Same reason as before, but this one only occurred because of a test's mocked selector. I replaced it with
a real one to get around that.
* Tests: Fail tests when selector returns new results when called with the same arguments
* Attempt to fix web app unit tests failing in CI
* Change CI tests to set workerIdleMemoryLimit instead of reducing maxWorkers
* Increase workerIdleMemoryLimit in CI
* Revert changes to test-ci command and revert changes to how unit test logs are collected
* Unrevert changes to test logging, re-add workerIdleMemoryLimit, and try disabling test coverage
* Actually disable coverage
* Fix flaky test
* Update a couple new tests to fit PR and remove an unnecessary act
* Replace bad mock in new unit test
* Fix types of new code
* Remove mock from new unit test
---
e2e-tests/cypress/tests/support/ui/post.ts | 2 +-
.../components/channels/message_priority.ts | 12 +-
.../standard_priority.spec.ts | 2 +-
webapp/channels/package.json | 42 +-
webapp/channels/src/actions/apps.ts | 2 +-
.../about_build_modal.test.tsx | 6 +-
.../components/actions_menu/actions_menu.tsx | 15 +-
.../components/actions_menu/index.test.tsx | 18 +-
.../modals/policy_test/test_modal.tsx | 2 +-
.../modals/user_sync/synced_user_list.tsx | 2 +-
.../access_control/policies.test.tsx | 3 +-
.../policy_details/policy_details.test.tsx | 2 +-
.../menu_item_blockable_link.tsx | 2 +-
.../admin_console/admin_sidebar/index.ts | 2 +-
.../cloud_trial_banner.test.tsx.snap | 75 +-
.../billing_subscriptions.tsx | 4 +-
.../__snapshots__/feature_list.test.tsx.snap | 100 +-
.../user_multiselector/user_multiselector.tsx | 5 +-
..._enable_disable_guest_accounts_setting.tsx | 2 +-
.../custom_profile_attributes.test.tsx | 3 +-
.../custom_profile_attributes.tsx | 6 +-
.../custom_terms_of_service_settings.tsx | 2 +-
.../group_details/group_users.tsx | 2 +-
.../admin_console/ip_filtering/index.tsx | 6 +-
.../admin_console/jobs/job_cancel_button.tsx | 2 +-
.../ldap_wizard/ldap_custom_setting.tsx | 4 +-
.../admin_console/ldap_wizard/ldap_wizard.tsx | 2 +-
...onfirm_license_removal_modal.test.tsx.snap | 25 +-
.../upload_license_modal.test.tsx.snap | 50 +-
.../modals/upload_license_modal.test.tsx | 2 +-
.../renew_license_card.test.tsx | 2 +-
.../renew_license_card/renew_license_card.tsx | 4 +-
.../team_edition/team_edition_right_panel.tsx | 2 +-
.../trial_banner/trial_banner.tsx | 4 +-
.../trial_license_card/trial_license_card.tsx | 4 +-
.../manage_teams_modal.test.tsx | 2 +-
.../admin_console/message_export_settings.tsx | 2 +-
.../permission_description.test.tsx.snap | 25 +-
.../edit_post_time_limit_modal.tsx | 2 +-
.../permission_description.tsx | 5 +-
.../permission_group.tsx | 2 +-
.../permission_row.tsx | 3 +-
.../permissions_tree/types.ts | 6 +-
.../admin_console/schema_admin_settings.tsx | 4 +-
.../modals/secure_connection_delete_modal.tsx | 2 +-
.../admin_console/secure_connections/utils.ts | 4 +-
.../admin_console/server_logs/logs.test.tsx | 4 +-
.../system_properties/section_utils.test.ts | 13 +-
.../system_properties.test.tsx | 8 +-
.../user_properties_utils.test.ts | 20 +-
.../system_role/system_role_permissions.tsx | 4 +-
.../system_user_detail.test.tsx.snap | 12 -
.../system_user_detail.test.tsx | 25 +-
.../channel/details/channel_modes.tsx | 8 +-
.../team/details/team_modes.tsx | 2 +-
.../team/details/team_profile.tsx | 4 +-
.../users_to_be_removed_modal.tsx | 2 +-
.../advanced_text_editor.test.tsx | 113 +-
.../advanced_text_editor/edit_post_footer.tsx | 2 +-
.../formatting_bar/formatting_bar.test.tsx | 8 +-
.../formatting_bar/formatting_bar.tsx | 4 +-
.../send_post_options/core_menu_options.tsx | 4 +-
.../recent_used_custom_date.tsx | 2 +-
.../advanced_text_editor/use_submit.tsx | 5 +-
.../system_analytics.test.tsx | 11 +-
.../configuration_bar/configuration_bar.tsx | 6 +-
.../no_internet_connection.tsx | 2 +-
.../index.test.tsx | 6 +-
.../renewal_link/renewal_link.test.tsx | 2 +-
.../apps_form/apps_form_component.test.tsx | 14 +-
.../apps_form/apps_form_container.test.tsx | 4 +-
.../apps_form/apps_form_container.tsx | 16 +-
webapp/channels/src/components/async_load.tsx | 4 +-
.../notification_from_members_modal.tsx | 14 +-
.../src/components/authorize/authorize.tsx | 6 +-
.../src/components/autocomplete_selector.tsx | 3 -
.../bookmark_delete_modal.tsx | 2 +-
.../create_modal_name_input.tsx | 3 +-
.../components/header_icon_wrapper.test.tsx | 4 +-
.../src/components/channel_header/index.ts | 3 +-
.../channel_invite_modal.test.tsx | 164 +-
.../channel_invite_modal.tsx | 2 +-
.../group_option/group_option.tsx | 6 +-
.../channel_identifier_router/actions.test.ts | 26 +-
.../channel_identifier_router.test.tsx | 11 +-
.../channel_identifier_router.tsx | 21 +-
.../channel_members_dropdown.tsx | 3 +-
.../channel_members_rhs/action_bar.tsx | 2 +-
.../channel_members_rhs/member.test.tsx | 20 +-
.../channel_members_rhs/member_list.tsx | 6 +-
.../channel_notifications_modal.test.tsx | 62 +-
.../channel_selector_modal.tsx | 2 +-
...channel_settings_access_rules_tab.test.tsx | 136 +-
.../channel_settings_archive_tab.tsx | 2 +-
...hannel_settings_configuration_tab.test.tsx | 166 +-
.../channel_settings_info_tab.test.tsx | 48 +-
.../channel_settings_modal.test.tsx | 254 ++-
.../__snapshots__/channel_view.test.tsx.snap | 75 +-
.../components/channel_view/channel_view.tsx | 8 +-
.../cloud_invoice_preview/index.tsx | 2 +-
.../__snapshots__/error.test.tsx.snap | 2 -
.../__snapshots__/success.test.tsx.snap | 4 -
.../cloud_usage_modal/index.test.tsx | 10 +-
.../workspace_limits_panel.tsx | 6 +-
.../commercial_support_modal.tsx | 6 +-
.../src/components/common/back_button.tsx | 7 +-
.../hooks/useAccessControlAttributes.test.tsx | 46 +-
.../common/hooks/useChannel.test.ts | 30 +-
...sePluginVisibilityInSharedChannel.test.tsx | 2 +-
.../components/common/hooks/usePost.test.ts | 35 +-
.../common/hooks/usePrefixedIds.test.ts | 39 +-
.../components/common/hooks/useUser.test.ts | 34 +-
.../components/common/hooks/use_team.test.ts | 34 +-
.../common/infinite_scroll.test.tsx | 32 +-
.../src/components/common/scrollbars.tsx | 4 +-
.../select_text_input/select_text_input.tsx | 6 +-
.../confirm_modal_redux.test.tsx | 37 +-
.../confirm_modal_redux.tsx | 4 +-
.../convert_channel_modal.test.tsx | 4 +-
.../convert_channel_modal.tsx | 4 +-
.../convert_gm_to_channel_modal.test.tsx | 3 +-
.../team_selector/team_selector.tsx | 5 +-
.../components/team_url/team_url.test.tsx | 51 +-
.../custom_status_text.test.tsx.snap | 8 +-
.../custom_status_modal.test.tsx | 12 +-
.../custom_status/custom_status_text.tsx | 12 +-
.../components/custom_status/expiry_time.tsx | 13 +-
.../datetime_input/datetime_input.test.tsx | 33 +-
.../delete_category_modal.tsx | 2 +-
.../delete_channel_modal.tsx | 2 +-
.../interactive_dialog_adapter.tsx | 2 +-
.../__snapshots__/draft_row.test.tsx.snap | 50 +-
.../delete_draft_modal.test.tsx.snap | 25 +-
.../__snapshots__/draft_actions.test.tsx.snap | 25 +-
.../send_draft_modal.test.tsx.snap | 25 +-
.../draft_actions/delete_draft_modal.tsx | 2 +-
.../delete_scheduled_post_modal.tsx | 2 +-
.../drafts/draft_actions/send_draft_modal.tsx | 2 +-
.../drafts/drafts_and_schedule_posts_tabs.tsx | 4 +-
.../components/drafts/panel/panel.test.tsx | 5 +-
.../dynamic_virtualized_list/list_item.tsx | 10 +-
.../edit_scheduled_post/edit_post_footer.tsx | 2 +-
.../src/components/emoji/render_emoji.tsx | 15 +-
.../components/emoji_picker_category.test.tsx | 13 +-
.../src/components/error_page/error_link.tsx | 9 +-
.../file_preview_modal.test.tsx.snap | 3 -
.../file_preview_modal_main_actions.test.tsx | 8 +-
.../file_preview_modal_main_actions.tsx | 44 +-
.../flag_post_modal.test.tsx | 14 +-
.../components/forward_post_modal/index.tsx | 2 +-
.../src/components/get_link_modal.test.tsx | 2 +-
.../get_public_link_modal.test.tsx | 2 +-
.../global_search_nav.test.tsx.snap | 50 +-
.../user_guide_dropdown/index.ts | 3 +-
.../user_guide_dropdown.tsx | 6 +-
.../product_menu_list.test.tsx | 4 +-
.../header_footer_route.tsx | 6 +-
.../integrations/abstract_command.tsx | 2 +-
.../add_incoming_webhook.test.tsx | 14 +-
.../src/components/integrations/bots/bots.tsx | 4 +-
.../confirm_integration.tsx | 42 +-
.../installed_commands/installed_commands.tsx | 2 +-
.../installed_outgoing_webhooks.tsx | 2 +-
...th_connection_audience_input.test.tsx.snap | 102 +-
...bstract_outgoing_oauth_connection.test.tsx | 2 +-
.../abstract_outgoing_oauth_connection.tsx | 4 +-
...talled_outgoing_oauth_connections.test.tsx | 22 +-
.../oauth_connection_audience_input.test.tsx | 113 +-
.../__snapshots__/invite_as.test.tsx.snap | 25 +-
.../invitation_modal/add_to_channels.tsx | 4 +-
.../invitation_modal.test.tsx | 44 +-
.../invitation_modal/invite_view.test.tsx | 44 +-
.../latex_inline/latex_inline.test.tsx | 3 +-
.../learn_more_trial_modal.test.tsx.snap | 25 +-
.../learn_more_trial_modal_step.test.tsx.snap | 75 +-
.../start_trial_btn.test.tsx.snap | 25 +-
.../start_trial_btn.test.tsx | 70 +-
.../linking_landing_page.tsx | 2 +-
.../src/components/loading_image_preview.tsx | 10 +-
.../src/components/login/login.test.tsx | 12 +-
.../src/components/menu/menu.test.tsx | 41 +-
.../src/components/menu/menu_item_link.tsx | 2 +-
.../src/components/mfa/setup/setup.test.tsx | 23 +-
.../user_details/user_details.test.tsx | 20 +-
.../move_thread_modal/move_thread_modal.tsx | 2 +-
.../multiselect/multiselect.test.tsx | 1 -
.../components/multiselect/multiselect.tsx | 2 +-
.../multiselect/multiselect_list.tsx | 4 +-
.../new_channel_modal.test.tsx | 70 +-
.../new_replies_banner/new_replies_banner.tsx | 2 +-
.../components/new_search/new_search.test.tsx | 76 +-
.../components/new_search/search_box.test.tsx | 13 +-
.../new_search/search_box_suggestions.tsx | 2 +-
.../src/components/notify_confirm_modal.tsx | 12 +-
.../password_reset_send_link.test.tsx | 7 +-
.../permalink_view/permalink_view.test.tsx | 53 +-
.../persist_notification_confirm_modal.tsx | 4 +-
.../plugin_link_tooltip/index.test.tsx | 20 +-
.../marketplace_item_plugin.tsx | 4 +-
.../plugin_marketplace/marketplace_modal.tsx | 2 +-
.../components/post/post_component.test.tsx | 18 +-
.../src/components/post/post_component.tsx | 2 +-
.../edited_post_item/edited_post_item.tsx | 2 +-
.../post_edit_history.test.tsx | 14 +-
.../post_markdown/system_message_helpers.tsx | 10 +-
.../last_users.test.tsx | 8 +-
.../data_spillage_report.test.tsx | 6 +-
.../button_binding/button_binding.test.tsx | 4 +-
.../failed_post_options.test.tsx | 10 +-
.../action_button/action_button.test.tsx | 4 +-
.../action_button/action_button.tsx | 2 +-
.../post_attachment_container.tsx | 11 +-
.../post_reaction/post_reaction.test.tsx | 2 +-
.../post_view/post_time/post_time.test.tsx | 46 +-
.../add_reaction_button.test.tsx | 4 +-
.../profile_popover/profile_popover.test.tsx | 31 +-
.../selectPropertyRenderer.tsx | 2 +-
.../textPropertyRenderer.tsx | 2 +-
.../purchase_modal/icon_message.tsx | 11 +-
.../query_param_action_controller.test.tsx | 4 +-
.../quick_input/quick_input.test.tsx | 4 +-
.../quick_switch_modal.test.tsx | 9 +-
.../quick_switch_modal/quick_switch_modal.tsx | 2 +-
.../root/performance_reporter_controller.tsx | 4 +-
.../src/components/root/root.test.tsx | 15 +-
.../index.test.tsx | 6 +-
.../channels/src/components/search/search.tsx | 18 +-
.../__snapshots__/search_bar.test.tsx.snap | 3 -
.../src/components/search_bar/search_bar.tsx | 18 +-
.../messages_or_files_selector.test.tsx.snap | 75 +-
.../search_results/search_results.tsx | 13 +-
.../searchable_user_list.tsx | 2 +-
.../searchable_user_list_container.tsx | 2 +-
.../__snapshots__/select_team.test.tsx.snap | 25 +-
.../src/components/select_team/index.ts | 9 +-
.../components/select_team/select_team.tsx | 9 +-
.../src/components/setting_item_max.test.tsx | 24 +-
.../shared_channel_indicator.test.tsx | 70 +-
.../components/shared_user_indicator.test.tsx | 70 +-
.../sidebar_category.test.tsx.snap | 10 -
.../sidebar/sidebar_category_header.tsx | 36 +-
.../sidebar_header/sidebar_team_menu.test.tsx | 14 +-
.../unread_channel_indicator.tsx | 24 +-
.../channels/src/components/signup/signup.tsx | 4 +-
.../src/components/size_aware_image.test.tsx | 19 +-
.../start_trial_form_modal/index.tsx | 2 +-
.../start_trial_form_modal.test.tsx | 58 +-
.../suggestion_box/suggestion_box.test.tsx | 16 +-
.../components/suggestion/suggestion_list.tsx | 2 +-
.../team_groups_manage_modal.test.tsx | 18 +-
.../allowed_domains_select.tsx | 2 +-
.../team_access_tab/team_access_tab.test.tsx | 17 +-
.../team_info_tab/team_info_tab.test.tsx | 78 +-
.../team_settings_modal.test.tsx | 4 +-
.../thread_footer/thread_footer.tsx | 4 +-
.../thread_item/thread_item.tsx | 2 +-
.../thread_list/thread_list.tsx | 2 +-
.../thread_list/virtualized_thread_list.tsx | 11 +-
.../global_threads_link.tsx | 2 +-
.../create_comment.tsx | 4 +-
.../virtualized_thread_viewer.tsx | 10 +-
.../collapsed_reply_threads_modal.tsx | 2 +-
.../crt_threads_pane_tutorial_tip.tsx | 2 +-
.../crt_tour/crt_unread_tutorial_tip.tsx | 2 +-
.../channels_and_direct_messages_tour_tip.tsx | 4 +-
.../trial_benefits_modal.test.tsx.snap | 50 +-
.../trial_benefits_modal.tsx | 6 +-
.../unarchive_channel_modal.tsx | 2 +-
.../unreads_status_handler/index.ts | 15 +-
.../user_account_name_menuitem.test.tsx.snap | 2 +-
.../user_account_name_menuitem.test.tsx | 2 +-
...er_account_out_of_office_menuitem.test.tsx | 2 +-
webapp/channels/src/components/user_list.tsx | 3 +-
.../render_emoticons_as_emoji.test.tsx | 4 +-
.../display/user_settings_display.test.tsx | 87 +-
.../general/user_settings_general.test.tsx | 60 +-
.../general/user_settings_general.tsx | 2 +-
.../email_notification_setting.test.tsx | 9 +-
.../user_settings_notifications.tsx | 2 +-
.../plugin/plugin_setting.test.tsx | 10 +-
.../security/mfa_section/mfa_section.test.tsx | 17 +-
.../user_access_token_section.tsx | 2 +-
.../admin_console/admin_panel_togglable.tsx | 9 +-
.../admin_console/admin_panel_with_button.tsx | 8 +-
.../widgets/inputs/dropdown_input_hybrid.tsx | 4 +-
.../widgets/inputs/input/input.test.tsx | 17 +-
.../__snapshots__/upgrade_link.test.tsx.snap | 25 +-
.../menu/menu_items/menu_cloud_trial.tsx | 4 +-
.../menu/menu_items/submenu_item.test.tsx | 8 +-
.../widgets/menu/menu_items/useWords.tsx | 5 +-
.../submenu_modal/submenu_modal.test.tsx | 6 +-
.../modals/components/save_changes_panel.tsx | 1 -
.../widgets/modals/full_screen_modal.tsx | 7 +-
.../with_error_boundary/index.test.tsx | 10 +-
.../components/with_tooltip/index.test.tsx | 42 +-
webapp/channels/src/entry.tsx | 8 +-
.../hooks/useChannelSystemPolicies.test.ts | 68 +-
.../src/reducers/entities/admin.test.ts | 6 +-
.../src/reducers/entities/cloud.test.ts | 4 +-
.../src/reducers/entities/general.test.ts | 4 +-
.../src/reducers/entities/groups.test.ts | 2 +-
.../src/reducers/entities/search.test.ts | 6 +-
.../src/reducers/entities/teams.test.ts | 6 +-
.../src/reducers/entities/typing.test.ts | 2 +-
.../src/reducers/entities/users.test.ts | 6 +-
.../selectors/entities/channel_banner.test.ts | 3 +-
.../entities/content_flagging.test.ts | 3 +-
.../src/selectors/entities/files.ts | 21 +-
.../src/selectors/entities/users.ts | 19 +-
.../src/store/configureStore.ts | 20 +-
.../mattermost-redux/src/types/actions.ts | 5 +-
.../src/types/extend_react_redux.d.ts | 10 +
.../src/types/extend_redux.d.ts | 50 +
webapp/channels/src/plugins/registry.ts | 9 +-
webapp/channels/src/reducers/storage.test.ts | 4 +-
.../channels/src/reducers/views/admin.test.ts | 2 +-
.../channels/src/reducers/views/lhs.test.ts | 2 +-
.../src/reducers/views/marketplace.test.ts | 2 +-
.../src/reducers/views/modals.test.tsx | 2 +-
.../src/reducers/views/rhs_suppressed.test.ts | 2 +-
webapp/channels/src/root.tsx | 2 +
webapp/channels/src/store/index.ts | 5 -
webapp/channels/src/stores/hooks.ts | 2 +-
.../src/tests/react_testing_utils.test.tsx | 8 +-
.../src/tests/react_testing_utils.tsx | 12 +-
webapp/channels/src/tests/setup_jest.ts | 98 +-
webapp/channels/src/tests/test_store.tsx | 4 +-
.../external/dynamic-virtualized-list.d.ts | 50 -
webapp/channels/src/types/store/plugins.ts | 2 +-
.../element_identification.test.tsx | 31 +-
.../plugin_setting_extraction.test.tsx | 2 +-
.../plugins/plugin_setting_extraction.tsx | 4 +-
webapp/channels/src/utils/utils.tsx | 8 +-
webapp/channels/tsconfig.json | 1 +
webapp/package-lock.json | 1728 ++++++-----------
webapp/package.json | 49 +-
...atch => dom-accessibility-api+0.6.3.patch} | 8 +-
.../patches/react-beautiful-dnd+13.1.1.patch | 109 ++
.../patches/redux-batched-actions+0.5.0.patch | 22 +
webapp/platform/components/package.json | 25 +-
webapp/platform/components/rollup.config.js | 1 +
webapp/platform/components/tsconfig.json | 3 +-
webapp/platform/mattermost-redux/package.json | 8 +-
.../platform/mattermost-redux/tsconfig.json | 1 +
344 files changed, 3259 insertions(+), 4192 deletions(-)
create mode 100644 webapp/channels/src/packages/mattermost-redux/src/types/extend_react_redux.d.ts
create mode 100644 webapp/channels/src/packages/mattermost-redux/src/types/extend_redux.d.ts
delete mode 100644 webapp/channels/src/types/external/dynamic-virtualized-list.d.ts
rename webapp/patches/{dom-accessibility-api+0.5.16.patch => dom-accessibility-api+0.6.3.patch} (93%)
create mode 100644 webapp/patches/react-beautiful-dnd+13.1.1.patch
create mode 100644 webapp/patches/redux-batched-actions+0.5.0.patch
diff --git a/e2e-tests/cypress/tests/support/ui/post.ts b/e2e-tests/cypress/tests/support/ui/post.ts
index 50d5547dd49..dbbc7485644 100644
--- a/e2e-tests/cypress/tests/support/ui/post.ts
+++ b/e2e-tests/cypress/tests/support/ui/post.ts
@@ -71,7 +71,7 @@ function editLastPostWithNewMessage(message: string) {
// # Update the post message and click Save
cy.get('#edit_textbox').clear().type(message)
- cy.get('#create_post').findByText('Save').should('be.visible').click();
+ cy.get('#create_post').findByText('Save').scrollIntoView().click();
}
Cypress.Commands.add('editLastPostWithNewMessage', editLastPostWithNewMessage);
diff --git a/e2e-tests/playwright/lib/src/ui/components/channels/message_priority.ts b/e2e-tests/playwright/lib/src/ui/components/channels/message_priority.ts
index 29994083bd7..8f6f56d1235 100644
--- a/e2e-tests/playwright/lib/src/ui/components/channels/message_priority.ts
+++ b/e2e-tests/playwright/lib/src/ui/components/channels/message_priority.ts
@@ -20,12 +20,12 @@ export default class MessagePriority {
// Priority menu that opens when clicking the icon
this.priorityMenu = container.locator('[role="menu"]');
- // Standard priority option in the menu (id comes from webapp implementation)
- this.standardPriorityOption = this.priorityMenu.locator('#menu-item-priority-standard');
-
// Priority dialog elements
this.priorityDialog = container.page().getByRole('menu');
this.dialogHeader = container.page().locator('h4.modal-title');
+
+ // Standard priority option in the menu
+ this.standardPriorityOption = this.priorityDialog.getByRole('menuitemradio', {name: 'Standard'});
}
async clickPriorityIcon() {
@@ -67,10 +67,4 @@ export default class MessagePriority {
await expect(this.priorityDialog).toBeVisible();
await expect(this.dialogHeader).toHaveText('Message priority');
}
-
- async verifyStandardOptionSelected() {
- const standardOption = this.priorityDialog.getByRole('menuitemradio', {name: 'Standard'});
- await expect(standardOption).toBeVisible();
- await expect(standardOption.locator('svg.StyledCheckIcon-dFKfoY')).toBeVisible();
- }
}
diff --git a/e2e-tests/playwright/specs/functional/channels/message_priority/standard_priority.spec.ts b/e2e-tests/playwright/specs/functional/channels/message_priority/standard_priority.spec.ts
index bf8c5c1803b..b07e9b90e63 100644
--- a/e2e-tests/playwright/specs/functional/channels/message_priority/standard_priority.spec.ts
+++ b/e2e-tests/playwright/specs/functional/channels/message_priority/standard_priority.spec.ts
@@ -25,7 +25,7 @@ test(
// * Verify priority dialog appears with standard option selected
await channelsPage.messagePriority.verifyPriorityDialog();
- await channelsPage.messagePriority.verifyStandardOptionSelected();
+ await channelsPage.messagePriority.verifyStandardPrioritySelected();
// # Close priority menu
await channelsPage.messagePriority.closePriorityMenu();
diff --git a/webapp/channels/package.json b/webapp/channels/package.json
index 9ee398b6de0..5cdc2673115 100644
--- a/webapp/channels/package.json
+++ b/webapp/channels/package.json
@@ -6,20 +6,21 @@
"version": "10.12.0",
"private": true,
"dependencies": {
- "@floating-ui/react": "0.26.28",
+ "@floating-ui/react": "0.26.6",
"@giphy/js-fetch-api": "5.1.0",
"@giphy/react-components": "8.1.0",
"@guyplusplus/turndown-plugin-gfm": "1.0.7",
"@mattermost/client": "10.12.0",
"@mattermost/desktop-api": "5.10.0-2",
- "@mattermost/dynamic-virtualized-list": "github:mattermost/dynamic-virtualized-list#08dde0c34a12d0384740db27d55e398d139d7a51",
"@mattermost/types": "10.12.0",
"@mui/base": "5.0.0-alpha.127",
"@mui/material": "5.11.16",
"@mui/styled-engine-sc": "5.11.11",
+ "@redux-devtools/extension": "3.3.0",
"@tanstack/react-table": "8.10.7",
"@tippyjs/react": "4.2.6",
"@types/color-hash": "1.0.2",
+ "@types/react-is": "18.2.1",
"@types/turndown": "5.0.5",
"bootstrap": "3.4.1",
"buffer": "6.0.3",
@@ -58,16 +59,16 @@
"pdfjs-dist": "4.4.168",
"process": "0.11.10",
"prop-types": "15.8.1",
- "react": "17.0.2",
+ "react": "18.2.0",
"react-beautiful-dnd": "13.1.1",
- "react-bootstrap": "github:mattermost/react-bootstrap#7d8660f06188a6433bb0f69b5816a97dc9ebe48c",
+ "react-bootstrap": "github:mattermost/react-bootstrap#05559f4c61c5a314783c390d2d82906ee8c7e558",
"react-color": "2.19.3",
"react-day-picker": "8.3.6",
- "react-dom": "17.0.2",
+ "react-dom": "18.2.0",
"react-intl": "*",
- "react-is": "17.0.2",
+ "react-is": "18.2.0",
"react-overlays": "0.9.3",
- "react-redux": "7.2.4",
+ "react-redux": "9.2.0",
"react-router-dom": "5.3.4",
"react-select": "5.9.0",
"react-transition-group": "4.4.5",
@@ -75,10 +76,10 @@
"react-window": "1.8.11",
"react-window-infinite-loader": "1.0.10",
"rebound": "0.1.0",
- "redux": "4.2.0",
+ "redux": "5.0.1",
"redux-batched-actions": "0.5.0",
"redux-persist": "6.0.0",
- "redux-thunk": "2.4.2",
+ "redux-thunk": "3.1.0",
"regenerator-runtime": "0.13.10",
"semver": "7.6.3",
"serialize-error": "11.0.3",
@@ -94,16 +95,16 @@
"zen-observable": "0.10.0"
},
"devDependencies": {
+ "@cfaester/enzyme-adapter-react-18": "0.8.0",
"@deanwhillier/jest-matchmedia-mock": "1.2.0",
"@mattermost/calls-common": "0.27.0",
"@mattermost/eslint-plugin": "*",
"@mattermost/mmjstool": "github:mattermost/mattermost-utilities#7b63833d208d482ba4a1c12230bb3e68dd9c5e5e",
- "@redux-devtools/extension": "3.2.3",
"@stylistic/stylelint-plugin": "3.1.2",
- "@testing-library/jest-dom": "5.16.4",
- "@testing-library/react": "12.1.4",
- "@testing-library/react-hooks": "8.0.1",
- "@testing-library/user-event": "13.5.0",
+ "@testing-library/dom": "10.4.1",
+ "@testing-library/jest-dom": "6.8.0",
+ "@testing-library/react": "16.3.0",
+ "@testing-library/user-event": "14.6.1",
"@types/country-list": "2.1.0",
"@types/enzyme": "3.10.11",
"@types/jest": "28.1.8",
@@ -112,33 +113,32 @@
"@types/luxon": "3.0.2",
"@types/mark.js": "8.11.6",
"@types/marked": "0.7.4",
- "@types/react": "17.0.83",
+ "@types/react": "18.2.64",
"@types/react-beautiful-dnd": "13.1.2",
"@types/react-bootstrap": "0.32.35",
"@types/react-color": "3.0.6",
"@types/react-custom-scrollbars": "4.0.10",
- "@types/react-dom": "17.0.25",
- "@types/react-is": "17.0.2",
+ "@types/react-dom": "18.2.25",
"@types/react-overlays": "1.1.3",
- "@types/react-redux": "7.1.31",
"@types/react-router-dom": "5.3.3",
"@types/react-transition-group": "4.4.5",
"@types/react-virtualized-auto-sizer": "1.0.1",
"@types/react-window": "1.8.5",
"@types/react-window-infinite-loader": "1.0.6",
- "@types/redux-mock-store": "1.0.3",
+ "@types/redux-mock-store": "1.5.0",
"@types/regenerator-runtime": "0.13.8",
+ "@types/scheduler": "0.16.8",
"@types/semver": "7.5.8",
"@types/shallow-equals": "1.0.3",
"@types/styled-components": "5.1.32",
"@types/tinycolor2": "1.4.6",
"@types/zen-observable": "0.8.7",
+ "babel-plugin-styled-components": "2.1.4",
"copy-webpack-plugin": "11.0.0",
"emoji-datasource": "6.1.1",
"emoji-datasource-apple": "6.1.1",
"emoji-datasource-google": "6.1.1",
"enzyme": "3.11.0",
- "enzyme-adapter-react-17-updated": "1.0.2",
"enzyme-to-json": "3.6.2",
"eslint-plugin-no-only-tests": "3.1.0",
"external-remotes-plugin": "1.0.0",
@@ -185,7 +185,7 @@
"test:watch": "cross-env TZ=Etc/UTC LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 jest --watch",
"test:updatesnapshot": "cross-env TZ=Etc/UTC LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 jest --updateSnapshot",
"test:debug": "cross-env TZ=Etc/UTC LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 jest --forceExit --detectOpenHandles --verbose",
- "test-ci": "cross-env TZ=Etc/UTC LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 jest --ci --maxWorkers=100% --coverage",
+ "test-ci": "cross-env TZ=Etc/UTC LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 jest --ci --maxWorkers=100% --workerIdleMemoryLimit=4096MB",
"clean": "rm -rf dist node_modules .eslintcache .stylelintcache tsconfig.tsbuildinfo",
"stats": "cross-env NODE_ENV=production webpack --profile --json > webpack_stats.json",
"mmjstool": "mmjstool",
diff --git a/webapp/channels/src/actions/apps.ts b/webapp/channels/src/actions/apps.ts
index 4c3885f91cc..1ebf45504fd 100644
--- a/webapp/channels/src/actions/apps.ts
+++ b/webapp/channels/src/actions/apps.ts
@@ -228,7 +228,7 @@ export function openAppsModal(form: AppForm, context: AppContext): AnyAction {
dialogType: AppsForm,
dialogProps: {
form,
- context,
+ appContext: context,
},
});
}
diff --git a/webapp/channels/src/components/about_build_modal/about_build_modal.test.tsx b/webapp/channels/src/components/about_build_modal/about_build_modal.test.tsx
index 357168a5ab4..176522e9a7c 100644
--- a/webapp/channels/src/components/about_build_modal/about_build_modal.test.tsx
+++ b/webapp/channels/src/components/about_build_modal/about_build_modal.test.tsx
@@ -159,7 +159,7 @@ describe('components/AboutBuildModal', () => {
expect(screen.getByRole('link', {name: 'mobile'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-mobile/blob/master/NOTICE.txt');
});
- test('should call onExited callback when the modal is hidden', () => {
+ test('should call onExited callback when the modal is hidden', async () => {
const onExited = jest.fn();
const state = {
entities: {
@@ -185,7 +185,7 @@ describe('components/AboutBuildModal', () => {
state,
);
- userEvent.click(screen.getByText('Close'));
+ await userEvent.click(screen.getByText('Close'));
expect(onExited).toHaveBeenCalledTimes(1);
});
@@ -252,7 +252,7 @@ describe('components/AboutBuildModal', () => {
test('should handle API errors gracefully', async () => {
// Temporarily suppress console.error for this test
- jest.spyOn(console, 'error').mockImplementation(() => {});
+ console.error = jest.fn();
// Mock the API call to throw an error
jest.spyOn(Client4, 'getLicenseLoadMetric').mockRejectedValue(new Error('API error'));
diff --git a/webapp/channels/src/components/actions_menu/actions_menu.tsx b/webapp/channels/src/components/actions_menu/actions_menu.tsx
index 8f1c35e8685..5a499fa3a54 100644
--- a/webapp/channels/src/components/actions_menu/actions_menu.tsx
+++ b/webapp/channels/src/components/actions_menu/actions_menu.tsx
@@ -129,13 +129,16 @@ export class ActionMenuClass extends React.PureComponent {
};
handleOpenMarketplace = (): void => {
- const openMarketplaceData = {
- modalId: ModalIdentifiers.PLUGIN_MARKETPLACE,
- dialogType: MarketplaceModal,
- };
- this.props.actions.openModal(openMarketplaceData);
-
this.closeDropdown();
+
+ // Wait for the menu to close to avoid clashing between the menu's focus trap and the modal's
+ requestAnimationFrame(() => {
+ const openMarketplaceData = {
+ modalId: ModalIdentifiers.PLUGIN_MARKETPLACE,
+ dialogType: MarketplaceModal,
+ };
+ this.props.actions.openModal(openMarketplaceData);
+ });
};
onClickAppBinding = async (binding: AppBinding) => {
diff --git a/webapp/channels/src/components/actions_menu/index.test.tsx b/webapp/channels/src/components/actions_menu/index.test.tsx
index 63b9d426946..9c6f96d1bae 100644
--- a/webapp/channels/src/components/actions_menu/index.test.tsx
+++ b/webapp/channels/src/components/actions_menu/index.test.tsx
@@ -8,7 +8,7 @@ import {Permissions} from 'mattermost-redux/constants';
import ActionsMenu from 'components/actions_menu';
import ModalController from 'components/modal_controller';
-import {act, renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
+import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
function ActionsMenuTestWrapper(props: Omit, 'isMenuOpen' | 'handleDropdownOpened'>) {
@@ -72,7 +72,7 @@ describe('ActionsMenu', () => {
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
// Open the menu
- screen.getByRole('button').click();
+ await userEvent.click(screen.getByRole('button'));
// The dialog should open up
await waitFor(() => {
@@ -82,17 +82,17 @@ describe('ActionsMenu', () => {
});
// The focus starts on the dialog itself, so pressing tab should move it to the button
- userEvent.tab();
+ await userEvent.tab();
expect(screen.queryByRole('button', {name: 'Visit the Marketplace'})).toHaveFocus();
// The focus should be trapped, so hitting tab again shouldn't change the focus
- userEvent.tab();
+ await userEvent.tab();
expect(screen.queryByRole('button', {name: 'Visit the Marketplace'})).toHaveFocus();
// Pressing enter should open the marketplace modal and close the menu
- userEvent.keyboard('{Enter}');
+ await userEvent.keyboard('{Enter}');
await waitFor(() => {
expect(screen.queryByRole('dialog', {name: 'actions'})).not.toBeInTheDocument();
@@ -100,16 +100,14 @@ describe('ActionsMenu', () => {
});
// Pressing escape should close the marketplace modal
- act(() => {
- userEvent.keyboard('{Escape}');
- });
+ await userEvent.type(screen.getByRole('dialog'), '{Escape}');
await waitFor(() => {
expect(screen.queryByRole('dialog', {name: 'App Marketplace'})).not.toBeInTheDocument();
});
// Reopen the menu
- screen.getByRole('button').click();
+ await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(screen.queryByRole('dialog')).toBeVisible();
@@ -117,7 +115,7 @@ describe('ActionsMenu', () => {
expect(screen.queryByRole('dialog')).toHaveAccessibleName('actions');
// Pressing escape should close the dialog
- userEvent.keyboard('{Escape}');
+ await userEvent.keyboard('{Escape}');
await waitFor(() => {
expect(screen.queryByRole('dialog', {name: 'actions'})).not.toBeInTheDocument();
diff --git a/webapp/channels/src/components/admin_console/access_control/modals/policy_test/test_modal.tsx b/webapp/channels/src/components/admin_console/access_control/modals/policy_test/test_modal.tsx
index 3f321a17072..9f92f209662 100644
--- a/webapp/channels/src/components/admin_console/access_control/modals/policy_test/test_modal.tsx
+++ b/webapp/channels/src/components/admin_console/access_control/modals/policy_test/test_modal.tsx
@@ -33,7 +33,7 @@ function TestResultsModal({
onExited,
actions,
}: Props): JSX.Element {
- const dispatch = useDispatch();
+ const dispatch = useDispatch();
const [term, setTerm] = useState('');
const [users, setUsers] = useState([]);
const [total, setTotal] = useState(0);
diff --git a/webapp/channels/src/components/admin_console/access_control/modals/user_sync/synced_user_list.tsx b/webapp/channels/src/components/admin_console/access_control/modals/user_sync/synced_user_list.tsx
index 724599d919d..d22ddf62bf6 100644
--- a/webapp/channels/src/components/admin_console/access_control/modals/user_sync/synced_user_list.tsx
+++ b/webapp/channels/src/components/admin_console/access_control/modals/user_sync/synced_user_list.tsx
@@ -30,7 +30,7 @@ const USERS_PER_PAGE = 10;
// - improve search
export const SyncedUserList = ({userIds, noResultsMessageId, noResultsDefaultMessage, actions}: SyncedUserListProps): JSX.Element => {
- const dispatch = useDispatch();
+ const dispatch = useDispatch();
const [users, setUsers] = useState([]);
const [currentPage, setCurrentPage] = useState(0);
diff --git a/webapp/channels/src/components/admin_console/access_control/policies.test.tsx b/webapp/channels/src/components/admin_console/access_control/policies.test.tsx
index 3fe59e0c5c8..07de6f8d465 100644
--- a/webapp/channels/src/components/admin_console/access_control/policies.test.tsx
+++ b/webapp/channels/src/components/admin_console/access_control/policies.test.tsx
@@ -3,7 +3,6 @@
import {shallow} from 'enzyme';
import React from 'react';
-import {act} from 'react-dom/test-utils';
import type {AccessControlPolicy} from '@mattermost/types/access_control';
@@ -11,6 +10,8 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
import type {Column} from 'components/admin_console/data_grid/data_grid';
+import {act} from 'tests/react_testing_utils';
+
import PolicyList from './policies';
const mockHistoryPushInternal = jest.fn();
diff --git a/webapp/channels/src/components/admin_console/access_control/policy_details/policy_details.test.tsx b/webapp/channels/src/components/admin_console/access_control/policy_details/policy_details.test.tsx
index dc4139b2430..d6acc8b55de 100644
--- a/webapp/channels/src/components/admin_console/access_control/policy_details/policy_details.test.tsx
+++ b/webapp/channels/src/components/admin_console/access_control/policy_details/policy_details.test.tsx
@@ -3,11 +3,11 @@
import {shallow} from 'enzyme';
import React from 'react';
-import {act} from 'react-dom/test-utils';
import type {ChannelWithTeamData} from '@mattermost/types/channels';
import {useChannelAccessControlActions} from 'hooks/useChannelAccessControlActions';
+import {act} from 'tests/react_testing_utils';
import PolicyDetails from './policy_details';
diff --git a/webapp/channels/src/components/admin_console/admin_navbar_dropdown/menu_item_blockable_link.tsx b/webapp/channels/src/components/admin_console/admin_navbar_dropdown/menu_item_blockable_link.tsx
index 40bc87b1693..4fa7c67a008 100644
--- a/webapp/channels/src/components/admin_console/admin_navbar_dropdown/menu_item_blockable_link.tsx
+++ b/webapp/channels/src/components/admin_console/admin_navbar_dropdown/menu_item_blockable_link.tsx
@@ -11,7 +11,7 @@ type Props = {
text: string | React.ReactNode;
};
-export const MenuItemBlockableLinkImpl: React.SFC = (props: Props): JSX.Element => {
+export const MenuItemBlockableLinkImpl = (props: Props): JSX.Element => {
const {to, text} = props;
return (
{text}
diff --git a/webapp/channels/src/components/admin_console/admin_sidebar/index.ts b/webapp/channels/src/components/admin_console/admin_sidebar/index.ts
index 03c11dabeb7..47f1a1e97ef 100644
--- a/webapp/channels/src/components/admin_console/admin_sidebar/index.ts
+++ b/webapp/channels/src/components/admin_console/admin_sidebar/index.ts
@@ -58,7 +58,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
};
}
-const connector = connect(mapStateToProps, mapDispatchToProps, null, {pure: false});
+const connector = connect(mapStateToProps, mapDispatchToProps);
export type PropsFromRedux = ConnectedProps;
diff --git a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/__snapshots__/cloud_trial_banner.test.tsx.snap b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/__snapshots__/cloud_trial_banner.test.tsx.snap
index 48ca4228222..0a56ee0a901 100644
--- a/webapp/channels/src/components/admin_console/billing/billing_subscriptions/__snapshots__/cloud_trial_banner.test.tsx.snap
+++ b/webapp/channels/src/components/admin_console/billing/billing_subscriptions/__snapshots__/cloud_trial_banner.test.tsx.snap
@@ -4,6 +4,9 @@ exports[`components/admin_console/billing_subscription/CloudTrialBanner should m
{text},
+ link: (text) => {text},
}}
/>
}
@@ -46,7 +46,7 @@ export const paymentFailedBanner = () => {
id='billing.subscription.info.mostRecentPaymentFailed.description.mostRecentPaymentFailed'
defaultMessage='It looks your most recent payment failed because the credit card on your account has expired. Please update your payment information to avoid any disruption.'
values={{
- link: (text: string) => {text},
+ link: (text) => {text},
}}
/>
}
diff --git a/webapp/channels/src/components/admin_console/billing/plan_details/__snapshots__/feature_list.test.tsx.snap b/webapp/channels/src/components/admin_console/billing/plan_details/__snapshots__/feature_list.test.tsx.snap
index 55a516e6a20..cd16e76c5bb 100644
--- a/webapp/channels/src/components/admin_console/billing/plan_details/__snapshots__/feature_list.test.tsx.snap
+++ b/webapp/channels/src/components/admin_console/billing/plan_details/__snapshots__/feature_list.test.tsx.snap
@@ -4,6 +4,9 @@ exports[`components/admin_console/billing/plan_details/feature_list should match
getUsersByIDs(state, initialValue || []));
+ const getUsersByIds = useMemo(makeGetUsersByIds, []);
+ const initialUsers = useSelector((state: GlobalState) => getUsersByIds(state, initialValue || []));
const selectInitialValue = initialUsers.
filter((userProfile) => Boolean(userProfile)).
map((userProfile: UserProfile) => ({
diff --git a/webapp/channels/src/components/admin_console/custom_enable_disable_guest_accounts_setting.tsx b/webapp/channels/src/components/admin_console/custom_enable_disable_guest_accounts_setting.tsx
index 12d12d29502..78641522711 100644
--- a/webapp/channels/src/components/admin_console/custom_enable_disable_guest_accounts_setting.tsx
+++ b/webapp/channels/src/components/admin_console/custom_enable_disable_guest_accounts_setting.tsx
@@ -58,7 +58,7 @@ const CustomEnableDisableGuestAccountsSetting = ({
id='admin.guest_access.helpText'
defaultMessage='When true, external guest can be invited to channels within teams. Please see Permissions Schemes for which roles can invite guests.'
values={{
- a: (chunks: string) => {chunks},
+ a: (chunks) => {chunks},
}}
/>
);
diff --git a/webapp/channels/src/components/admin_console/custom_profile_attributes/custom_profile_attributes.test.tsx b/webapp/channels/src/components/admin_console/custom_profile_attributes/custom_profile_attributes.test.tsx
index 57d2afff3dd..fae291d43a4 100644
--- a/webapp/channels/src/components/admin_console/custom_profile_attributes/custom_profile_attributes.test.tsx
+++ b/webapp/channels/src/components/admin_console/custom_profile_attributes/custom_profile_attributes.test.tsx
@@ -3,13 +3,12 @@
import {screen, fireEvent} from '@testing-library/react';
import React from 'react';
-import {act} from 'react-dom/test-utils';
import type {UserPropertyField, UserPropertyFieldGroupID, UserPropertyFieldType} from '@mattermost/types/properties';
import {Client4} from 'mattermost-redux/client';
-import {renderWithContext} from 'tests/react_testing_utils';
+import {act, renderWithContext} from 'tests/react_testing_utils';
import CustomProfileAttributes from './custom_profile_attributes';
diff --git a/webapp/channels/src/components/admin_console/custom_profile_attributes/custom_profile_attributes.tsx b/webapp/channels/src/components/admin_console/custom_profile_attributes/custom_profile_attributes.tsx
index 1b2e6a3dc01..5622dc1118a 100644
--- a/webapp/channels/src/components/admin_console/custom_profile_attributes/custom_profile_attributes.tsx
+++ b/webapp/channels/src/components/admin_console/custom_profile_attributes/custom_profile_attributes.tsx
@@ -31,7 +31,7 @@ const AttributeHelpText = memo(({attributeKey, attributeName, attributeType}: At
defaultMessage='(Optional) The attribute in the AD/LDAP server used to populate the {name} of users in Mattermost. When set, users cannot edit their {name}, since it is synchronized with the LDAP server. When left blank, users can set their {name} in Account Menu > Account Settings > Profile.'
values={{
name: attributeName,
- strong: (msg: string) => {msg},
+ strong: (msg) => {msg},
}}
/>
)}
@@ -51,7 +51,7 @@ const AttributeHelpText = memo(({attributeKey, attributeName, attributeType}: At
defaultMessage='(Warning) This attribute will be converted to a TEXT attribute, if the field is set to synchronize.'
values={{
name: attributeName,
- strong: (msg: string) => {msg},
+ strong: (msg) => {msg},
}}
/>
@@ -134,7 +134,7 @@ const CustomProfileAttributes: React.FC = (props: Props): JSX.Element | n
id='admin.customProfileAttributes.subtitle'
defaultMessage='You can add or remove custom profile attributes by going to the system properties page.'
values={{
- link: (msg: string) => (
+ link: (msg) => (
diff --git a/webapp/channels/src/components/admin_console/custom_terms_of_service_settings/custom_terms_of_service_settings.tsx b/webapp/channels/src/components/admin_console/custom_terms_of_service_settings/custom_terms_of_service_settings.tsx
index 35646708512..289a5dac464 100644
--- a/webapp/channels/src/components/admin_console/custom_terms_of_service_settings/custom_terms_of_service_settings.tsx
+++ b/webapp/channels/src/components/admin_console/custom_terms_of_service_settings/custom_terms_of_service_settings.tsx
@@ -205,7 +205,7 @@ export default class CustomTermsOfServiceSettings extends OLDAdminSettings {chunks},
+ a: (chunks) => {chunks},
}}
/>
}
diff --git a/webapp/channels/src/components/admin_console/group_settings/group_details/group_users.tsx b/webapp/channels/src/components/admin_console/group_settings/group_details/group_users.tsx
index 322f42f895b..a042ed37f33 100644
--- a/webapp/channels/src/components/admin_console/group_settings/group_details/group_users.tsx
+++ b/webapp/channels/src/components/admin_console/group_settings/group_details/group_users.tsx
@@ -165,7 +165,7 @@ export default class GroupUsers extends React.PureComponent {
'AD/LDAP Connector is configured to sync and manage this group and its users. Click here to view'
}
values={{
- a: (chunks: string) => (
+ a: (chunks) => (
{chunks}
diff --git a/webapp/channels/src/components/admin_console/ip_filtering/index.tsx b/webapp/channels/src/components/admin_console/ip_filtering/index.tsx
index 34d2b1e55bf..b19c411d55f 100644
--- a/webapp/channels/src/components/admin_console/ip_filtering/index.tsx
+++ b/webapp/channels/src/components/admin_console/ip_filtering/index.tsx
@@ -256,7 +256,7 @@ const IPFiltering = () => {
id={'admin.ip_filtering.no_filters_added'}
defaultMessage={'Are you sure you want to apply these IP filter changes? There are currently no filters added, so all IP addresses will have access to the workspace.'}
values={{
- strong: (content: string) => {content},
+ strong: (content) => {content},
}}
/>
);
@@ -269,7 +269,7 @@ const IPFiltering = () => {
id={'admin.ip_filtering.turn_off_ip_filtering'}
defaultMessage={'Are you sure you want to turn off IP Filtering? All IP addresses will have access to the workspace.'}
values={{
- strong: (content: string) => {content},
+ strong: (content) => {content},
}}
/>
);
@@ -282,7 +282,7 @@ const IPFiltering = () => {
id={'admin.ip_filtering.apply_ip_filter_changes_are_you_sure'}
defaultMessage={'Are you sure you want to apply these IP Filter changes? Users with IP addresses outside of the IP ranges provided will no longer have access to the workspace.'}
values={{
- strong: (content: string) => {content},
+ strong: (content) => {content},
}}
/>
);
diff --git a/webapp/channels/src/components/admin_console/jobs/job_cancel_button.tsx b/webapp/channels/src/components/admin_console/jobs/job_cancel_button.tsx
index ad4d365ba8b..04ebae3e0b4 100644
--- a/webapp/channels/src/components/admin_console/jobs/job_cancel_button.tsx
+++ b/webapp/channels/src/components/admin_console/jobs/job_cancel_button.tsx
@@ -21,7 +21,7 @@ const JobCancelButton = (props: Props): JSX.Element|null => {
const intl = useIntl();
let cancelButton = null;
- const handleClick = useCallback((e) => {
+ const handleClick = useCallback((e: React.MouseEvent) => {
e.preventDefault();
onClick(job.id);
}, [onClick, job.id]);
diff --git a/webapp/channels/src/components/admin_console/ldap_wizard/ldap_custom_setting.tsx b/webapp/channels/src/components/admin_console/ldap_wizard/ldap_custom_setting.tsx
index 780af92abb9..bc3c1f9e51f 100644
--- a/webapp/channels/src/components/admin_console/ldap_wizard/ldap_custom_setting.tsx
+++ b/webapp/channels/src/components/admin_console/ldap_wizard/ldap_custom_setting.tsx
@@ -61,9 +61,9 @@ const LDAPCustomSetting = (props: Props) => {
if (props.setting.showTitle) {
return (
{componentInstance}
diff --git a/webapp/channels/src/components/admin_console/ldap_wizard/ldap_wizard.tsx b/webapp/channels/src/components/admin_console/ldap_wizard/ldap_wizard.tsx
index 9ee55dc526e..21893b2a686 100644
--- a/webapp/channels/src/components/admin_console/ldap_wizard/ldap_wizard.tsx
+++ b/webapp/channels/src/components/admin_console/ldap_wizard/ldap_wizard.tsx
@@ -84,7 +84,7 @@ type State = {
[x: string]: unknown;
saveNeeded: false | 'both' | 'permissions' | 'config';
saving: boolean;
- serverError: string | { message: string; id?: string } | null;
+ serverError: string | null;
confirmNeededId: string;
showConfirmId: string;
clientWarning: string | boolean;
diff --git a/webapp/channels/src/components/admin_console/license_settings/modals/__snapshots__/confirm_license_removal_modal.test.tsx.snap b/webapp/channels/src/components/admin_console/license_settings/modals/__snapshots__/confirm_license_removal_modal.test.tsx.snap
index bbae5f184ea..a662dd1b9d6 100644
--- a/webapp/channels/src/components/admin_console/license_settings/modals/__snapshots__/confirm_license_removal_modal.test.tsx.snap
+++ b/webapp/channels/src/components/admin_console/license_settings/modals/__snapshots__/confirm_license_removal_modal.test.tsx.snap
@@ -4,6 +4,9 @@ exports[`components/admin_console/license_settings/modals/confirm_license_remova
= ({license, totalUsers,
id='admin.license.renewalCard.licenseExpiring'
defaultMessage='License expires in {days} days on {date, date, long}.'
values={{
- date: endOfLicense,
+ date: endOfLicense.toDate(),
days: daysToEndLicense,
}}
/>
@@ -51,7 +51,7 @@ const RenewLicenseCard: React.FC = ({license, totalUsers,
id='admin.license.renewalCard.licenseExpired'
defaultMessage='License expired on {date, date, long}.'
values={{
- date: endOfLicense,
+ date: endOfLicense.toDate(),
}}
/>
);
diff --git a/webapp/channels/src/components/admin_console/license_settings/team_edition/team_edition_right_panel.tsx b/webapp/channels/src/components/admin_console/license_settings/team_edition/team_edition_right_panel.tsx
index 70d32efbf94..7f1164093d2 100644
--- a/webapp/channels/src/components/admin_console/license_settings/team_edition/team_edition_right_panel.tsx
+++ b/webapp/channels/src/components/admin_console/license_settings/team_edition/team_edition_right_panel.tsx
@@ -104,7 +104,7 @@ const TeamEditionRightPanel: React.FC = ({
id='admin.licenseSettings.teamEdition.teamEditionRightPanel.acceptTermsInitial'
defaultMessage='By clicking Upgrade, I agree to the terms of the Mattermost '
values={{
- b: (chunks: string) => {chunks},
+ b: (chunks) => {chunks},
}}
/>
{
id='admin.license.trial-request.embargoed'
defaultMessage='We were unable to process the request due to limitations for embargoed countries. Learn more in our documentation, or reach out to legal@mattermost.com for questions around export limitations.'
values={{
- link: (text: string) => (
+ link: (text) => (
Learn more in our documentation, or reach out to legal@mattermost.com for questions around export limitations.',
},
{
- link: (text: string) => (
+ link: (text) => (
= ({license}: Props) => {
id='admin.license.trialLicenseCard.expiringToday'
defaultMessage='Your free trial expires Today at {time}. Contact sales to purchase a license and continue using advanced features after the trial ends.'
values={{
- b: (chunks: string) => {chunks},
+ b: (chunks) => {chunks},
time: moment(endDate).endOf('day').format('h:mm a ') + moment().tz(getBrowserTimezone()).format('z'),
}}
/>
@@ -45,7 +45,7 @@ const TrialLicenseCard: React.FC = ({license}: Props) => {
id='admin.license.trialLicenseCard.expiringAfterFewDays'
defaultMessage='Your free trial will expire in {daysCount} {daysCount, plural, one {day} other {days}}. Contact sales to purchase a license and continue using advanced features.'
values={{
- b: (chunks: string) => {chunks},
+ b: (chunks) => {chunks},
daysCount: daysToEndLicense,
}}
/>
diff --git a/webapp/channels/src/components/admin_console/manage_teams_modal/manage_teams_modal.test.tsx b/webapp/channels/src/components/admin_console/manage_teams_modal/manage_teams_modal.test.tsx
index 48227e06f87..b2294e40255 100644
--- a/webapp/channels/src/components/admin_console/manage_teams_modal/manage_teams_modal.test.tsx
+++ b/webapp/channels/src/components/admin_console/manage_teams_modal/manage_teams_modal.test.tsx
@@ -4,13 +4,13 @@
import type {ReactWrapper} from 'enzyme';
import {mount, shallow} from 'enzyme';
import React from 'react';
-import {act} from 'react-dom/test-utils';
import {IntlProvider} from 'react-intl';
import {General} from 'mattermost-redux/constants';
import ManageTeamsModal from 'components/admin_console/manage_teams_modal/manage_teams_modal';
+import {act} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import ManageTeamsDropdown from './manage_teams_dropdown';
diff --git a/webapp/channels/src/components/admin_console/message_export_settings.tsx b/webapp/channels/src/components/admin_console/message_export_settings.tsx
index b2d31874758..cdf47519356 100644
--- a/webapp/channels/src/components/admin_console/message_export_settings.tsx
+++ b/webapp/channels/src/components/admin_console/message_export_settings.tsx
@@ -330,7 +330,7 @@ export class MessageExportSettings extends OLDAdminSettings (
+ a: (chunks) => (
{chunks}
diff --git a/webapp/channels/src/components/admin_console/permission_schemes_settings/__snapshots__/permission_description.test.tsx.snap b/webapp/channels/src/components/admin_console/permission_schemes_settings/__snapshots__/permission_description.test.tsx.snap
index e7231316bc5..b102e163b7b 100644
--- a/webapp/channels/src/components/admin_console/permission_schemes_settings/__snapshots__/permission_description.test.tsx.snap
+++ b/webapp/channels/src/components/admin_console/permission_schemes_settings/__snapshots__/permission_description.test.tsx.snap
@@ -87,6 +87,9 @@ exports[`components/admin_console/permission_schemes_settings/permission_descrip
{chunks},
+ b: (chunks) => {chunks},
}}
/>
diff --git a/webapp/channels/src/components/admin_console/permission_schemes_settings/permission_description.tsx b/webapp/channels/src/components/admin_console/permission_schemes_settings/permission_description.tsx
index 1e4654a64bb..75498b3971f 100644
--- a/webapp/channels/src/components/admin_console/permission_schemes_settings/permission_description.tsx
+++ b/webapp/channels/src/components/admin_console/permission_schemes_settings/permission_description.tsx
@@ -9,14 +9,13 @@ import type {Role} from '@mattermost/types/roles';
import WithTooltip from 'components/with_tooltip';
-import type {AdditionalValues} from './permissions_tree/types';
import {rolesRolesStrings} from './strings/roles';
type Props = {
id: string;
inherited?: Partial
;
selectRow: (id: string) => void;
- additionalValues?: AdditionalValues | AdditionalValues['edit_post'];
+ additionalValues?: Record;
description: string | JSX.Element;
}
@@ -50,7 +49,7 @@ const PermissionDescription = ({
defaultMessage='Inherited from {name}.'
values={{
name: formattedName,
- link: (text: string) => (
+ link: (text) => (
{text}
),
}}
diff --git a/webapp/channels/src/components/admin_console/permission_schemes_settings/permission_group.tsx b/webapp/channels/src/components/admin_console/permission_schemes_settings/permission_group.tsx
index 7c833e8d4ae..80387ce0bf9 100644
--- a/webapp/channels/src/components/admin_console/permission_schemes_settings/permission_group.tsx
+++ b/webapp/channels/src/components/admin_console/permission_schemes_settings/permission_group.tsx
@@ -143,7 +143,7 @@ export default class PermissionGroup extends React.PureComponent {
return true;
};
- renderPermission = (permission: string, additionalValues: AdditionalValues) => {
+ renderPermission = (permission: string, additionalValues: Record) => {
if (!this.isInScope(permission)) {
return null;
}
diff --git a/webapp/channels/src/components/admin_console/permission_schemes_settings/permission_row.tsx b/webapp/channels/src/components/admin_console/permission_schemes_settings/permission_row.tsx
index 2fb445851f2..cc6791be3bd 100644
--- a/webapp/channels/src/components/admin_console/permission_schemes_settings/permission_row.tsx
+++ b/webapp/channels/src/components/admin_console/permission_schemes_settings/permission_row.tsx
@@ -9,7 +9,6 @@ import type {Role} from '@mattermost/types/roles';
import PermissionCheckbox from './permission_checkbox';
import PermissionDescription from './permission_description';
-import type {AdditionalValues} from './permissions_tree/types';
import {permissionRolesStrings} from './strings/permissions';
type Props = {
@@ -21,7 +20,7 @@ type Props = {
selectRow: (id: string) => void;
value: string;
onChange: (id: string) => void;
- additionalValues: AdditionalValues;
+ additionalValues: Record;
}
const PermissionRow = ({
diff --git a/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/types.ts b/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/types.ts
index 1b59575f4b8..abe02898d11 100644
--- a/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/types.ts
+++ b/webapp/channels/src/components/admin_console/permission_schemes_settings/permissions_tree/types.ts
@@ -16,8 +16,4 @@ export type Group = {
isVisible?: (license?: ClientLicense) => boolean;
}
-export type AdditionalValues = {
- [edit_post: string]: {
- editTimeLimitButton: JSX.Element;
- };
-}
+export type AdditionalValues = Record>;
diff --git a/webapp/channels/src/components/admin_console/schema_admin_settings.tsx b/webapp/channels/src/components/admin_console/schema_admin_settings.tsx
index 0f1a1c2ccff..4711839e084 100644
--- a/webapp/channels/src/components/admin_console/schema_admin_settings.tsx
+++ b/webapp/channels/src/components/admin_console/schema_admin_settings.tsx
@@ -968,9 +968,9 @@ export class SchemaAdminSettings extends React.PureComponent {
if (setting.showTitle) {
return (
{componentInstance}
diff --git a/webapp/channels/src/components/admin_console/secure_connections/modals/secure_connection_delete_modal.tsx b/webapp/channels/src/components/admin_console/secure_connections/modals/secure_connection_delete_modal.tsx
index 3617ceb87d4..025924749f8 100644
--- a/webapp/channels/src/components/admin_console/secure_connections/modals/secure_connection_delete_modal.tsx
+++ b/webapp/channels/src/components/admin_console/secure_connections/modals/secure_connection_delete_modal.tsx
@@ -38,7 +38,7 @@ function SecureConnectionDeleteModal({
id={'admin.secure_connections.confirm.delete.text'}
defaultMessage={'Are you sure you want to delete the secure connection {displayName}?'}
values={{
- strong: (chunk: string) => {chunk},
+ strong: (chunk) => {chunk},
displayName,
}}
/>
diff --git a/webapp/channels/src/components/admin_console/secure_connections/utils.ts b/webapp/channels/src/components/admin_console/secure_connections/utils.ts
index dc3efcc96c4..932983e333e 100644
--- a/webapp/channels/src/components/admin_console/secure_connections/utils.ts
+++ b/webapp/channels/src/components/admin_console/secure_connections/utils.ts
@@ -19,8 +19,6 @@ import {Client4} from 'mattermost-redux/client';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getActiveTeamsList, getTeam} from 'mattermost-redux/selectors/entities/teams';
-import type {ActionFuncAsync} from 'types/store';
-
export const useRemoteClusters = () => {
const [remoteClusters, setRemoteClusters] = useState();
const [loadingState, setLoadingState] = useState(true);
@@ -153,7 +151,7 @@ export const useSharedChannelRemoteRows = (remoteId: string, opts: {filter: 'hom
}
setLoadingState(true);
- dispatch>>(async (dispatch, getState) => {
+ dispatch(async (dispatch, getState) => {
const collected: IDMappedObjects = {};
const missing: SharedChannelRemote[] = [];
diff --git a/webapp/channels/src/components/admin_console/server_logs/logs.test.tsx b/webapp/channels/src/components/admin_console/server_logs/logs.test.tsx
index ee588467abf..b3307091e51 100644
--- a/webapp/channels/src/components/admin_console/server_logs/logs.test.tsx
+++ b/webapp/channels/src/components/admin_console/server_logs/logs.test.tsx
@@ -65,7 +65,7 @@ describe('components/admin_console/server_logs/Logs', () => {
test.each(['caller', 'msg', 'worker', 'job_id', 'whatever'])('should search input be performed on %s attribute',
async (searchString: string) => {
const searchInput = screen.getByTestId('searchInput');
- userEvent.type(searchInput, searchString);
+ await userEvent.type(searchInput, searchString);
await waitFor(() => {
expect(screen.queryByText('msg 1')).toBeInTheDocument();
@@ -77,7 +77,7 @@ describe('components/admin_console/server_logs/Logs', () => {
test.each(['level', 'timestamp'])('should search input not be performed on %s attribute',
async (searchString: string) => {
const searchInput = screen.getByTestId('searchInput');
- userEvent.type(searchInput, searchString);
+ await userEvent.type(searchInput, searchString);
await waitFor(() => {
expect(screen.queryByText('msg 1')).not.toBeInTheDocument();
diff --git a/webapp/channels/src/components/admin_console/system_properties/section_utils.test.ts b/webapp/channels/src/components/admin_console/system_properties/section_utils.test.ts
index b9e24753b1b..033f27a53c6 100644
--- a/webapp/channels/src/components/admin_console/system_properties/section_utils.test.ts
+++ b/webapp/channels/src/components/admin_console/system_properties/section_utils.test.ts
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {act} from '@testing-library/react-hooks';
+import {act} from '@testing-library/react';
import type {DeepPartial} from '@mattermost/types/utilities';
@@ -106,13 +106,12 @@ describe('useOperation', () => {
await act(async () => {
await actionPromise;
-
- const [, status3] = result.current;
-
- expect(status3.loading).toBe(false);
- expect(await actionPromise).toBe('test response value');
- expect(status3.error).toBe(undefined);
});
+
+ const [, status3] = result.current;
+ expect(status3.loading).toBe(false);
+ expect(status3.error).toBe(undefined);
+ expect(await actionPromise!).toBe('test response value');
});
it('should run operation on command with error and loading phases: false -> true -> false', async () => {
diff --git a/webapp/channels/src/components/admin_console/system_properties/system_properties.test.tsx b/webapp/channels/src/components/admin_console/system_properties/system_properties.test.tsx
index b5e8a0beaa8..f8e7915b53a 100644
--- a/webapp/channels/src/components/admin_console/system_properties/system_properties.test.tsx
+++ b/webapp/channels/src/components/admin_console/system_properties/system_properties.test.tsx
@@ -75,10 +75,10 @@ describe('SystemProperties', () => {
expect(screen.getByRole('heading', {name: 'Configure user attributes'})).toBeInTheDocument();
- expect(screen.queryByDisplayValue('test attribute 0')).toBeInTheDocument();
- expect(screen.queryByDisplayValue('test attribute 1')).toBeInTheDocument();
- expect(screen.queryByDisplayValue('test attribute 2')).toBeInTheDocument();
- expect(screen.queryByDisplayValue('test attribute 3')).toBeInTheDocument();
+ expect(await screen.findByDisplayValue('test attribute 0')).toBeInTheDocument();
+ expect(await screen.findByDisplayValue('test attribute 1')).toBeInTheDocument();
+ expect(await screen.findByDisplayValue('test attribute 2')).toBeInTheDocument();
+ expect(await screen.findByDisplayValue('test attribute 3')).toBeInTheDocument();
});
});
});
diff --git a/webapp/channels/src/components/admin_console/system_properties/user_properties_utils.test.ts b/webapp/channels/src/components/admin_console/system_properties/user_properties_utils.test.ts
index 2ef5a68852d..a3599a15ae0 100644
--- a/webapp/channels/src/components/admin_console/system_properties/user_properties_utils.test.ts
+++ b/webapp/channels/src/components/admin_console/system_properties/user_properties_utils.test.ts
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {act} from '@testing-library/react-hooks';
+import {act} from '@testing-library/react';
import type {UserPropertyField, UserPropertyFieldPatch} from '@mattermost/types/properties';
import type {DeepPartial} from '@mattermost/types/utilities';
@@ -9,7 +9,7 @@ import type {DeepPartial} from '@mattermost/types/utilities';
import {Client4} from 'mattermost-redux/client';
import {generateId} from 'mattermost-redux/utils/helpers';
-import {renderHookWithContext} from 'tests/react_testing_utils';
+import {renderHookWithContext, waitFor} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import type {GlobalState} from 'types/store';
@@ -71,7 +71,7 @@ describe('useUserPropertyFields', () => {
getFields.mockResolvedValue([field0, field1, field2, field3]);
it('should return a collection', async () => {
- const {result, rerender, waitFor} = renderHookWithContext(() => {
+ const {result, rerender} = renderHookWithContext(() => {
return useUserPropertyFields();
}, getBaseState());
@@ -100,7 +100,7 @@ describe('useUserPropertyFields', () => {
});
it('should successfully handle edits', async () => {
- const {result, rerender, waitFor} = renderHookWithContext(() => {
+ const {result, rerender} = renderHookWithContext(() => {
return useUserPropertyFields();
}, getBaseState());
@@ -151,7 +151,7 @@ describe('useUserPropertyFields', () => {
it('should successfully handle reordering', async () => {
patchField.mockImplementation((id: string, patch: UserPropertyFieldPatch) => Promise.resolve({...baseField, ...patch, id, update_at: Date.now()} as UserPropertyField));
- const {result, rerender, waitFor} = renderHookWithContext(() => {
+ const {result, rerender} = renderHookWithContext(() => {
return useUserPropertyFields();
}, getBaseState());
@@ -202,7 +202,7 @@ describe('useUserPropertyFields', () => {
});
it('should successfully handle deletes', async () => {
- const {result, rerender, waitFor} = renderHookWithContext(() => {
+ const {result, rerender} = renderHookWithContext(() => {
return useUserPropertyFields();
}, getBaseState());
@@ -251,7 +251,7 @@ describe('useUserPropertyFields', () => {
it('should successfully handle creates', async () => {
createField.mockImplementation((patch) => Promise.resolve({...baseField, ...patch, id: generateId()} as UserPropertyField));
- const {result, rerender, waitFor} = renderHookWithContext(() => {
+ const {result, rerender} = renderHookWithContext(() => {
return useUserPropertyFields();
}, getBaseState());
@@ -300,7 +300,7 @@ describe('useUserPropertyFields', () => {
});
it('should validate name uniqueness', async () => {
- const {result, rerender, waitFor} = renderHookWithContext(() => {
+ const {result, rerender} = renderHookWithContext(() => {
return useUserPropertyFields();
}, getBaseState());
@@ -330,7 +330,7 @@ describe('useUserPropertyFields', () => {
});
it('should validate names already taken', async () => {
- const {result, rerender, waitFor} = renderHookWithContext(() => {
+ const {result, rerender} = renderHookWithContext(() => {
return useUserPropertyFields();
}, getBaseState());
@@ -370,7 +370,7 @@ describe('useUserPropertyFields', () => {
});
it('should validate name required', async () => {
- const {result, rerender, waitFor} = renderHookWithContext(() => {
+ const {result, rerender} = renderHookWithContext(() => {
return useUserPropertyFields();
}, getBaseState());
diff --git a/webapp/channels/src/components/admin_console/system_roles/system_role/system_role_permissions.tsx b/webapp/channels/src/components/admin_console/system_roles/system_role/system_role_permissions.tsx
index 11c0208ec82..e61435876fb 100644
--- a/webapp/channels/src/components/admin_console/system_roles/system_role/system_role_permissions.tsx
+++ b/webapp/channels/src/components/admin_console/system_roles/system_role/system_role_permissions.tsx
@@ -212,7 +212,7 @@ export default class SystemRolePermissions extends React.PureComponent (
+ a: (chunks) => (
{chunks},
+ b: (chunks) => {chunks},
}}
/>
diff --git a/webapp/channels/src/components/admin_console/system_user_detail/__snapshots__/system_user_detail.test.tsx.snap b/webapp/channels/src/components/admin_console/system_user_detail/__snapshots__/system_user_detail.test.tsx.snap
index 9d4278929eb..3a7f3d53a45 100644
--- a/webapp/channels/src/components/admin_console/system_user_detail/__snapshots__/system_user_detail.test.tsx.snap
+++ b/webapp/channels/src/components/admin_console/system_user_detail/__snapshots__/system_user_detail.test.tsx.snap
@@ -49,9 +49,7 @@ exports[`SystemUserDetail should match default snapshot 1`] = `
class="AdminUserCard__user-info"
>
-
-
-
-
-
-
-
-
-
-
-
-
{
} as RouteComponentProps),
};
- const waitForLoadingToFinish = async (container: HTMLElement) => {
- const noUserBody = container.querySelector('.noUserBody');
- const spinner = within(noUserBody as HTMLElement).getByTestId('loadingSpinner');
- expect(spinner).toBeInTheDocument();
-
- await waitFor(() => {
- expect(container.querySelector('[data-testid="loadingSpinner"]')).not.toBeInTheDocument();
- });
+ const waitForLoadingToFinish = async () => {
+ await waitForElementToBeRemoved(screen.queryAllByTitle('Loading Icon'));
+ await waitFor(() => expect(screen.queryByText('No teams found')).toBeInTheDocument());
};
test('should match default snapshot', async () => {
const props = defaultProps;
const {container} = renderWithContext();
- await waitForLoadingToFinish(container);
+ await waitForLoadingToFinish();
expect(container).toMatchSnapshot();
});
@@ -80,7 +75,7 @@ describe('SystemUserDetail', () => {
};
const {container} = renderWithContext();
- await waitForLoadingToFinish(container);
+ await waitForLoadingToFinish();
expect(container).toMatchSnapshot();
});
@@ -92,7 +87,7 @@ describe('SystemUserDetail', () => {
};
const {container} = renderWithContext();
- await waitForLoadingToFinish(container);
+ await waitForLoadingToFinish();
expect(container).toMatchSnapshot();
});
@@ -104,7 +99,7 @@ describe('SystemUserDetail', () => {
};
const {container} = renderWithContext();
- await waitForLoadingToFinish(container);
+ await waitForLoadingToFinish();
expect(container).toMatchSnapshot();
});
@@ -118,7 +113,7 @@ describe('SystemUserDetail', () => {
const {container} = renderWithContext();
- await waitForLoadingToFinish(container);
+ await waitForLoadingToFinish();
const activateButton = container.querySelector('button[disabled]');
expect(activateButton).toHaveTextContent('Deactivate (Managed By LDAP)');
@@ -133,7 +128,7 @@ describe('SystemUserDetail', () => {
};
const {container} = renderWithContext();
- await waitForLoadingToFinish(container);
+ await waitForLoadingToFinish();
expect(container).toMatchSnapshot();
});
diff --git a/webapp/channels/src/components/admin_console/team_channel_settings/channel/details/channel_modes.tsx b/webapp/channels/src/components/admin_console/team_channel_settings/channel/details/channel_modes.tsx
index df511ebe133..848b76f1aab 100644
--- a/webapp/channels/src/components/admin_console/team_channel_settings/channel/details/channel_modes.tsx
+++ b/webapp/channels/src/components/admin_console/team_channel_settings/channel/details/channel_modes.tsx
@@ -21,7 +21,7 @@ interface Props {
policyEnforcedToggleAvailable: boolean;
}
-const SyncGroupsToggle: React.SFC = (props: Props): JSX.Element => {
+const SyncGroupsToggle = (props: Props): JSX.Element => {
const {isPublic, isSynced, isDefault, onToggle, isDisabled, policyEnforced} = props;
return (
= (props: Props): JSX.Element => {
);
};
-const AllowAllToggle: React.SFC = (props: Props): JSX.Element | null => {
+const AllowAllToggle = (props: Props): JSX.Element | null => {
const {isPublic, isSynced, isDefault, onToggle, isDisabled, policyEnforced} = props;
if (isSynced) {
return null;
@@ -113,7 +113,7 @@ const AllowAllToggle: React.SFC = (props: Props): JSX.Element | null => {
);
};
-const PolicyEnforceToggle: React.SFC = (props: Props): JSX.Element | null => {
+const PolicyEnforceToggle = (props: Props): JSX.Element | null => {
const {isPublic, isSynced, isDefault, onToggle, isDisabled, policyEnforced, policyEnforcedToggleAvailable} = props;
if (isSynced) {
return null;
@@ -152,7 +152,7 @@ const PolicyEnforceToggle: React.SFC = (props: Props): JSX.Element | null
);
};
-export const ChannelModes: React.SFC = (props: Props): JSX.Element => {
+export const ChannelModes = (props: Props): JSX.Element => {
const {isPublic, isSynced, isDefault, onToggle, isDisabled, groupsSupported, policyEnforced, policyEnforcedToggleAvailable, abacSupported} = props;
return (
(
+ link: (msg) => (
{chunks},
+ b: (chunks) => {chunks},
}}
/>
@@ -157,7 +157,7 @@ export function TeamProfile({team, isArchived, onToggleArchive, isDisabled, save
id='admin.teamSettings.teamDetail.teamDescription'
defaultMessage='Team Description:'
values={{
- b: (chunks: string) => {chunks},
+ b: (chunks) => {chunks},
}}
/>
diff --git a/webapp/channels/src/components/admin_console/team_channel_settings/users_to_be_removed_modal.tsx b/webapp/channels/src/components/admin_console/team_channel_settings/users_to_be_removed_modal.tsx
index f9716cb70d3..ef9ee67fa81 100644
--- a/webapp/channels/src/components/admin_console/team_channel_settings/users_to_be_removed_modal.tsx
+++ b/webapp/channels/src/components/admin_console/team_channel_settings/users_to_be_removed_modal.tsx
@@ -47,7 +47,7 @@ const UsersToBeRemovedModal = ({total, scope, scopeId, users, onExited}: Props)
defaultMessage='{total, number} {total, plural, one {User} other {Users}} To Be Removed'
values={{
total,
- b: (chunks: string) => {chunks},
+ b: (chunks) => {chunks},
}}
/>
);
diff --git a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx
index 64da39c015a..ff853527988 100644
--- a/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx
+++ b/webapp/channels/src/components/advanced_text_editor/advanced_text_editor.test.tsx
@@ -13,7 +13,7 @@ import type {FileUpload} from 'components/file_upload/file_upload';
import type Textbox from 'components/textbox/textbox';
import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
-import {renderWithContext, userEvent, screen, act} from 'tests/react_testing_utils';
+import {renderWithContext, userEvent, screen} from 'tests/react_testing_utils';
import Constants, {Locations, StoragePrefixes} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
@@ -201,9 +201,7 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
);
const textbox = screen.getByTestId('post_textbox');
- await act(async () => {
- userEvent.type(textbox, 'something{esc}');
- });
+ await userEvent.type(textbox, 'something{escape}');
expect(textbox).not.toHaveFocus();
expect(mockedUpdateDraft).not.toHaveBeenCalled();
@@ -230,17 +228,15 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
}),
);
const textbox = screen.getByTestId('edit_textbox');
- await act(async () => {
- userEvent.type(textbox, 'something{esc}');
- });
+ await userEvent.type(textbox, 'something{escape}', {advanceTimers: jest.advanceTimersByTime});
expect(textbox).not.toHaveFocus();
// save is called with a short delayed after pressing escape key
- act(() => {
- jest.advanceTimersByTime(Constants.SAVE_DRAFT_TIMEOUT + 50);
- });
+ jest.advanceTimersByTime(Constants.SAVE_DRAFT_TIMEOUT + 50);
expect(mockedRemoveDraft).toHaveBeenCalled();
expect(mockedUpdateDraft).not.toHaveBeenCalled();
+
+ jest.useRealTimers();
});
});
@@ -269,14 +265,13 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
expect(screen.getByPlaceholderText('Write to Test Channel')).toHaveValue('original draft');
- await act(async () => {
- rerender(
- ,
- );
- });
+ rerender(
+ ,
+ );
+
expect(screen.getByPlaceholderText('Write to Other Channel')).toHaveValue('a different draft');
});
@@ -288,20 +283,16 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
initialState,
);
- await act(async () => {
- userEvent.type(screen.getByPlaceholderText('Write to Test Channel'), 'some text');
- });
+ await userEvent.type(screen.getByPlaceholderText('Write to Test Channel'), 'some text');
expect(mockedUpdateDraft).not.toHaveBeenCalled();
- await act(async () => {
- rerender(
- ,
- );
- });
+ rerender(
+ ,
+ );
expect(mockedUpdateDraft).toHaveBeenCalled();
expect(mockedUpdateDraft.mock.calls[0][1]).toMatchObject({
@@ -330,14 +321,12 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
expect(mockedUpdateDraft).not.toHaveBeenCalled();
- await act(async () => {
- rerender(
- ,
- );
- });
+ rerender(
+ ,
+ );
expect(mockedUpdateDraft).not.toHaveBeenCalled();
});
@@ -360,20 +349,16 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
}),
);
- await act(async () => {
- userEvent.type(screen.getByPlaceholderText('Write to Test Channel'), ' plus some new text');
- });
+ await userEvent.type(screen.getByPlaceholderText('Write to Test Channel'), ' plus some new text');
expect(mockedUpdateDraft).not.toHaveBeenCalled();
- await act(async () => {
- rerender(
- ,
- );
- });
+ rerender(
+ ,
+ );
expect(mockedUpdateDraft).toHaveBeenCalled();
expect(mockedUpdateDraft.mock.calls[0][1]).toMatchObject({
@@ -400,21 +385,17 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
}),
);
- await act(async () => {
- userEvent.clear(screen.getByPlaceholderText('Write to Test Channel'));
- });
+ await userEvent.clear(screen.getByPlaceholderText('Write to Test Channel'));
expect(mockedRemoveDraft).not.toHaveBeenCalled();
expect(mockedUpdateDraft).not.toHaveBeenCalled();
- await act(async () => {
- rerender(
- ,
- );
- });
+ rerender(
+ ,
+ );
expect(mockedRemoveDraft).toHaveBeenCalled();
expect(mockedUpdateDraft).not.toHaveBeenCalled();
@@ -431,14 +412,12 @@ describe('components/avanced_text_editor/advanced_text_editor', () => {
expect(mockedRemoveDraft).not.toHaveBeenCalled();
expect(mockedUpdateDraft).not.toHaveBeenCalled();
- await act(async () => {
- rerender(
- ,
- );
- });
+ rerender(
+ ,
+ );
expect(mockedRemoveDraft).not.toHaveBeenCalled();
expect(mockedUpdateDraft).not.toHaveBeenCalled();
diff --git a/webapp/channels/src/components/advanced_text_editor/edit_post_footer.tsx b/webapp/channels/src/components/advanced_text_editor/edit_post_footer.tsx
index 4f095471c2c..d05c06b2649 100644
--- a/webapp/channels/src/components/advanced_text_editor/edit_post_footer.tsx
+++ b/webapp/channels/src/components/advanced_text_editor/edit_post_footer.tsx
@@ -51,7 +51,7 @@ export default function EditPostFooter(props: Props) {
defaultMessage='{key}ENTER to Save, ESC to Cancel'
values={{
key: sendOnCtrlEnter ? ctrlSendKey : '',
- strong: (x: string) => {x},
+ strong: (x) => {x},
}}
/>
diff --git a/webapp/channels/src/components/advanced_text_editor/formatting_bar/formatting_bar.test.tsx b/webapp/channels/src/components/advanced_text_editor/formatting_bar/formatting_bar.test.tsx
index e0c7afc4301..18a053ea510 100644
--- a/webapp/channels/src/components/advanced_text_editor/formatting_bar/formatting_bar.test.tsx
+++ b/webapp/channels/src/components/advanced_text_editor/formatting_bar/formatting_bar.test.tsx
@@ -63,7 +63,7 @@ describe('FormattingBar', () => {
expect(screen.queryByLabelText('show hidden formatting options')).not.toBeInTheDocument();
});
- test('MM-56705 should not submit form when clicking on hidden formatting button', () => {
+ test('MM-56705 should not submit form when clicking on hidden formatting button', async () => {
jest.spyOn(Hooks, 'useFormattingBarControls').mockReturnValue({wideMode: 'narrow', ...splitFormattingBarControls('narrow')});
const onSubmit = jest.fn();
@@ -76,13 +76,13 @@ describe('FormattingBar', () => {
expect(screen.queryByLabelText('heading')).toBe(null);
- userEvent.click(screen.getByLabelText('show hidden formatting options'));
+ await userEvent.click(screen.getByLabelText('show hidden formatting options'));
expect(screen.queryByLabelText('heading')).toBeVisible();
expect(onSubmit).not.toHaveBeenCalled();
});
- test('should disable tooltip when hidden controls are shown', () => {
+ test('should disable tooltip when hidden controls are shown', async () => {
jest.spyOn(Hooks, 'useFormattingBarControls').mockReturnValue({wideMode: 'narrow', ...splitFormattingBarControls('narrow')});
const {container} = renderWithContext(
@@ -92,7 +92,7 @@ describe('FormattingBar', () => {
const hiddenControlsButton = screen.getByLabelText('show hidden formatting options');
// Click to show hidden controls
- userEvent.click(hiddenControlsButton);
+ await userEvent.click(hiddenControlsButton);
// Find the WithTooltip component and verify it has disabled prop
const tooltipWrapper = container.querySelector('.tooltipContainer');
diff --git a/webapp/channels/src/components/advanced_text_editor/formatting_bar/formatting_bar.tsx b/webapp/channels/src/components/advanced_text_editor/formatting_bar/formatting_bar.tsx
index 5c2a9b2528d..b8012ad273b 100644
--- a/webapp/channels/src/components/advanced_text_editor/formatting_bar/formatting_bar.tsx
+++ b/webapp/channels/src/components/advanced_text_editor/formatting_bar/formatting_bar.tsx
@@ -12,7 +12,7 @@ import {DotsHorizontalIcon} from '@mattermost/compass-icons/components';
import WithTooltip from 'components/with_tooltip';
-import type {ApplyMarkdownOptions} from 'utils/markdown/apply_markdown';
+import type {ApplyMarkdownOptions, MarkdownMode} from 'utils/markdown/apply_markdown';
import FormattingIcon, {IconContainer} from './formatting_icon';
import {useFormattingBarControls} from './hooks';
@@ -178,7 +178,7 @@ const FormattingBar = (props: FormattingBarProps): JSX.Element => {
* function signature as if we would define it directly in the props of
* the FormattingIcon component. This should improve render-performance
*/
- const makeFormattingHandler = useCallback((mode) => () => {
+ const makeFormattingHandler = useCallback((mode: MarkdownMode) => () => {
// if the formatting is disabled just return without doing anything
if (disableControls) {
return;
diff --git a/webapp/channels/src/components/advanced_text_editor/send_button/send_post_options/core_menu_options.tsx b/webapp/channels/src/components/advanced_text_editor/send_button/send_post_options/core_menu_options.tsx
index 1a70aa31ff5..5e8941bfd7d 100644
--- a/webapp/channels/src/components/advanced_text_editor/send_button/send_post_options/core_menu_options.tsx
+++ b/webapp/channels/src/components/advanced_text_editor/send_button/send_post_options/core_menu_options.tsx
@@ -85,7 +85,7 @@ function CoreMenuOptions({handleOnSelect, channelId}: Props) {
extraProps.trailingElements = teammateTimeDisplay;
}
- const tomorrowClickHandler = useCallback((e) => handleOnSelect(e, tomorrow9amTime), [handleOnSelect, tomorrow9amTime]);
+ const tomorrowClickHandler = useCallback((e: React.UIEvent) => handleOnSelect(e, tomorrow9amTime), [handleOnSelect, tomorrow9amTime]);
const optionTomorrow = (
);
- const nextMondayClickHandler = useCallback((e) => handleOnSelect(e, nextMonday), [handleOnSelect, nextMonday]);
+ const nextMondayClickHandler = useCallback((e: React.UIEvent) => handleOnSelect(e, nextMonday), [handleOnSelect, nextMonday]);
const optionNextMonday = (
handleOnSelect(e, recentlyUsedCustomDateVal.timestamp!), [handleOnSelect, recentlyUsedCustomDateVal.timestamp]);
+ const handleRecentlyUsedCustomTime = useCallback((e: React.UIEvent) => handleOnSelect(e, recentlyUsedCustomDateVal.timestamp!), [handleOnSelect, recentlyUsedCustomDateVal.timestamp]);
if (
!shouldShowRecentlyUsedCustomTime(now.toMillis(), recentlyUsedCustomDateVal, userCurrentTimezone, tomorrow9amTime, nextMonday)
diff --git a/webapp/channels/src/components/advanced_text_editor/use_submit.tsx b/webapp/channels/src/components/advanced_text_editor/use_submit.tsx
index e61baa77717..8e282065b9d 100644
--- a/webapp/channels/src/components/advanced_text_editor/use_submit.tsx
+++ b/webapp/channels/src/components/advanced_text_editor/use_submit.tsx
@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import type React from 'react';
-import {useCallback, useRef, useState} from 'react';
+import {useCallback, useMemo, useRef, useState} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import type {ServerError} from '@mattermost/types/errors';
@@ -12,7 +12,7 @@ import {FileTypes} from 'mattermost-redux/action_types';
import {getChannelTimezones} from 'mattermost-redux/actions/channels';
import {Permissions} from 'mattermost-redux/constants';
import {getChannel, getAllChannelStats} from 'mattermost-redux/selectors/entities/channels';
-import {getFilesIdsForPost} from 'mattermost-redux/selectors/entities/files';
+import {makeGetFileIdsForPost} from 'mattermost-redux/selectors/entities/files';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
@@ -79,6 +79,7 @@ const useSubmit = (
const dispatch = useDispatch();
+ const getFilesIdsForPost = useMemo(makeGetFileIdsForPost, []);
const postFileIds = useSelector((state: GlobalState) => getFilesIdsForPost(state, postId || ''));
const isDraftSubmitting = useRef(false);
diff --git a/webapp/channels/src/components/analytics/system_analytics/system_analytics.test.tsx b/webapp/channels/src/components/analytics/system_analytics/system_analytics.test.tsx
index f526e9682c7..85d6d5b2358 100644
--- a/webapp/channels/src/components/analytics/system_analytics/system_analytics.test.tsx
+++ b/webapp/channels/src/components/analytics/system_analytics/system_analytics.test.tsx
@@ -1,13 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {fireEvent} from '@testing-library/react';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import SystemAnalytics from 'components/analytics/system_analytics';
-import {renderWithContext, screen} from 'tests/react_testing_utils';
+import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import Constants from 'utils/constants';
const StatTypes = Constants.StatTypes;
@@ -92,7 +91,7 @@ describe('components/analytics/system_analytics/system_analytics.tsx', () => {
renderWithContext(, state, {useMockedStore: true});
const detailsElement = screen.getByText('Load Advanced Statistics');
- fireEvent.click(detailsElement);
+ await userEvent.click(detailsElement);
await screen.findByTestId('totalPostsLineChart');
@@ -237,12 +236,8 @@ describe('components/analytics/system_analytics/system_analytics.tsx', () => {
renderWithContext(, state, {useMockedStore: true});
- await new Promise(process.nextTick);
-
const detailsElement = screen.getByText('Load Advanced Statistics');
- fireEvent.click(detailsElement);
-
- await screen.findByTestId('totalPostsLineChart');
+ await userEvent.click(detailsElement);
expect(screen.getByTestId('totalPosts')).toHaveTextContent('45');
expect(screen.getByTestId('totalPostsLineChart')).toBeInTheDocument();
diff --git a/webapp/channels/src/components/announcement_bar/configuration_bar/configuration_bar.tsx b/webapp/channels/src/components/announcement_bar/configuration_bar/configuration_bar.tsx
index d0a1e32d892..e34adc32f5f 100644
--- a/webapp/channels/src/components/announcement_bar/configuration_bar/configuration_bar.tsx
+++ b/webapp/channels/src/components/announcement_bar/configuration_bar/configuration_bar.tsx
@@ -251,8 +251,8 @@ const ConfigurationAnnouncementBar = (props: Props) => {
}
if (props.canViewSystemErrors && props.config?.SiteURL === '') {
- const values: Record = {
- linkSite: (msg: string) => (
+ const values = {
+ linkSite: (msg: ReactNode[]) => (
{
{msg}
),
- linkConsole: (msg: string) => (
+ linkConsole: (msg: ReactNode[]) => (
{msg}
diff --git a/webapp/channels/src/components/announcement_bar/no_internet_connection/no_internet_connection.tsx b/webapp/channels/src/components/announcement_bar/no_internet_connection/no_internet_connection.tsx
index 6fffb4c613f..f5ea625c40b 100644
--- a/webapp/channels/src/components/announcement_bar/no_internet_connection/no_internet_connection.tsx
+++ b/webapp/channels/src/components/announcement_bar/no_internet_connection/no_internet_connection.tsx
@@ -42,7 +42,7 @@ const NoInternetConnection: React.FC = (props: NoInte
id='announcement_bar.warn.contact_support_email'
defaultMessage='Contact support.'
values={{
- a: (chunks: string) => (
+ a: (chunks) => (
{
expect(screen.getByText('We need your permission to show notifications in the browser.')).toBeInTheDocument();
- await waitFor(async () => {
- userEvent.click(screen.getByText('Manage notification preferences'));
- });
+ await userEvent.click(screen.getByText('Manage notification preferences'));
expect(utilsNotifications.requestNotificationPermission).toHaveBeenCalled();
expect(screen.queryByText('We need your permission to show browser notifications.')).not.toBeInTheDocument();
diff --git a/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.test.tsx b/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.test.tsx
index dbb0bb06014..ee18430cac9 100644
--- a/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.test.tsx
+++ b/webapp/channels/src/components/announcement_bar/renewal_link/renewal_link.test.tsx
@@ -3,10 +3,10 @@
import type {ReactWrapper} from 'enzyme';
import React from 'react';
-import {act} from 'react-dom/test-utils';
import {Provider} from 'react-redux';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
+import {act} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store';
import RenewalLink from './renewal_link';
diff --git a/webapp/channels/src/components/apps_form/apps_form_component.test.tsx b/webapp/channels/src/components/apps_form/apps_form_component.test.tsx
index 56c574dd23f..3222b23b849 100644
--- a/webapp/channels/src/components/apps_form/apps_form_component.test.tsx
+++ b/webapp/channels/src/components/apps_form/apps_form_component.test.tsx
@@ -182,7 +182,7 @@ describe('AppsFormComponent', () => {
const fields = [selectField];
const props = {
...baseProps,
- context: {},
+ appsContext: {},
form: {
fields,
},
@@ -1117,7 +1117,7 @@ describe('AppsFormComponent', () => {
});
describe('onHide and Modal Behavior', () => {
- test('should call onHide prop when onHide is triggered', () => {
+ test('should call onHide prop when onHide is triggered', async () => {
const mockOnHide = jest.fn();
const props = {
@@ -1128,7 +1128,7 @@ describe('AppsFormComponent', () => {
renderWithContext();
const cancelButton = screen.getByRole('button', {name: /cancel/i});
- userEvent.click(cancelButton);
+ await userEvent.click(cancelButton);
expect(mockOnHide).toHaveBeenCalled();
});
@@ -1145,8 +1145,8 @@ describe('AppsFormComponent', () => {
}).not.toThrow();
const cancelButton = screen.getByRole('button', {name: /cancel/i});
- expect(() => {
- userEvent.click(cancelButton);
+ expect(async () => {
+ await userEvent.click(cancelButton);
}).not.toThrow();
});
@@ -1168,8 +1168,8 @@ describe('AppsFormComponent', () => {
expect(cancelButton).toBeInTheDocument();
// Clicking cancel should handle submit_on_cancel logic
- expect(() => {
- userEvent.click(cancelButton);
+ expect(async () => {
+ await userEvent.click(cancelButton);
}).not.toThrow();
});
});
diff --git a/webapp/channels/src/components/apps_form/apps_form_container.test.tsx b/webapp/channels/src/components/apps_form/apps_form_container.test.tsx
index 6a4b0408c09..163dfb48a47 100644
--- a/webapp/channels/src/components/apps_form/apps_form_container.test.tsx
+++ b/webapp/channels/src/components/apps_form/apps_form_container.test.tsx
@@ -13,7 +13,7 @@ import {RawAppsFormContainer} from './apps_form_container';
describe('components/apps_form/AppsFormContainer', () => {
const emojiMap = new EmojiMap(new Map());
- const context = {
+ const appContext = {
app_id: 'app',
channel_id: 'channel',
team_id: 'team',
@@ -51,7 +51,7 @@ describe('components/apps_form/AppsFormContainer', () => {
path: '/form_url',
},
},
- context,
+ appContext,
actions: {
doAppSubmit: jest.fn().mockResolvedValue({}),
doAppFetchForm: jest.fn(),
diff --git a/webapp/channels/src/components/apps_form/apps_form_container.tsx b/webapp/channels/src/components/apps_form/apps_form_container.tsx
index 40acca779fa..856a3ddfe5e 100644
--- a/webapp/channels/src/components/apps_form/apps_form_container.tsx
+++ b/webapp/channels/src/components/apps_form/apps_form_container.tsx
@@ -18,7 +18,7 @@ import AppsForm from './apps_form_component';
type Props = {
intl: IntlShape;
form?: AppForm;
- context?: AppContext;
+ appContext?: AppContext;
timezone?: string;
onExited: () => void;
onHide?: () => void;
@@ -59,11 +59,11 @@ class AppsFormContainer extends React.PureComponent {
const errMsg = this.props.intl.formatMessage({id: 'apps.error.form.no_submit', defaultMessage: '`submit` is not defined'});
return {error: makeCallErrorResponse(makeErrorMsg(errMsg))};
}
- if (!this.props.context) {
+ if (!this.props.appContext) {
return {error: makeCallErrorResponse('unreachable: empty context')};
}
- const creq = createCallRequest(form.submit, this.props.context, {}, submission.values);
+ const creq = createCallRequest(form.submit, this.props.appContext, {}, submission.values);
const res = await this.props.actions.doAppSubmit(creq, this.props.intl) as DoAppCallResult;
if (res.error) {
return res;
@@ -127,11 +127,11 @@ class AppsFormContainer extends React.PureComponent {
defaultMessage: 'Called refresh on no refresh field.',
})))};
}
- if (!this.props.context) {
+ if (!this.props.appContext) {
return {error: makeCallErrorResponse('unreachable: empty context')};
}
- const creq = createCallRequest(form.source, this.props.context, {}, values);
+ const creq = createCallRequest(form.source, this.props.appContext, {}, values);
creq.selected_field = field.name;
const res = await this.props.actions.doAppFetchForm(creq, this.props.intl);
@@ -180,11 +180,11 @@ class AppsFormContainer extends React.PureComponent {
defaultMessage: '`lookup` is not defined.',
})))};
}
- if (!this.props.context) {
+ if (!this.props.appContext) {
return {error: makeCallErrorResponse('unreachable: empty context')};
}
- const creq = createCallRequest(field.lookup, this.props.context, {}, values);
+ const creq = createCallRequest(field.lookup, this.props.appContext, {}, values);
creq.selected_field = field.name;
creq.query = userInput;
@@ -194,7 +194,7 @@ class AppsFormContainer extends React.PureComponent {
render() {
const {form} = this.state;
- if (!form?.submit || !this.props.context) {
+ if (!form?.submit || !this.props.appContext) {
return null;
}
diff --git a/webapp/channels/src/components/async_load.tsx b/webapp/channels/src/components/async_load.tsx
index da72a5c01eb..c1ed5830cd7 100644
--- a/webapp/channels/src/components/async_load.tsx
+++ b/webapp/channels/src/components/async_load.tsx
@@ -1,14 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React, {lazy, type ComponentType} from 'react';
+import React, {lazy} from 'react';
import type {PluggableComponentType, PluggableProps} from 'plugins/pluggable/pluggable';
import type {PluginsState, ProductSubComponentNames} from 'types/store/plugins';
export function makeAsyncComponent(displayName: string, LazyComponent: React.ComponentType, fallback: React.ReactNode = null) {
- const Component: ComponentType = (props) => (
+ const Component = (props: ComponentProps & React.JSX.IntrinsicAttributes) => (
diff --git a/webapp/channels/src/components/at_sum_members_mention/notification_from_members_modal.tsx b/webapp/channels/src/components/at_sum_members_mention/notification_from_members_modal.tsx
index a027b35ac6a..41768541e13 100644
--- a/webapp/channels/src/components/at_sum_members_mention/notification_from_members_modal.tsx
+++ b/webapp/channels/src/components/at_sum_members_mention/notification_from_members_modal.tsx
@@ -23,7 +23,8 @@ import {openDirectChannelToUserId} from 'actions/channel_actions';
import {closeModal} from 'actions/views/modals';
import {isModalOpen} from 'selectors/views/modals';
-import MemberList from 'components/channel_members_rhs/member_list';
+import type {ListItem} from 'components/channel_members_rhs/member_list';
+import MemberList, {ListItemType} from 'components/channel_members_rhs/member_list';
import {ModalIdentifiers} from 'utils/constants';
import {mapFeatureIdToTranslation} from 'utils/notify_admin_utils';
@@ -44,17 +45,6 @@ export interface ChannelMember {
displayName: string;
}
-enum ListItemType {
- Member = 'member',
- FirstSeparator = 'first-separator',
- Separator = 'separator',
-}
-
-export interface ListItem {
- type: ListItemType;
- data: ChannelMember | JSX.Element;
-}
-
const MembersContainer = styled.div`
flex: 1 1 auto;
padding: 0 4px 16px;
diff --git a/webapp/channels/src/components/authorize/authorize.tsx b/webapp/channels/src/components/authorize/authorize.tsx
index 78673d91c0d..aba7af8629c 100644
--- a/webapp/channels/src/components/authorize/authorize.tsx
+++ b/webapp/channels/src/components/authorize/authorize.tsx
@@ -134,7 +134,7 @@ export default class Authorize extends React.PureComponent {
defaultMessage='Authorize {appName} to Connect to Your Mattermost User Account'
values={{
appName: app.name,
- b: (chunks: string) => {chunks},
+ b: (chunks) => {chunks},
}}
/>
@@ -145,7 +145,7 @@ export default class Authorize extends React.PureComponent {
defaultMessage='The app {appName} would like the ability to access and modify your basic information.'
values={{
appName: app.name,
- b: (chunks: string) => {chunks},
+ b: (chunks) => {chunks},
}}
/>
@@ -155,7 +155,7 @@ export default class Authorize extends React.PureComponent {
defaultMessage='Allow {appName} access?'
values={{
appName: app.name,
- b: (chunks: string) => {chunks},
+ b: (chunks) => {chunks},
}}
/>
diff --git a/webapp/channels/src/components/autocomplete_selector.tsx b/webapp/channels/src/components/autocomplete_selector.tsx
index cae91968ff4..72d081dce5b 100644
--- a/webapp/channels/src/components/autocomplete_selector.tsx
+++ b/webapp/channels/src/components/autocomplete_selector.tsx
@@ -28,7 +28,6 @@ type Props = {
inputClassName: string;
helpText?: React.ReactNode | string;
placeholder?: string;
- footer?: Node;
disabled?: boolean;
toggleFocus?: ((focus: boolean) => void) | null;
listComponent: typeof SuggestionList | typeof ModalSuggestionList;
@@ -110,7 +109,6 @@ export default class AutocompleteSelector extends React.PureComponent
{helpTextContent}
- {footer}
);
diff --git a/webapp/channels/src/components/channel_bookmarks/bookmark_delete_modal.tsx b/webapp/channels/src/components/channel_bookmarks/bookmark_delete_modal.tsx
index c9f2c2052c8..97dae087e94 100644
--- a/webapp/channels/src/components/channel_bookmarks/bookmark_delete_modal.tsx
+++ b/webapp/channels/src/components/channel_bookmarks/bookmark_delete_modal.tsx
@@ -38,7 +38,7 @@ function BookmarkDeleteModal({
id={'channel_bookmarks.confirm.delete.text'}
defaultMessage={'Are you sure you want to delete the bookmark {displayName}?'}
values={{
- strong: (chunk: string) => {chunk},
+ strong: (chunk) => {chunk},
displayName,
}}
/>
diff --git a/webapp/channels/src/components/channel_bookmarks/create_modal_name_input.tsx b/webapp/channels/src/components/channel_bookmarks/create_modal_name_input.tsx
index fdfee38862a..fafbb5b56e9 100644
--- a/webapp/channels/src/components/channel_bookmarks/create_modal_name_input.tsx
+++ b/webapp/channels/src/components/channel_bookmarks/create_modal_name_input.tsx
@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import type {ComponentProps} from 'react';
import React, {useCallback, useRef} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import styled from 'styled-components';
@@ -89,7 +88,7 @@ const CreateModalNameInput = ({
setEmoji('');
};
- const handleInputChange: ComponentProps['onChange'] = useCallback((e) => {
+ const handleInputChange = useCallback((e: React.ChangeEvent) => {
setDisplayName(e.currentTarget.value);
}, []);
diff --git a/webapp/channels/src/components/channel_header/components/header_icon_wrapper.test.tsx b/webapp/channels/src/components/channel_header/components/header_icon_wrapper.test.tsx
index 679e175e0ab..f250a56495c 100644
--- a/webapp/channels/src/components/channel_header/components/header_icon_wrapper.test.tsx
+++ b/webapp/channels/src/components/channel_header/components/header_icon_wrapper.test.tsx
@@ -34,7 +34,7 @@ describe('components/channel_header/components/HeaderIconWrapper', () => {
expect(screen.getByLabelText('Recent mentions')).toBeVisible();
expect(screen.queryByText('Recent mentions')).not.toBeInTheDocument();
- userEvent.hover(screen.getByLabelText('Recent mentions'));
+ await userEvent.hover(screen.getByLabelText('Recent mentions'));
await waitFor(() => {
expect(screen.queryByText('Recent mentions')).toBeInTheDocument();
@@ -55,7 +55,7 @@ describe('components/channel_header/components/HeaderIconWrapper', () => {
expect(screen.queryByText('b')).not.toBeInTheDocument();
expect(screen.queryByText('c')).not.toBeInTheDocument();
- userEvent.hover(screen.getByLabelText('Recent mentions'));
+ await userEvent.hover(screen.getByLabelText('Recent mentions'));
await waitFor(() => {
expect(screen.queryByText('Recent mentions')).toBeInTheDocument();
diff --git a/webapp/channels/src/components/channel_header/index.ts b/webapp/channels/src/components/channel_header/index.ts
index 94eb77eb314..40e388efc87 100644
--- a/webapp/channels/src/components/channel_header/index.ts
+++ b/webapp/channels/src/components/channel_header/index.ts
@@ -3,7 +3,6 @@
import type {ConnectedProps} from 'react-redux';
import {connect} from 'react-redux';
-import {withRouter} from 'react-router-dom';
import {bindActionCreators} from 'redux';
import type {Dispatch} from 'redux';
@@ -127,4 +126,4 @@ const connector = connect(makeMapStateToProps, mapDispatchToProps);
export type PropsFromRedux = ConnectedProps;
-export default withRouter(connector(ChannelHeader));
+export default connector(ChannelHeader);
diff --git a/webapp/channels/src/components/channel_invite_modal/channel_invite_modal.test.tsx b/webapp/channels/src/components/channel_invite_modal/channel_invite_modal.test.tsx
index 6c6a98868c8..c512bcc4227 100644
--- a/webapp/channels/src/components/channel_invite_modal/channel_invite_modal.test.tsx
+++ b/webapp/channels/src/components/channel_invite_modal/channel_invite_modal.test.tsx
@@ -1,10 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {fireEvent, screen} from '@testing-library/react';
+import {fireEvent, screen, waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
-import {act} from 'react-dom/test-utils';
import {GenericModal} from '@mattermost/components';
import type {Channel} from '@mattermost/types/channels';
@@ -18,7 +17,7 @@ import ChannelInviteModal from 'components/channel_invite_modal/channel_invite_m
import type {Value} from 'components/multiselect/multiselect';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
-import {renderWithContext} from 'tests/react_testing_utils';
+import {act, renderWithContext} from 'tests/react_testing_utils';
type UserProfileValue = Value & UserProfile;
@@ -291,41 +290,33 @@ describe('components/channel_invite_modal', () => {
membersInTeam: {'user-1': {user_id: 'user-1', team_id: channel.team_id, roles: '', delete_at: 0, scheme_admin: false, scheme_guest: false, scheme_user: true, mention_count: 0, mention_count_root: 0, msg_count: 0, msg_count_root: 0} as TeamMembership},
};
- await act(async () => {
- const {getByText} = renderWithContext(
- ,
- );
+ const {getByText} = renderWithContext(
+ ,
+ );
- // First, we need to simulate selecting a user
- const input = screen.getByRole('combobox', {name: /search for people/i});
+ // First, we need to simulate selecting a user
+ const input = screen.getByRole('combobox', {name: /search for people/i});
- // Type the search term
- await userEvent.type(input, 'user-1');
+ // Type the search term
+ await userEvent.type(input, 'user-1');
- // Wait for the promise to resolve
- await act(async () => {
- // Wait for the dropdown option to appear
- const option = await screen.findByText('user-1');
+ // Wait for the dropdown option to appear
+ const option = await screen.findByText('user-1', {selector: '.more-modal__name > span'});
- // Click the option
- userEvent.click(option);
+ // Click the option
+ await userEvent.click(option);
- // Confirm that the user is now displayed in the selected users
- expect(screen.getByText('user-1')).toBeInTheDocument();
+ // Confirm that the user is now displayed in the selected users
+ expect(screen.getByText('user-1')).toBeInTheDocument();
- // Find and click the save button
- const saveButton = getByText('Add');
- fireEvent.click(saveButton);
- });
+ // Find and click the save button
+ const saveButton = getByText('Add');
+ await userEvent.click(saveButton);
- // Wait for the promise to resolve
- await act(async () => {
- await new Promise((resolve) => setTimeout(resolve, 0));
- });
-
- // Check that addUsersToChannel was called
+ // Check that addUsersToChannel was called
+ await waitFor(() => {
expect(addUsersToChannelMock).toHaveBeenCalled();
});
});
@@ -347,41 +338,33 @@ describe('components/channel_invite_modal', () => {
membersInTeam: {'user-1': {user_id: 'user-1', team_id: channel.team_id, roles: '', delete_at: 0, scheme_admin: false, scheme_guest: false, scheme_user: true, mention_count: 0, mention_count_root: 0, msg_count: 0, msg_count_root: 0} as TeamMembership},
};
- await act(async () => {
- const {getByText} = renderWithContext(
- ,
- );
+ const {getByText} = renderWithContext(
+ ,
+ );
- // First, we need to simulate selecting a user
- const input = screen.getByRole('combobox', {name: /search for people/i});
+ // First, we need to simulate selecting a user
+ const input = screen.getByRole('combobox', {name: /search for people/i});
- // Type the search term
- await userEvent.type(input, 'user-1');
+ // Type the search term
+ await userEvent.type(input, 'user-1');
- // Wait for the promise to resolve
- await act(async () => {
- // Wait for the dropdown option to appear
- const option = await screen.findByText('user-1');
+ // Wait for the dropdown option to appear
+ const option = await screen.findByText('user-1', {selector: '.more-modal__name > span'});
- // Click the option
- userEvent.click(option);
+ // Click the option
+ await userEvent.click(option);
- // Confirm that the user is now displayed in the selected users
- expect(screen.getByText('user-1')).toBeInTheDocument();
+ // Confirm that the user is now displayed in the selected users
+ expect(screen.getByText('user-1')).toBeInTheDocument();
- // Find and click the save button
- const saveButton = getByText('Add');
- fireEvent.click(saveButton);
- });
+ // Find and click the save button
+ const saveButton = getByText('Add');
+ await userEvent.click(saveButton);
- // Wait for the promise to resolve
- await act(async () => {
- await new Promise((resolve) => setTimeout(resolve, 0));
- });
-
- // Check that addUsersToChannel was called
+ // Check that addUsersToChannel was called
+ await waitFor(() => {
expect(addUsersToChannelMock).toHaveBeenCalled();
});
});
@@ -399,33 +382,29 @@ describe('components/channel_invite_modal', () => {
};
- await act(async () => {
- const {getByText} = renderWithContext(
- ,
- );
+ const {getByText} = renderWithContext(
+ ,
+ );
- // First, we need to simulate selecting a user
- const input = screen.getByRole('combobox', {name: /search for people/i});
+ // First, we need to simulate selecting a user
+ const input = screen.getByRole('combobox', {name: /search for people/i});
- await userEvent.type(input, 'user-1');
+ await userEvent.type(input, 'user-1');
- await act(async () => {
- const option = await screen.findByText('user-1');
+ const option = await screen.findByText('user-1', {selector: '.more-modal__name > span'});
- userEvent.click(option);
+ await userEvent.click(option);
- expect(screen.getByText('user-1')).toBeInTheDocument();
+ expect(screen.getByText('user-1')).toBeInTheDocument();
- const saveButton = getByText('Add');
- fireEvent.click(saveButton);
- });
+ const saveButton = getByText('Add');
+ await userEvent.click(saveButton);
- // Check that onAddCallback was called and addUsersToChannel was not
- expect(onAddCallback).toHaveBeenCalled();
- expect(props.actions.addUsersToChannel).not.toHaveBeenCalled();
- });
+ // Check that onAddCallback was called and addUsersToChannel was not
+ expect(onAddCallback).toHaveBeenCalled();
+ expect(props.actions.addUsersToChannel).not.toHaveBeenCalled();
});
test('should trim the search term', async () => {
@@ -441,25 +420,22 @@ describe('components/channel_invite_modal', () => {
},
};
- await act(async () => {
- renderWithContext(
- ,
- );
+ renderWithContext(
+ ,
+ );
- // Find the search input
- const input = screen.getByRole('combobox', {name: /search for people/i});
+ // Find the search input
+ const input = screen.getByRole('combobox', {name: /search for people/i});
- // Directly trigger the change event with a value that has spaces
+ // Directly trigger the change event with a value that has spaces
+ act(() => {
fireEvent.change(input, {target: {value: ' something '}});
+ });
- // Wait for the search timeout plus some extra time
- await act(async () => {
- await new Promise((resolve) => setTimeout(resolve, 200));
- });
-
- // Verify the search was called with the trimmed term
+ // Verify the search was called with the trimmed term
+ await waitFor(() => {
expect(searchProfilesMock).toHaveBeenCalledWith(
expect.stringContaining('something'),
expect.any(Object),
diff --git a/webapp/channels/src/components/channel_invite_modal/channel_invite_modal.tsx b/webapp/channels/src/components/channel_invite_modal/channel_invite_modal.tsx
index 98112134f6b..7dec9a4b27a 100644
--- a/webapp/channels/src/components/channel_invite_modal/channel_invite_modal.tsx
+++ b/webapp/channels/src/components/channel_invite_modal/channel_invite_modal.tsx
@@ -573,7 +573,7 @@ const ChannelInviteModalComponent = (props: Props) => {
id='channel_invite.no_options_message'
defaultMessage='No matches found - Invite them to the team'
values={{
- InvitationModalLink: (chunks: string) => (
+ InvitationModalLink: (chunks) => (
{
addUserProfile,
} = props;
- const getProfilesByIdsAndUsernames = makeGetProfilesByIdsAndUsernames();
+ const getProfilesByIdsAndUsernames = useMemo(makeGetProfilesByIdsAndUsernames, []);
- const profiles = useSelector((state: GlobalState) => getProfilesByIdsAndUsernames(state, {allUserIds: group.member_ids || [], allUsernames: []}) as UserProfileValue[]);
+ const profiles = useSelector((state: GlobalState) => getProfilesByIdsAndUsernames(state, {allUserIds: group.member_ids}) as UserProfileValue[]);
const overflowNames = useSelector((state: GlobalState) => {
if (group?.member_ids) {
return group?.member_ids.map((userId) => displayNameGetter(state, true)(getUser(state, userId))).join(', ');
diff --git a/webapp/channels/src/components/channel_layout/channel_identifier_router/actions.test.ts b/webapp/channels/src/components/channel_layout/channel_identifier_router/actions.test.ts
index a4c40e071c4..ac97671802b 100644
--- a/webapp/channels/src/components/channel_layout/channel_identifier_router/actions.test.ts
+++ b/webapp/channels/src/components/channel_layout/channel_identifier_router/actions.test.ts
@@ -22,6 +22,8 @@ import TestHelper from 'packages/mattermost-redux/test/test_helper';
import mockStore from 'tests/test_store';
import {joinPrivateChannelPrompt} from 'utils/channel_utils';
+import type {Match} from './channel_identifier_router';
+
jest.mock('actions/global_actions', () => ({
emitChannelClickEvent: jest.fn(),
}));
@@ -137,7 +139,7 @@ describe('Actions', () => {
const testStore = await mockStore(initialState);
const history = {replace: jest.fn()};
- await testStore.dispatch((goToChannelByChannelId({params: {team: 'team1', identifier: 'channel_id3', path: '/'}, url: ''}, history as any) as any));
+ await testStore.dispatch((goToChannelByChannelId({params: {team: 'team1', identifier: 'channel_id3', path: '/'}, url: ''} as Match, history as any)));
expect(joinChannel).toHaveBeenCalledWith('current_user_id', 'team_id1', 'channel_id3', '');
expect(history.replace).toHaveBeenCalledWith('/team1/channels/achannel3');
});
@@ -147,14 +149,14 @@ describe('Actions', () => {
test('switch to channel on different team with same name', async () => {
const testStore = await mockStore(initialState);
- await testStore.dispatch((goToChannelByChannelName({params: {team: 'team2', identifier: 'achannel', path: '/'}, url: ''}, {} as any) as any));
+ await testStore.dispatch((goToChannelByChannelName({params: {team: 'team2', identifier: 'achannel', path: '/'}, url: ''} as Match, {} as any)));
expect(emitChannelClickEvent).toHaveBeenCalledWith(channel2);
});
test('switch to public channel we have locally but need to join', async () => {
const testStore = await mockStore(initialState);
- await testStore.dispatch((goToChannelByChannelName({params: {team: 'team1', identifier: 'achannel3', path: '/'}, url: ''}, {} as any) as any));
+ await testStore.dispatch((goToChannelByChannelName({params: {team: 'team1', identifier: 'achannel3', path: '/'}, url: ''} as Match, {} as any)));
expect(joinChannel).toHaveBeenCalledWith('current_user_id', 'team_id1', 'channel_id3', 'achannel3');
expect(emitChannelClickEvent).toHaveBeenCalledWith(channel3);
});
@@ -165,7 +167,7 @@ describe('Actions', () => {
const channel = {id: 'channel_id3a', name: 'achannel3a', team_id: 'team_id1', type: 'O'};
(joinChannel as jest.Mock).mockReturnValueOnce({type: '', data: {channel}});
(getChannelByNameAndTeamName as jest.Mock).mockReturnValueOnce({type: '', data: channel});
- await testStore.dispatch((goToChannelByChannelName({params: {team: 'team1', identifier: channel.name, path: '/'}, url: ''}, {} as any) as any));
+ await testStore.dispatch((goToChannelByChannelName({params: {team: 'team1', identifier: channel.name, path: '/'}, url: ''} as Match, {} as any)));
expect(joinChannel).toHaveBeenCalledWith('current_user_id', 'team_id1', 'channel_id3a', 'achannel3a');
expect(emitChannelClickEvent).toHaveBeenCalledWith(channel);
});
@@ -187,7 +189,7 @@ describe('Actions', () => {
const channel = {id: 'channel_id6', name: 'achannel6', team_id: 'team_id1', type: 'P'};
(joinChannel as jest.Mock).mockReturnValueOnce({type: '', data: {channel}});
(getChannelByNameAndTeamName as jest.Mock).mockReturnValueOnce({type: '', data: channel});
- await testStore.dispatch((goToChannelByChannelName({params: {team: 'team1', identifier: channel.name, path: '/'}, url: ''}, {} as any) as any));
+ await testStore.dispatch((goToChannelByChannelName({params: {team: 'team1', identifier: channel.name, path: '/'}, url: ''} as Match, {} as any)));
expect(getChannelByNameAndTeamName).toHaveBeenCalledWith('team1', channel.name, true);
expect(getChannelMember).toHaveBeenCalledWith(channel.id, 'current_user_id');
expect(joinPrivateChannelPrompt).toHaveBeenCalled();
@@ -227,7 +229,7 @@ describe('Actions', () => {
const channel = {id: 'channel_id6', name: 'achannel6', team_id: 'team_id1', type: 'P'};
(joinChannel as jest.Mock).mockReturnValueOnce({type: '', data: {channel}});
(getChannelByNameAndTeamName as jest.Mock).mockReturnValueOnce({type: '', data: channel});
- await testStore.dispatch((goToChannelByChannelName({params: {team: 'team1', identifier: channel.name, path: '/'}, url: ''}, {} as any) as any));
+ await testStore.dispatch((goToChannelByChannelName({params: {team: 'team1', identifier: channel.name, path: '/'}, url: ''} as Match, {} as any)));
expect(getChannelByNameAndTeamName).toHaveBeenCalledWith('team1', channel.name, true);
expect(getChannelMember).toHaveBeenCalledWith(channel.id, 'current_user_id');
expect(joinPrivateChannelPrompt).toHaveBeenCalled();
@@ -240,7 +242,7 @@ describe('Actions', () => {
const testStore = await mockStore(initialState);
const history = {replace: jest.fn()};
- await testStore.dispatch((goToDirectChannelByUserId({params: {team: 'team1', identifier: 'channel', path: '/'}, url: ''}, history as any, 'user_id2') as any));
+ await testStore.dispatch((goToDirectChannelByUserId({params: {team: 'team1', identifier: 'channel', path: '/'}, url: ''} as Match, history as any, 'user_id2') as any));
expect(history.replace).toHaveBeenCalledWith('/team1/messages/@user2');
});
@@ -248,7 +250,7 @@ describe('Actions', () => {
const testStore = await mockStore(initialState);
const history = {replace: jest.fn()};
- await testStore.dispatch((goToDirectChannelByUserId({params: {team: 'team2', identifier: 'channel', path: '/'}, url: ''}, history as any, 'user_id2') as any));
+ await testStore.dispatch((goToDirectChannelByUserId({params: {team: 'team2', identifier: 'channel', path: '/'}, url: ''} as Match, history as any, 'user_id2') as any));
expect(history.replace).toHaveBeenCalledWith('/team2/messages/@user2');
});
});
@@ -258,7 +260,7 @@ describe('Actions', () => {
const testStore = await mockStore(initialState);
const history = {replace: jest.fn()};
- await testStore.dispatch((goToDirectChannelByUserIds({params: {team: 'team1', identifier: 'current_user_id__user_id2', path: '/'}, url: ''}, history as any) as any));
+ await testStore.dispatch((goToDirectChannelByUserIds({params: {team: 'team1', identifier: 'current_user_id__user_id2', path: '/'}, url: ''} as Match, history as any)));
expect(history.replace).toHaveBeenCalledWith('/team1/messages/@user2');
});
@@ -266,7 +268,7 @@ describe('Actions', () => {
const testStore = await mockStore(initialState);
const history = {replace: jest.fn()};
- await testStore.dispatch((goToDirectChannelByUserIds({params: {team: 'team2', identifier: 'current_user_id__user_id2', path: '/'}, url: ''}, history as any) as any));
+ await testStore.dispatch((goToDirectChannelByUserIds({params: {team: 'team2', identifier: 'current_user_id__user_id2', path: '/'}, url: ''} as Match, history as any)));
expect(history.replace).toHaveBeenCalledWith('/team2/messages/@user2');
});
});
@@ -276,7 +278,7 @@ describe('Actions', () => {
const testStore = await mockStore(initialState);
const history = {replace: jest.fn()};
- await testStore.dispatch((goToDirectChannelByEmail({params: {team: 'team1', identifier: 'user2@bladekick.com', path: '/'}, url: ''}, history as any) as any));
+ await testStore.dispatch((goToDirectChannelByEmail({params: {team: 'team1', identifier: 'user2@bladekick.com', path: '/'}, url: ''} as Match, history as any)));
expect(getUserByEmail).not.toHaveBeenCalled();
expect(history.replace).toHaveBeenCalledWith('/team1/messages/@user2');
});
@@ -285,7 +287,7 @@ describe('Actions', () => {
const testStore = await mockStore(initialState);
const history = {replace: jest.fn()};
- await testStore.dispatch((goToDirectChannelByEmail({params: {team: 'team1', identifier: 'user3@bladekick.com', path: '/'}, url: ''}, history as any) as any));
+ await testStore.dispatch((goToDirectChannelByEmail({params: {team: 'team1', identifier: 'user3@bladekick.com', path: '/'}, url: ''} as Match, history as any)));
expect(getUserByEmail).toHaveBeenCalledWith('user3@bladekick.com');
expect(history.replace).toHaveBeenCalledWith('/team1/messages/@user3');
});
diff --git a/webapp/channels/src/components/channel_layout/channel_identifier_router/channel_identifier_router.test.tsx b/webapp/channels/src/components/channel_layout/channel_identifier_router/channel_identifier_router.test.tsx
index 9aa9f74ec56..df42768c195 100644
--- a/webapp/channels/src/components/channel_layout/channel_identifier_router/channel_identifier_router.test.tsx
+++ b/webapp/channels/src/components/channel_layout/channel_identifier_router/channel_identifier_router.test.tsx
@@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
+import type {History} from 'history';
import React from 'react';
import {getHistory} from 'utils/browser_history';
@@ -14,18 +15,20 @@ describe('components/channel_layout/CenterChannel', () => {
const baseProps = {
match: {
+ isExact: false,
params: {
identifier: 'identifier',
team: 'team',
path: '/path',
},
+ path: '/team/channel/identifier',
url: '/team/channel/identifier',
},
actions: {
onChannelByIdentifierEnter: jest.fn(),
},
- history: [],
+ history: [] as unknown as History,
};
test('should call onChannelByIdentifierEnter on props change', () => {
@@ -36,11 +39,13 @@ describe('components/channel_layout/CenterChannel', () => {
const props2 = {
match: {
+ isExact: false,
params: {
identifier: 'identifier2',
team: 'team2',
path: '/path2',
},
+ path: '/team2/channel/identifier2',
url: '/team2/channel/identifier2',
},
};
@@ -62,12 +67,14 @@ describe('components/channel_layout/CenterChannel', () => {
const props = {
...baseProps,
match: {
+ isExact: false,
params: {
identifier: 'identifier',
team: 'team',
path: '/path',
postid: 'abcd',
},
+ path: '/team/channel/identifier/abcd',
url: '/team/channel/identifier/abcd',
},
};
@@ -80,12 +87,14 @@ describe('components/channel_layout/CenterChannel', () => {
const props = {
...baseProps,
match: {
+ isExact: false,
params: {
identifier: 'identifier1',
team: 'team1',
path: '/path1',
postid: 'abcd',
},
+ path: '/team1/channel/identifier1/abcd',
url: '/team1/channel/identifier1/abcd',
},
};
diff --git a/webapp/channels/src/components/channel_layout/channel_identifier_router/channel_identifier_router.tsx b/webapp/channels/src/components/channel_layout/channel_identifier_router/channel_identifier_router.tsx
index 29ad4d52d75..12f9a832c6e 100644
--- a/webapp/channels/src/components/channel_layout/channel_identifier_router/channel_identifier_router.tsx
+++ b/webapp/channels/src/components/channel_layout/channel_identifier_router/channel_identifier_router.tsx
@@ -1,31 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
+import type {History} from 'history';
import React from 'react';
+import type {match} from 'react-router-dom';
import ChannelView from 'components/channel_view/index';
import {getHistory} from 'utils/browser_history';
import Constants from 'utils/constants';
-export interface Match {
- params: {
- identifier: string;
- team: string;
- postid?: string;
- path: string;
- };
- url: string;
-}
+export type Match = match<{
+ identifier: string;
+ team: string;
+ postid?: string;
+ path: string;
+}>;
export type MatchAndHistory = Pick
type Props = {
match: Match;
actions: {
- onChannelByIdentifierEnter: (props: MatchAndHistory) => any;
+ onChannelByIdentifierEnter: (props: MatchAndHistory) => void;
};
- history: any;
+ history: History;
};
export default class ChannelIdentifierRouter extends React.PureComponent {
diff --git a/webapp/channels/src/components/channel_members_dropdown/channel_members_dropdown.tsx b/webapp/channels/src/components/channel_members_dropdown/channel_members_dropdown.tsx
index d765a939649..89a076875b3 100644
--- a/webapp/channels/src/components/channel_members_dropdown/channel_members_dropdown.tsx
+++ b/webapp/channels/src/components/channel_members_dropdown/channel_members_dropdown.tsx
@@ -19,6 +19,7 @@ import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import {Constants, ModalIdentifiers} from 'utils/constants';
import type {ModalData} from 'types/actions';
+import type {MMAction} from 'types/store';
const ROWS_FROM_BOTTOM_TO_OPEN_UP = 2;
@@ -39,7 +40,7 @@ export interface Props {
updateChannelMemberSchemeRoles: (channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean) => Promise;
removeChannelMember: (channelId: string, userId: string) => Promise;
getChannelMember: (channelId: string, userId: string) => void;
- openModal: (modalData: ModalData
) => void;
+ openModal:
(modalData: ModalData
) => MMAction;
};
}
diff --git a/webapp/channels/src/components/channel_members_rhs/action_bar.tsx b/webapp/channels/src/components/channel_members_rhs/action_bar.tsx
index 44e4d80796b..a83086bd936 100644
--- a/webapp/channels/src/components/channel_members_rhs/action_bar.tsx
+++ b/webapp/channels/src/components/channel_members_rhs/action_bar.tsx
@@ -71,7 +71,7 @@ export interface Props {
const ActionBar = ({className, channelType, membersCount, canManageMembers, editing, actions}: Props) => {
const showManageButton = channelType !== Constants.GM_CHANNEL && membersCount > 1;
- const handleShortcut = useCallback((e) => {
+ const handleShortcut = useCallback((e: KeyboardEvent) => {
if (isKeyPressed(e, Constants.KeyCodes.ESCAPE) && editing) {
actions.stopEditing();
}
diff --git a/webapp/channels/src/components/channel_members_rhs/member.test.tsx b/webapp/channels/src/components/channel_members_rhs/member.test.tsx
index 0ba5f4e0971..5963495f9bf 100644
--- a/webapp/channels/src/components/channel_members_rhs/member.test.tsx
+++ b/webapp/channels/src/components/channel_members_rhs/member.test.tsx
@@ -7,7 +7,7 @@ import React from 'react';
import type {ChannelType} from '@mattermost/types/channels';
import type {UserProfile} from '@mattermost/types/users';
-import {renderWithContext, screen, waitFor, act} from 'tests/react_testing_utils';
+import {renderWithContext, screen, waitFor} from 'tests/react_testing_utils';
import Member from './member';
import type {ChannelMember} from './member_list';
@@ -197,13 +197,10 @@ describe('components/channel_members_rhs/Member', () => {
const icon = screen.getByTestId('SharedChannelIcon');
expect(icon).toBeInTheDocument();
- await act(async () => {
- userEvent.hover(icon);
- jest.advanceTimersByTime(1000);
+ await userEvent.hover(icon, {advanceTimers: jest.advanceTimersByTime});
- await waitFor(() => {
- expect(screen.getByText('Shared with: Remote Organization')).toBeInTheDocument();
- });
+ await waitFor(() => {
+ expect(screen.getByText('Shared with: Remote Organization')).toBeInTheDocument();
});
});
@@ -229,13 +226,10 @@ describe('components/channel_members_rhs/Member', () => {
const icon = screen.getByTestId('SharedChannelIcon');
expect(icon).toBeInTheDocument();
- await act(async () => {
- userEvent.hover(icon);
- jest.advanceTimersByTime(1000);
+ await userEvent.hover(icon, {advanceTimers: jest.advanceTimersByTime});
- await waitFor(() => {
- expect(screen.getByText('Shared with trusted organizations')).toBeInTheDocument();
- });
+ await waitFor(() => {
+ expect(screen.getByText('Shared with trusted organizations')).toBeInTheDocument();
});
});
diff --git a/webapp/channels/src/components/channel_members_rhs/member_list.tsx b/webapp/channels/src/components/channel_members_rhs/member_list.tsx
index b3898917dd4..be9b60db900 100644
--- a/webapp/channels/src/components/channel_members_rhs/member_list.tsx
+++ b/webapp/channels/src/components/channel_members_rhs/member_list.tsx
@@ -26,10 +26,10 @@ export enum ListItemType {
Separator = 'separator',
}
-export interface ListItem {
+export type ListItem = {
type: ListItemType;
data: ChannelMember | JSX.Element;
-}
+};
export interface Props {
channel: Channel;
members: ListItem[];
@@ -120,7 +120,7 @@ const MemberList = ({
key={index}
style={style}
>
- {members[index].data}
+ {members[index].data as JSX.Element}
);
default:
diff --git a/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.test.tsx b/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.test.tsx
index 217bf9bd642..581a55e7e5b 100644
--- a/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.test.tsx
+++ b/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.test.tsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {act, screen, waitFor} from '@testing-library/react';
+import {screen, waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
@@ -70,9 +70,7 @@ describe('ChannelNotificationsModal', () => {
expect(screen.queryByText('Mobile Notifications')).not.toBeInTheDocument();
expect(screen.getByText('Follow all threads in this channel')).toBeInTheDocument();
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: /Save/i}));
- });
+ await userEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
@@ -99,17 +97,13 @@ describe('ChannelNotificationsModal', () => {
);
const ignoreChannel = screen.getByTestId('ignoreMentions');
- await act(async () => {
- await userEvent.click(ignoreChannel);
- });
+ await userEvent.click(ignoreChannel);
expect(ignoreChannel).toBeChecked();
// Verify the checkbox label is present
expect(screen.getByText('Ignore mentions for @channel, @here and @all')).toBeInTheDocument();
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: /Save/i}));
- });
+ await userEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
@@ -146,20 +140,14 @@ describe('ChannelNotificationsModal', () => {
expect(nothingRadio).toBeInTheDocument();
// Test clicking through the options
- await act(async () => {
- await userEvent.click(allRadio);
- });
+ await userEvent.click(allRadio);
expect(allRadio).toBeChecked();
- await act(async () => {
- await userEvent.click(mentionsRadio);
- });
+ await userEvent.click(mentionsRadio);
expect(mentionsRadio).toBeChecked();
expect(allRadio).not.toBeChecked();
- await act(async () => {
- await userEvent.click(nothingRadio);
- });
+ await userEvent.click(nothingRadio);
expect(nothingRadio).toBeChecked();
expect(mentionsRadio).not.toBeChecked();
@@ -168,9 +156,7 @@ describe('ChannelNotificationsModal', () => {
expect(screen.getByText(/Mentions, direct messages, and keywords only/)).toBeInTheDocument();
expect(screen.getByText(/Nothing/)).toBeInTheDocument();
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: /Save/i}));
- });
+ await userEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
@@ -194,14 +180,10 @@ describe('ChannelNotificationsModal', () => {
renderWithContext();
// Since the default value is checked, we will uncheck the checkbox
- await act(async () => {
- await userEvent.click(screen.getByTestId('desktopNotificationSoundsCheckbox'));
- });
+ await userEvent.click(screen.getByTestId('desktopNotificationSoundsCheckbox'));
expect(screen.getByTestId('desktopNotificationSoundsCheckbox')).not.toBeChecked();
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: /Save/i}));
- });
+ await userEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() => {
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
@@ -252,9 +234,7 @@ describe('ChannelNotificationsModal', () => {
// When "same as desktop" is checked, mobile-specific options should not be visible
expect(screen.queryByTestId('mobile-notify-me-radio-section')).not.toBeInTheDocument();
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: /Save/i}));
- });
+ await userEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
@@ -300,26 +280,18 @@ describe('ChannelNotificationsModal', () => {
const noneRadio = screen.getByTestId('MobileNotification-none');
// Test clicking through the mobile notification options
- await act(async () => {
- await userEvent.click(allRadio);
- });
+ await userEvent.click(allRadio);
expect(allRadio).toBeChecked();
- await act(async () => {
- await userEvent.click(mentionRadio);
- });
+ await userEvent.click(mentionRadio);
expect(mentionRadio).toBeChecked();
expect(allRadio).not.toBeChecked();
- await act(async () => {
- await userEvent.click(noneRadio);
- });
+ await userEvent.click(noneRadio);
expect(noneRadio).toBeChecked();
expect(mentionRadio).not.toBeChecked();
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: /Save/i}));
- });
+ await userEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
@@ -356,9 +328,7 @@ describe('ChannelNotificationsModal', () => {
expect(screen.getByText('Mobile Notifications')).toBeInTheDocument();
expect(screen.getByText('Mute or ignore')).toBeInTheDocument();
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: /Save/i}));
- });
+ await userEvent.click(screen.getByRole('button', {name: /Save/i}));
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
diff --git a/webapp/channels/src/components/channel_selector_modal/channel_selector_modal.tsx b/webapp/channels/src/components/channel_selector_modal/channel_selector_modal.tsx
index e2e2e6244f7..6fcc1d2aa95 100644
--- a/webapp/channels/src/components/channel_selector_modal/channel_selector_modal.tsx
+++ b/webapp/channels/src/components/channel_selector_modal/channel_selector_modal.tsx
@@ -246,7 +246,7 @@ export class ChannelSelectorModal extends React.PureComponent {
id='channelSelectorModal.title'
defaultMessage='Add Channels to Channel Selection List'
values={{
- b: (chunks: string) => {chunks},
+ b: (chunks) => {chunks},
}}
/>
diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_access_rules_tab.test.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_access_rules_tab.test.tsx
index 30eec9e9c16..9628649fe2c 100644
--- a/webapp/channels/src/components/channel_settings_modal/channel_settings_access_rules_tab.test.tsx
+++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_access_rules_tab.test.tsx
@@ -6,11 +6,10 @@ import React from 'react';
import type {UserPropertyField} from '@mattermost/types/properties';
import TableEditor from 'components/admin_console/access_control/editors/table_editor/table_editor';
-import SaveChangesPanel from 'components/widgets/modals/components/save_changes_panel';
import {useChannelAccessControlActions} from 'hooks/useChannelAccessControlActions';
import {useChannelSystemPolicies} from 'hooks/useChannelSystemPolicies';
-import {renderWithContext, screen, waitFor, userEvent} from 'tests/react_testing_utils';
+import {act, renderWithContext, screen, waitFor, userEvent} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import ChannelSettingsAccessRulesTab from './channel_settings_access_rules_tab';
@@ -25,32 +24,9 @@ jest.mock('components/admin_console/access_control/editors/table_editor/table_ed
return jest.fn(() => React.createElement('div', {'data-testid': 'table-editor'}, 'TableEditor'));
});
-// Mock SaveChangesPanel
-jest.mock('components/widgets/modals/components/save_changes_panel', () => {
- const React = require('react');
- return jest.fn((props) => {
- return React.createElement('div', {
- 'data-testid': 'save-changes-panel',
- 'data-state': props.state,
- }, [
- React.createElement('button', {
- key: 'save',
- 'data-testid': 'SaveChangesPanel__save-btn',
- onClick: props.handleSubmit,
- }, 'Save'),
- React.createElement('button', {
- key: 'cancel',
- 'data-testid': 'SaveChangesPanel__cancel-btn',
- onClick: props.handleCancel,
- }, 'Reset'),
- ]);
- });
-});
-
const mockUseChannelAccessControlActions = useChannelAccessControlActions as jest.MockedFunction;
const mockUseChannelSystemPolicies = useChannelSystemPolicies as jest.MockedFunction;
const MockedTableEditor = TableEditor as jest.MockedFunction;
-const MockedSaveChangesPanel = SaveChangesPanel as jest.MockedFunction;
describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () => {
const mockActions = {
@@ -181,9 +157,9 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
},
});
- // Suppress console methods for tests
- jest.spyOn(console, 'error').mockImplementation(() => {});
- jest.spyOn(console, 'warn').mockImplementation(() => {});
+ // Console methods are suppressed in these tests. They'll be restored by setup_jest.ts after the test.
+ console.error = jest.fn();
+ console.warn = jest.fn();
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(window, 'alert').mockImplementation(() => {});
});
@@ -274,13 +250,15 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
expect(screen.getByText('Select user attributes and values as rules to restrict channel membership')).toBeInTheDocument();
});
- test('should call useChannelAccessControlActions hook', () => {
+ test('should call useChannelAccessControlActions hook', async () => {
renderWithContext(
,
initialState,
);
- expect(mockUseChannelAccessControlActions).toHaveBeenCalledTimes(2); // Once for the hook call and once for the mock return
+ await waitFor(() => {
+ expect(mockUseChannelAccessControlActions).toHaveBeenCalledTimes(2); // Once for the hook call and once for the mock return
+ });
});
test('should load user attributes on mount', async () => {
@@ -363,7 +341,9 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
const onChangeCallback = MockedTableEditor.mock.calls[0][0].onChange;
// Simulate expression change
- onChangeCallback('user.attributes.department == "Engineering"');
+ act(() => {
+ onChangeCallback('user.attributes.department == "Engineering"');
+ });
expect(setAreThereUnsavedChanges).toHaveBeenCalledWith(true);
});
@@ -808,7 +788,7 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
expect(screen.getByTestId('table-editor')).toBeInTheDocument();
});
- expect(screen.queryByTestId('save-changes-panel')).not.toBeInTheDocument();
+ expect(screen.queryByText('You have unsaved changes')).not.toBeInTheDocument();
});
test('should show SaveChangesPanel when expression changes', async () => {
@@ -828,7 +808,7 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
onChangeCallback('user.attributes.department == "Engineering"');
await waitFor(() => {
- expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
+ expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
});
@@ -856,7 +836,7 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
await userEvent.click(checkbox);
await waitFor(() => {
- expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
+ expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
});
@@ -904,11 +884,11 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
// Wait for SaveChangesPanel to appear
await waitFor(() => {
- expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
+ expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
// Click Save button
- const saveButton = screen.getByTestId('SaveChangesPanel__save-btn');
+ const saveButton = screen.getByText('Save');
await userEvent.click(saveButton);
// Wait for confirmation modal to appear
@@ -985,11 +965,6 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
await userEvent.click(saveButton);
await userEvent.click(saveButton);
- // Wait for async operations to complete
- await waitFor(() => {
- expect(mockActions.saveChannelPolicy).toHaveBeenCalled();
- });
-
// Should only have been called once due to duplicate prevention
expect(mockActions.saveChannelPolicy).toHaveBeenCalledTimes(1);
});
@@ -1015,16 +990,16 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
// Wait for SaveChangesPanel to appear
await waitFor(() => {
- expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
+ expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
// Click Reset button
- const resetButton = screen.getByTestId('SaveChangesPanel__cancel-btn');
+ const resetButton = screen.getByText('Reset');
await userEvent.click(resetButton);
// Verify panel disappears
await waitFor(() => {
- expect(screen.queryByTestId('save-changes-panel')).not.toBeInTheDocument();
+ expect(screen.queryByText('You have unsaved changes')).not.toBeInTheDocument();
});
// Verify checkbox is reset
@@ -1050,9 +1025,8 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
onChangeCallback('invalid expression');
await waitFor(() => {
- const panel = screen.getByTestId('save-changes-panel');
+ const panel = screen.getByText('Invalid expression format');
expect(panel).toBeInTheDocument();
- expect(panel).toHaveAttribute('data-state', 'error');
});
});
@@ -1086,53 +1060,11 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
await userEvent.click(checkbox);
await waitFor(() => {
- const panel = screen.getByTestId('save-changes-panel');
- expect(panel).toBeInTheDocument();
- expect(panel).toHaveAttribute('data-state', 'error');
+ const panel = screen.getByText('You have unsaved changes');
+ expect(panel).toHaveClass('error');
});
});
- test('should pass correct props to SaveChangesPanel', async () => {
- renderWithContext(
- ,
- initialState,
- );
-
- // Wait for initial loading to complete
- await waitFor(() => {
- expect(screen.getByTestId('table-editor')).toBeInTheDocument();
- });
-
- // Add an expression first to enable the checkbox
- const onChangeCallback = MockedTableEditor.mock.calls[0][0].onChange;
- onChangeCallback('user.attributes.department == "Engineering"');
-
- await waitFor(() => {
- const checkbox = screen.getByRole('checkbox');
- expect(checkbox).not.toBeDisabled();
- });
-
- // Toggle auto-sync to show panel
- const checkbox = screen.getByRole('checkbox');
- await userEvent.click(checkbox);
-
- await waitFor(() => {
- expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
- });
-
- expect(MockedSaveChangesPanel).toHaveBeenCalledWith(
- expect.objectContaining({
- handleSubmit: expect.any(Function),
- handleCancel: expect.any(Function),
- handleClose: expect.any(Function),
- tabChangeError: false,
- state: undefined,
- cancelButtonText: 'Reset',
- }),
- expect.anything(),
- );
- });
-
test('should update SaveChangesPanel state to saved after successful save', async () => {
// Ensure the searchUsers mock returns current user for validation to pass
mockActions.searchUsers.mockResolvedValue({
@@ -1170,11 +1102,11 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
await userEvent.click(checkbox);
await waitFor(() => {
- expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
+ expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
// Click Save button
- const saveButton = screen.getByTestId('SaveChangesPanel__save-btn');
+ const saveButton = screen.getByText('Save');
await userEvent.click(saveButton);
// Wait for confirmation modal to appear
@@ -1189,8 +1121,8 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
// Wait for save to complete and panel to show saved state
await waitFor(() => {
- const panel = screen.getByTestId('save-changes-panel');
- expect(panel).toHaveAttribute('data-state', 'saved');
+ const panel = screen.getByText('Settings saved');
+ expect(panel).toBeVisible();
});
});
});
@@ -1358,10 +1290,10 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
// Click Save to trigger membership calculation
await waitFor(() => {
- expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
+ expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
- const saveButton = screen.getByTestId('SaveChangesPanel__save-btn');
+ const saveButton = screen.getByText('Save');
await userEvent.click(saveButton);
// Wait for confirmation modal to appear
@@ -1433,10 +1365,10 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
// Click Save to trigger self-exclusion validation
await waitFor(() => {
- expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
+ expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
- const saveButton = screen.getByTestId('SaveChangesPanel__save-btn');
+ const saveButton = screen.getByText('Save');
await userEvent.click(saveButton);
// Wait for confirmation modal to appear
@@ -1488,10 +1420,10 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
// Click Save to trigger membership calculation
await waitFor(() => {
- expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
+ expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
- const saveButton = screen.getByTestId('SaveChangesPanel__save-btn');
+ const saveButton = screen.getByText('Save');
await userEvent.click(saveButton);
// Wait for confirmation modal to appear
@@ -1559,10 +1491,10 @@ describe('components/channel_settings_modal/ChannelSettingsAccessRulesTab', () =
// Click Save to trigger membership calculation
await waitFor(() => {
- expect(screen.getByTestId('save-changes-panel')).toBeInTheDocument();
+ expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
});
- const saveButton = screen.getByTestId('SaveChangesPanel__save-btn');
+ const saveButton = screen.getByText('Save');
await userEvent.click(saveButton);
// Wait for confirmation modal to appear
diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_archive_tab.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_archive_tab.tsx
index 96f7496806d..7c4d5ebcda3 100644
--- a/webapp/channels/src/components/channel_settings_modal/channel_settings_archive_tab.tsx
+++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_archive_tab.tsx
@@ -75,7 +75,7 @@ function ChannelSettingsArchiveTab({
defaultMessage='Are you sure you wish to archive the {display_name} channel?'
values={{
display_name: channel.display_name,
- strong: (chunks: string) => {chunks},
+ strong: (chunks) => {chunks},
}}
/>
diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.test.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.test.tsx
index 8255b021f9f..0af933044fb 100644
--- a/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.test.tsx
+++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.test.tsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {act, screen} from '@testing-library/react';
+import {screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
@@ -93,9 +93,7 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(toggle).not.toHaveClass('active');
// Click the toggle to enable the banner
- await act(async () => {
- await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
- });
+ await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Banner text and color inputs should be visible when banner is enabled
expect(screen.getByTestId('channel_banner_banner_text_textbox')).toBeInTheDocument();
@@ -130,9 +128,7 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(screen.queryByTestId('channel_banner_banner_text_textbox')).not.toBeInTheDocument();
// Click the toggle to enable the banner
- await act(async () => {
- await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
- });
+ await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Banner settings should now be visible
expect(screen.getByTestId('channel_banner_banner_text_textbox')).toBeInTheDocument();
@@ -146,9 +142,7 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
// Enable the banner
- await act(async () => {
- await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
- });
+ await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Add a small delay to ensure all state updates are processed
await new Promise((resolve) => setTimeout(resolve, 0));
@@ -164,28 +158,20 @@ describe('ChannelSettingsConfigurationTab', () => {
renderWithContext();
// Enable the banner
- await act(async () => {
- await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
- });
+ await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Enter banner text
- await act(async () => {
- const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
- await userEvent.clear(textInput);
- await userEvent.type(textInput, 'New banner text');
- });
+ const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
+ await userEvent.clear(textInput);
+ await userEvent.type(textInput, 'New banner text');
// Set banner color
const colorInput = screen.getByTestId('color-inputColorValue');
- await act(async () => {
- await userEvent.clear(colorInput);
- await userEvent.type(colorInput, '#AA00AA');
- });
+ await userEvent.clear(colorInput);
+ await userEvent.type(colorInput, '#AA00AA');
// Click the Save button
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: 'Save'}));
- });
+ await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Verify patchChannel was called with the updated values
expect(patchChannel).toHaveBeenCalledWith('channel1', {
@@ -202,11 +188,9 @@ describe('ChannelSettingsConfigurationTab', () => {
renderWithContext();
// Change the banner text
- await act(async () => {
- const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
- await userEvent.clear(textInput);
- await userEvent.type(textInput, 'Changed banner text');
- });
+ const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
+ await userEvent.clear(textInput);
+ await userEvent.type(textInput, 'Changed banner text');
// Add a small delay to ensure all state updates are processed
await new Promise((resolve) => setTimeout(resolve, 0));
@@ -215,9 +199,7 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(screen.getByRole('button', {name: 'Save'})).toBeInTheDocument();
// Click the Reset button
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: 'Reset'}));
- });
+ await userEvent.click(screen.getByRole('button', {name: 'Reset'}));
// Form should be reset to original values
expect(screen.getByTestId('channel_banner_banner_text_textbox')).toHaveValue('Test banner text');
@@ -230,20 +212,14 @@ describe('ChannelSettingsConfigurationTab', () => {
renderWithContext();
// Enable the banner
- await act(async () => {
- await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
- });
+ await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Leave banner text empty
- await act(async () => {
- const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
- await userEvent.clear(textInput);
- });
+ const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
+ await userEvent.clear(textInput);
// Click the Save button
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: 'Save'}));
- });
+ await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// SaveChangesPanel should show error state
const errorMessage = screen.getByText(/Banner text is required/);
@@ -255,19 +231,15 @@ describe('ChannelSettingsConfigurationTab', () => {
renderWithContext();
// Enable the banner
- await act(async () => {
- await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
- });
+ await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Create a string that exceeds the allowed character limit
const longText = 'a'.repeat(1025);
// Enter long banner text
- await act(async () => {
- const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
- await userEvent.clear(textInput);
- await userEvent.type(textInput, longText);
- });
+ const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
+ await userEvent.clear(textInput);
+ await userEvent.type(textInput, longText);
// Add a small delay to ensure all state updates are processed
await new Promise((resolve) => setTimeout(resolve, 0));
@@ -286,9 +258,7 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(previewButton).not.toHaveClass('active');
// Click the preview button
- await act(async () => {
- await userEvent.click(previewButton);
- });
+ await userEvent.click(previewButton);
// Preview should now be active
expect(previewButton).toHaveClass('active');
@@ -301,9 +271,7 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(screen.getByTestId('channel_banner_banner_text_textbox')).toBeInTheDocument();
// Click the toggle to disable the banner
- await act(async () => {
- await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
- });
+ await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Banner settings should now be hidden
expect(screen.queryByTestId('channel_banner_banner_text_textbox')).not.toBeInTheDocument();
@@ -316,21 +284,15 @@ describe('ChannelSettingsConfigurationTab', () => {
renderWithContext();
// Enable the banner
- await act(async () => {
- await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
- });
+ await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Enter banner text but leave color empty
- await act(async () => {
- const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
- await userEvent.clear(textInput);
- await userEvent.type(textInput, 'New banner text');
- });
+ const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
+ await userEvent.clear(textInput);
+ await userEvent.type(textInput, 'New banner text');
// Click the Save button
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: 'Save'}));
- });
+ await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// SaveChangesPanel should show error state
const errorMessage = screen.getByText(/Banner color is required/);
@@ -345,28 +307,20 @@ describe('ChannelSettingsConfigurationTab', () => {
renderWithContext();
// Enable the banner
- await act(async () => {
- await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
- });
+ await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
// Enter banner text
- await act(async () => {
- const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
- await userEvent.clear(textInput);
- await userEvent.type(textInput, 'New banner text');
- });
+ const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
+ await userEvent.clear(textInput);
+ await userEvent.type(textInput, 'New banner text');
// Enter a valid hex color
- await act(async () => {
- const colorInput = screen.getByTestId('color-inputColorValue');
- await userEvent.clear(colorInput);
- await userEvent.type(colorInput, '#ff0000');
- });
+ const colorInput = screen.getByTestId('color-inputColorValue');
+ await userEvent.clear(colorInput);
+ await userEvent.type(colorInput, '#ff0000');
// Click the Save button
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: 'Save'}));
- });
+ await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Verify patchChannel was called with the correct color
expect(patchChannel).toHaveBeenCalledWith('channel1', expect.objectContaining({
@@ -402,18 +356,14 @@ describe('ChannelSettingsConfigurationTab', () => {
);
// Enter a invalid hex color
- await act(async () => {
- const colorInput = screen.getByTestId('color-inputColorValue');
- await userEvent.clear(colorInput);
- await userEvent.type(colorInput, 'not-a-color');
- });
+ const colorInput = screen.getByTestId('color-inputColorValue');
+ await userEvent.clear(colorInput);
+ await userEvent.type(colorInput, 'not-a-color');
// Do another action to trigger blur on this input so color is validated
- await act(async () => {
- const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
- await userEvent.clear(textInput);
- await userEvent.type(textInput, 'Test text');
- });
+ const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
+ await userEvent.clear(textInput);
+ await userEvent.type(textInput, 'Test text');
// if invalid, the color automatically returns to the original color
expect(screen.getByTestId('color-inputColorValue')).toHaveValue(originalColor);
@@ -422,11 +372,8 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
// Modify the color to a valid one
- await act(async () => {
- const colorInput = screen.getByTestId('color-inputColorValue');
- await userEvent.clear(colorInput);
- await userEvent.type(colorInput, '#123456');
- });
+ await userEvent.clear(colorInput);
+ await userEvent.type(colorInput, '#123456');
// Add a small delay to ensure all state updates are processed
await new Promise((resolve) => setTimeout(resolve, 0));
@@ -442,23 +389,17 @@ describe('ChannelSettingsConfigurationTab', () => {
renderWithContext();
// Add whitespace to the banner text
- await act(async () => {
- const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
- await userEvent.clear(textInput);
- await userEvent.type(textInput, ' Banner text with whitespace ');
- });
+ const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
+ await userEvent.clear(textInput);
+ await userEvent.type(textInput, ' Banner text with whitespace ');
// Add whitespace to the banner color
- await act(async () => {
- const colorInput = screen.getByTestId('color-inputColorValue');
- await userEvent.clear(colorInput);
- await userEvent.type(colorInput, ' #00FF00 ');
- });
+ const colorInput = screen.getByTestId('color-inputColorValue');
+ await userEvent.clear(colorInput);
+ await userEvent.type(colorInput, ' #00FF00 ');
// Click the Save button
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: 'Save'}));
- });
+ await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Verify patchChannel was called with the trimmed values
expect(patchChannel).toHaveBeenCalledWith('channel1', {
@@ -475,7 +416,6 @@ describe('ChannelSettingsConfigurationTab', () => {
await new Promise((resolve) => setTimeout(resolve, 0));
// The text input should now have the trimmed value
- const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
expect(textInput).toHaveValue('Banner text with whitespace');
});
});
diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.test.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.test.tsx
index 01248bd3bc1..4b7adf94648 100644
--- a/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.test.tsx
+++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.test.tsx
@@ -199,9 +199,7 @@ describe('ChannelSettingsInfoTab', () => {
await new Promise((resolve) => setTimeout(resolve, 0));
// Click the Save button in the SaveChangesPanel.
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: 'Save'}));
- });
+ await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Verify patchChannel was called with the updated values (without type change).
// Note: URL should remain unchanged when editing existing channels
@@ -242,9 +240,7 @@ describe('ChannelSettingsInfoTab', () => {
await new Promise((resolve) => setTimeout(resolve, 0));
// Click the Save button
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: 'Save'}));
- });
+ await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Verify patchChannel was called with the trimmed values
expect(patchChannel).toHaveBeenCalledWith('channel1', {
@@ -286,9 +282,7 @@ describe('ChannelSettingsInfoTab', () => {
expect(screen.getByRole('button', {name: 'Save'})).toBeInTheDocument();
// Click the Save button
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: 'Save'}));
- });
+ await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Add a small delay to ensure all state updates are processed
await new Promise((resolve) => setTimeout(resolve, 0));
@@ -315,9 +309,7 @@ describe('ChannelSettingsInfoTab', () => {
expect(screen.queryByRole('button', {name: 'Save'})).toBeInTheDocument();
// Click the Reset button.
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: 'Reset'}));
- });
+ await userEvent.click(screen.getByRole('button', {name: 'Reset'}));
// Form should be reset to original values.
expect(screen.getByRole('textbox', {name: 'Channel name'})).toHaveValue('Test Channel');
@@ -344,9 +336,7 @@ describe('ChannelSettingsInfoTab', () => {
await new Promise((resolve) => setTimeout(resolve, 0));
// Click the Save button.
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: 'Save'}));
- });
+ await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// SaveChangesPanel should show 'error' state.
const errorMessage = screen.getByText(/There are errors in the form above/);
@@ -527,9 +517,7 @@ describe('ChannelSettingsInfoTab', () => {
await userEvent.click(privateButton);
// Click Save button
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: 'Save'}));
- });
+ await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Verify the modal is shown
expect(screen.getByTestId('convert-confirm-modal')).toBeInTheDocument();
@@ -548,14 +536,10 @@ describe('ChannelSettingsInfoTab', () => {
await userEvent.click(privateButton);
// Click Save button to show modal
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: 'Save'}));
- });
+ await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Click confirm button in modal
- await act(async () => {
- await userEvent.click(screen.getByText(/Yes, Convert Channel/i));
- });
+ await userEvent.click(screen.getByText(/Yes, Convert Channel/i));
// Verify updateChannelPrivacy was called
expect(updateChannelPrivacy).toHaveBeenCalledWith('channel1', 'P');
@@ -574,14 +558,10 @@ describe('ChannelSettingsInfoTab', () => {
await userEvent.click(privateButton);
// Click Save button to show modal
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: 'Save'}));
- });
+ await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Click cancel button in modal
- await act(async () => {
- await userEvent.click(screen.getByText(/Cancel/i));
- });
+ await userEvent.click(screen.getByText(/Cancel/i));
// Verify updateChannelPrivacy was not called
expect(updateChannelPrivacy).not.toHaveBeenCalled();
@@ -603,14 +583,10 @@ describe('ChannelSettingsInfoTab', () => {
await userEvent.click(privateButton);
// Click Save button to show modal
- await act(async () => {
- await userEvent.click(screen.getByRole('button', {name: 'Save'}));
- });
+ await userEvent.click(screen.getByRole('button', {name: 'Save'}));
// Click confirm button in modal
- await act(async () => {
- await userEvent.click(screen.getByText(/Yes, Convert Channel/i));
- });
+ await userEvent.click(screen.getByText(/Yes, Convert Channel/i));
// Verify error state is shown
expect(screen.getByText(/There are errors in the form above/)).toBeInTheDocument();
diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_modal.test.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_modal.test.tsx
index 6e73a473984..89c2071b1af 100644
--- a/webapp/channels/src/components/channel_settings_modal/channel_settings_modal.test.tsx
+++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_modal.test.tsx
@@ -5,11 +5,10 @@ import {screen, waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
-import type {DeepPartial} from '@mattermost/types/utilities';
+import {General} from 'mattermost-redux/constants';
import {renderWithContext} from 'tests/react_testing_utils';
-
-import type {GlobalState} from 'types/store';
+import {TestHelper} from 'utils/test_helper';
import ChannelSettingsModal from './channel_settings_modal';
@@ -18,35 +17,6 @@ let mockPrivateChannelPermission = true;
let mockPublicChannelPermission = true;
let mockManageChannelAccessRulesPermission = false;
-// Variable to control group-constrained status in tests
-let mockGroupConstrained = false;
-
-// Mock the redux selectors
-jest.mock('mattermost-redux/selectors/entities/channels', () => ({
- getChannel: jest.fn().mockImplementation((state, channelId) => {
- // Return a mock channel based on the channelId
- return {
- id: channelId,
- team_id: 'team1',
- display_name: 'Test Channel',
- name: channelId === 'default_channel' ? 'town-square' : 'test-channel',
- purpose: 'Testing purpose',
- header: 'Channel header',
- type: mockChannelType, // Use a variable to control the channel type
- create_at: 0,
- update_at: 0,
- delete_at: 0,
- last_post_at: 0,
- total_msg_count: 0,
- extra_update_at: 0,
- creator_id: 'creator1',
- last_root_post_at: 0,
- scheme_id: '',
- group_constrained: mockGroupConstrained,
- };
- }),
-}));
-
// Mock the channel banner selector
jest.mock('mattermost-redux/selectors/entities/channel_banner', () => ({
selectChannelBannerEnabled: jest.fn().mockImplementation((state) => {
@@ -140,9 +110,6 @@ type TabType = {
display?: boolean;
};
-// Variable to control the channel type in tests
-let mockChannelType = 'O';
-
// Mock the settings sidebar
jest.mock('components/settings_sidebar', () => {
return function MockSettingsSidebar({tabs, activeTab, updateTab}: {tabs: TabType[]; activeTab: string; updateTab: (tab: string) => void}): JSX.Element {
@@ -165,30 +132,58 @@ jest.mock('components/settings_sidebar', () => {
};
});
-const baseProps = {
- channelId: 'channel1',
- isOpen: true,
- onExited: jest.fn(),
- focusOriginElement: 'button1',
-};
-
describe('ChannelSettingsModal', () => {
+ const channelId = 'channel1';
+
+ const baseProps = {
+ channelId,
+ isOpen: true,
+ onExited: jest.fn(),
+ focusOriginElement: 'button1',
+ };
+
+ function makeTestState() {
+ return {
+ entities: {
+ channels: {
+ channels: {
+ [channelId]: TestHelper.getChannelMock({
+ id: channelId,
+ type: General.OPEN_CHANNEL,
+ purpose: 'Testing purpose',
+ header: 'Channel header',
+ group_constrained: false,
+ }),
+ },
+ },
+ general: {
+ license: {
+ SkuShortName: '',
+ },
+ },
+ },
+ };
+ }
+
beforeEach(() => {
jest.clearAllMocks();
- mockChannelType = 'O'; // Default to public channel
mockPrivateChannelPermission = true;
mockPublicChannelPermission = true;
mockManageChannelAccessRulesPermission = false; // Default to no access rules permission
- mockGroupConstrained = false; // Default to not group-constrained
});
it('should render the modal with correct header text', async () => {
- renderWithContext();
+ const testState = makeTestState();
+
+ renderWithContext(, testState);
+
expect(screen.getByText('Channel Settings')).toBeInTheDocument();
});
it('should render Info tab by default', async () => {
- renderWithContext();
+ const testState = makeTestState();
+
+ renderWithContext(, testState);
// Wait for the lazy-loaded components
await waitFor(() => {
@@ -197,7 +192,9 @@ describe('ChannelSettingsModal', () => {
});
it('should switch tabs when clicked', async () => {
- renderWithContext();
+ const testState = makeTestState();
+
+ renderWithContext(, testState);
// Wait for the sidebar to load
await waitFor(() => {
@@ -216,11 +213,11 @@ describe('ChannelSettingsModal', () => {
});
it('should not show archive tab for default channel', async () => {
- renderWithContext(
- ,
- );
+ const testState = makeTestState();
+
+ testState.entities.channels.channels[channelId].name = 'town-square';
+
+ renderWithContext(, testState);
// Wait for the sidebar to load
await waitFor(() => {
@@ -235,10 +232,11 @@ describe('ChannelSettingsModal', () => {
});
it('should show archive tab for public channel when user has permission', async () => {
- mockChannelType = 'O';
mockPublicChannelPermission = true;
- renderWithContext();
+ const testState = makeTestState();
+
+ renderWithContext(, testState);
// Wait for the sidebar to load
await waitFor(() => {
@@ -250,10 +248,11 @@ describe('ChannelSettingsModal', () => {
});
it('should not show archive tab for public channel when user does not have permission', async () => {
- mockChannelType = 'O';
mockPublicChannelPermission = false;
- renderWithContext();
+ const testState = makeTestState();
+
+ renderWithContext(, testState);
// Wait for the sidebar to load
await waitFor(() => {
@@ -265,10 +264,12 @@ describe('ChannelSettingsModal', () => {
});
it('should show archive tab for private channel when user has permission', async () => {
- mockChannelType = 'P';
mockPrivateChannelPermission = true;
- renderWithContext();
+ const testState = makeTestState();
+ testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
+
+ renderWithContext(, testState);
// Wait for the sidebar to load
await waitFor(() => {
@@ -280,10 +281,12 @@ describe('ChannelSettingsModal', () => {
});
it('should not show archive tab for private channel when user does not have permission', async () => {
- mockChannelType = 'P';
mockPrivateChannelPermission = false;
- renderWithContext();
+ const testState = makeTestState();
+ testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
+
+ renderWithContext(, testState);
// Wait for the sidebar to load
await waitFor(() => {
@@ -295,67 +298,44 @@ describe('ChannelSettingsModal', () => {
});
it('should not show configuration tab with no license', async () => {
- const baseState: DeepPartial = {
- entities: {
- general: {
- license: {
- SkuShortName: '',
- },
- },
- },
- };
- renderWithContext(, baseState as GlobalState);
+ const testState = makeTestState();
+
+ renderWithContext(, testState);
expect(screen.queryByTestId('configuration-tab-button')).not.toBeInTheDocument();
});
it('should not show configuration tab with professional license', async () => {
- const baseState: DeepPartial = {
- entities: {
- general: {
- license: {
- SkuShortName: 'professional',
- },
- },
- },
- };
- renderWithContext(, baseState as GlobalState);
+ const testState = makeTestState();
+ testState.entities.general.license.SkuShortName = 'professional';
+
+ renderWithContext(, testState);
expect(screen.queryByTestId('configuration-tab-button')).not.toBeInTheDocument();
});
it('should not show configuration tab with enterprise license', async () => {
- const baseState: DeepPartial = {
- entities: {
- general: {
- license: {
- SkuShortName: 'enterprise',
- },
- },
- },
- };
- renderWithContext(, baseState as GlobalState);
+ const testState = makeTestState();
+ testState.entities.general.license.SkuShortName = 'enterprise';
+
+ renderWithContext(, testState);
expect(screen.queryByTestId('configuration-tab-button')).not.toBeInTheDocument();
});
it('should show configuration tab when enterprise advanced license', async () => {
- const baseState: DeepPartial = {
- entities: {
- general: {
- license: {
- SkuShortName: 'advanced',
- },
- },
- },
- };
- renderWithContext(, baseState as GlobalState);
+ const testState = makeTestState();
+ testState.entities.general.license.SkuShortName = 'advanced';
+
+ renderWithContext(, testState);
expect(screen.getByTestId('configuration-tab-button')).toBeInTheDocument();
});
describe('Access Control tab visibility', () => {
it('should show Access Control tab for private channel when user has permission', async () => {
- mockChannelType = 'P';
mockManageChannelAccessRulesPermission = true;
- renderWithContext();
+ const testState = makeTestState();
+ testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
+
+ renderWithContext(, testState);
// Wait for the sidebar to load
await waitFor(() => {
@@ -368,10 +348,12 @@ describe('ChannelSettingsModal', () => {
});
it('should not show Access Control tab for private channel when user lacks permission', async () => {
- mockChannelType = 'P';
mockManageChannelAccessRulesPermission = false;
- renderWithContext();
+ const testState = makeTestState();
+ testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
+
+ renderWithContext(, testState);
// Wait for the sidebar to load
await waitFor(() => {
@@ -384,10 +366,11 @@ describe('ChannelSettingsModal', () => {
});
it('should not show Access Control tab for public channel even with permission', async () => {
- mockChannelType = 'O';
mockManageChannelAccessRulesPermission = true;
- renderWithContext();
+ const testState = makeTestState();
+
+ renderWithContext(, testState);
// Wait for the sidebar to load
await waitFor(() => {
@@ -400,10 +383,11 @@ describe('ChannelSettingsModal', () => {
});
it('should not show Access Control tab for public channel without permission', async () => {
- mockChannelType = 'O';
mockManageChannelAccessRulesPermission = false;
- renderWithContext();
+ const testState = makeTestState();
+
+ renderWithContext(, testState);
// Wait for the sidebar to load
await waitFor(() => {
@@ -416,10 +400,12 @@ describe('ChannelSettingsModal', () => {
});
it('should be able to navigate to Access Control tab when visible', async () => {
- mockChannelType = 'P';
mockManageChannelAccessRulesPermission = true;
- renderWithContext();
+ const testState = makeTestState();
+ testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
+
+ renderWithContext(, testState);
// Wait for the sidebar to load
await waitFor(() => {
@@ -439,10 +425,12 @@ describe('ChannelSettingsModal', () => {
});
it('should show correct tab label as "Access Control"', async () => {
- mockChannelType = 'P';
mockManageChannelAccessRulesPermission = true;
- renderWithContext();
+ const testState = makeTestState();
+ testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
+
+ renderWithContext(, testState);
// Wait for the sidebar to load
await waitFor(() => {
@@ -455,14 +443,13 @@ describe('ChannelSettingsModal', () => {
});
it('should show Access Control tab for default channel if private and user has permission', async () => {
- mockChannelType = 'P';
mockManageChannelAccessRulesPermission = true;
- renderWithContext(
- ,
- );
+ const testState = makeTestState();
+ testState.entities.channels.channels[channelId].name = 'town-square';
+ testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
+
+ renderWithContext(, testState);
// Wait for the sidebar to load
await waitFor(() => {
@@ -474,11 +461,13 @@ describe('ChannelSettingsModal', () => {
});
it('should not show Access Control tab for group-constrained private channel even with permission', async () => {
- mockChannelType = 'P';
mockManageChannelAccessRulesPermission = true;
- mockGroupConstrained = true;
- renderWithContext();
+ const testState = makeTestState();
+ testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
+ testState.entities.channels.channels[channelId].group_constrained = true;
+
+ renderWithContext(, testState);
// Wait for the sidebar to load
await waitFor(() => {
@@ -491,11 +480,13 @@ describe('ChannelSettingsModal', () => {
});
it('should not show Access Control tab for group-constrained private channel without permission', async () => {
- mockChannelType = 'P';
mockManageChannelAccessRulesPermission = false;
- mockGroupConstrained = true;
- renderWithContext();
+ const testState = makeTestState();
+ testState.entities.channels.channels[channelId].type = General.PRIVATE_CHANNEL;
+ testState.entities.channels.channels[channelId].group_constrained = true;
+
+ renderWithContext(, testState);
// Wait for the sidebar to load
await waitFor(() => {
@@ -508,11 +499,12 @@ describe('ChannelSettingsModal', () => {
});
it('should not show Access Control tab for group-constrained public channel', async () => {
- mockChannelType = 'O';
mockManageChannelAccessRulesPermission = true;
- mockGroupConstrained = true;
- renderWithContext();
+ const testState = makeTestState();
+ testState.entities.channels.channels[channelId].group_constrained = true;
+
+ renderWithContext(, testState);
// Wait for the sidebar to load
await waitFor(() => {
@@ -531,7 +523,7 @@ describe('ChannelSettingsModal', () => {
});
it('should close immediately when no unsaved changes exist', async () => {
- renderWithContext();
+ renderWithContext(, makeTestState());
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
@@ -546,7 +538,7 @@ describe('ChannelSettingsModal', () => {
});
it('should prevent close on first attempt with unsaved changes', async () => {
- renderWithContext();
+ renderWithContext(, makeTestState());
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
@@ -571,7 +563,7 @@ describe('ChannelSettingsModal', () => {
});
it('should allow close on second attempt (warn-once behavior)', async () => {
- renderWithContext();
+ renderWithContext(, makeTestState());
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
@@ -598,7 +590,7 @@ describe('ChannelSettingsModal', () => {
});
it('should reset warning state when changes are saved', async () => {
- renderWithContext();
+ renderWithContext(, makeTestState());
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
diff --git a/webapp/channels/src/components/channel_view/__snapshots__/channel_view.test.tsx.snap b/webapp/channels/src/components/channel_view/__snapshots__/channel_view.test.tsx.snap
index 733b7b70c6c..ecb4cb49e68 100644
--- a/webapp/channels/src/components/channel_view/__snapshots__/channel_view.test.tsx.snap
+++ b/webapp/channels/src/components/channel_view/__snapshots__/channel_view.test.tsx.snap
@@ -9,30 +9,7 @@ exports[`components/channel_view Should match snapshot if channel is archived 1`
id="centerChannelFileDropOverlay"
overlayType="center"
/>
-
+
@@ -79,30 +56,7 @@ exports[`components/channel_view Should match snapshot if channel is deactivated
id="centerChannelFileDropOverlay"
overlayType="center"
/>
-
+
@@ -148,30 +102,7 @@ exports[`components/channel_view Should match snapshot with base props 1`] = `
id="centerChannelFileDropOverlay"
overlayType="center"
/>
-
+
diff --git a/webapp/channels/src/components/channel_view/channel_view.tsx b/webapp/channels/src/components/channel_view/channel_view.tsx
index 29f9ae4ac52..1fb7e3134fa 100644
--- a/webapp/channels/src/components/channel_view/channel_view.tsx
+++ b/webapp/channels/src/components/channel_view/channel_view.tsx
@@ -122,7 +122,7 @@ export default class ChannelView extends React.PureComponent {
id='channelView.archivedChannelWithDeactivatedUser'
defaultMessage='You are viewing an archived channel with a deactivated user. New messages cannot be posted.'
values={{
- b: (chunks: string) => {chunks},
+ b: (chunks) => {chunks},
}}
/>