diff --git a/server/channels/app/migrations.go b/server/channels/app/migrations.go
index c7d1aa78127..a23fa4a3cb4 100644
--- a/server/channels/app/migrations.go
+++ b/server/channels/app/migrations.go
@@ -640,7 +640,7 @@ func (s *Server) doSetupContentFlaggingProperties() error {
contentFlaggingPropertyNameStatus: {
GroupID: group.ID,
Name: contentFlaggingPropertyNameStatus,
- Type: model.PropertyFieldTypeText,
+ Type: model.PropertyFieldTypeSelect,
},
contentFlaggingPropertyNameReportingUserID: {
GroupID: group.ID,
@@ -650,7 +650,7 @@ func (s *Server) doSetupContentFlaggingProperties() error {
contentFlaggingPropertyNameReportingReason: {
GroupID: group.ID,
Name: contentFlaggingPropertyNameReportingReason,
- Type: model.PropertyFieldTypeText,
+ Type: model.PropertyFieldTypeSelect,
},
contentFlaggingPropertyNameReportingComment: {
GroupID: group.ID,
diff --git a/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/content_reviewers.test.tsx b/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/content_reviewers.test.tsx
index 8255018fa9d..dcbb7a06cda 100644
--- a/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/content_reviewers.test.tsx
+++ b/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/content_reviewers.test.tsx
@@ -14,15 +14,16 @@ import ContentFlaggingContentReviewers from './content_reviewers';
// Mock the UserMultiSelector component
jest.mock('../../content_flagging/user_multiselector/user_multiselector', () => ({
- UserMultiSelector: ({id, initialValue, onChange}: {id: string; initialValue: string[]; onChange: (userIds: string[]) => void}) => (
+ __esModule: true,
+ UserSelector: ({id, multiSelectInitialValue, multiSelectOnChange}: {id: string; multiSelectInitialValue: string[]; multiSelectOnChange: (userIds: string[]) => void}) => (
- {initialValue.join(',')}
+ {multiSelectInitialValue.join(',')}
),
}));
diff --git a/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/content_reviewers.tsx b/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/content_reviewers.tsx
index dbd4b4c1ebc..1f1354ad407 100644
--- a/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/content_reviewers.tsx
+++ b/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/content_reviewers.tsx
@@ -16,7 +16,7 @@ import {
SectionHeader,
} from 'components/admin_console/system_properties/controls';
-import {UserMultiSelector} from '../../content_flagging/user_multiselector/user_multiselector';
+import {UserSelector} from '../../content_flagging/user_multiselector/user_multiselector';
import type {SystemConsoleCustomSettingsComponentProps} from '../../schema_admin_settings';
import './content_reviewers.scss';
@@ -148,10 +148,11 @@ export default function ContentFlaggingContentReviewers(props: SystemConsoleCust
-
diff --git a/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/team_reviewers_section/team_reviewers_section.test.tsx b/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/team_reviewers_section/team_reviewers_section.test.tsx
index e8526879e18..a37ce332c6b 100644
--- a/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/team_reviewers_section/team_reviewers_section.test.tsx
+++ b/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/team_reviewers_section/team_reviewers_section.test.tsx
@@ -18,21 +18,6 @@ jest.mock('mattermost-redux/actions/teams', () => ({
searchTeams: jest.fn(),
}));
-jest.mock('../../user_multiselector/user_multiselector', () => ({
- UserMultiSelector: ({id, initialValue, onChange}: {id: string; initialValue: string[]; onChange: (ids: string[]) => void}) => (
-
- {`Selected: {${initialValue.join(', ')}`}
-
-
- ),
-}));
-
-jest.mock('components/widgets/team_icon/team_icon', () => ({
- TeamIcon: ({content}: {content: string}) => {content}
,
-}));
-
const mockSearchTeams = jest.mocked(searchTeams);
describe('TeamReviewersSection', () => {
@@ -265,40 +250,6 @@ describe('TeamReviewersSection', () => {
});
});
- test('should handle reviewer selection changes', async () => {
- const onChange = jest.fn();
- renderWithContext(
- ,
- );
-
- await waitFor(() => {
- expect(mockSearchTeams).toHaveBeenCalledWith('', {page: 0, per_page: 10});
- });
-
- await waitFor(() => {
- const teamNameCells = screen.getAllByTestId('teamName');
- expect(teamNameCells).toHaveLength(2);
- expect(teamNameCells[0]).toBeVisible();
- expect(teamNameCells[0]).toHaveTextContent('Team One');
-
- expect(teamNameCells[1]).toBeVisible();
- expect(teamNameCells[1]).toHaveTextContent('Team Two');
- });
-
- const changeReviewersButton = screen.getAllByText('Change Reviewers')[0];
- fireEvent.click(changeReviewersButton);
-
- expect(onChange).toHaveBeenCalledWith({
- team1: {
- Enabled: false,
- ReviewerIds: ['user1', 'user2'],
- },
- });
- });
-
test('should display existing reviewer settings', async () => {
const teamReviewersSetting = {
team1: {
@@ -354,17 +305,6 @@ describe('TeamReviewersSection', () => {
});
test('should reset page to 0 when searching', async () => {
- // mockSearchTeams.
- // mockReturnValueOnce({
- // data: {teams: mockTeams, total_count: 20},
- // } as never).
- // mockResolvedValueOnce({
- // data: {teams: mockTeams, total_count: 20},
- // } as never).
- // mockResolvedValueOnce({
- // data: {teams: mockTeams, total_count: 5},
- // } as never);
-
mockSearchTeams.mockReturnValueOnce(async () => ({
data: {teams: mockTeams, total_count: 20},
})).
@@ -480,10 +420,6 @@ describe('TeamReviewersSection', () => {
expect(mockSearchTeams).toHaveBeenCalledWith('', {page: 0, per_page: 10});
});
- await waitFor(() => {
- expect(screen.getAllByText('Team One')).toHaveLength(4);
- });
-
const updatedToggle = screen.getAllByRole('button', {name: /enable or disable content reviewers for this team/i})[0];
// Second click - disable
diff --git a/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/team_reviewers_section/team_reviewers_section.tsx b/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/team_reviewers_section/team_reviewers_section.tsx
index 57fe019a2d4..0ce5b572f67 100644
--- a/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/team_reviewers_section/team_reviewers_section.tsx
+++ b/webapp/channels/src/components/admin_console/content_flagging/content_reviewers/team_reviewers_section/team_reviewers_section.tsx
@@ -16,7 +16,7 @@ import {TeamIcon} from 'components/widgets/team_icon/team_icon';
import * as Utils from 'utils/utils';
-import {UserMultiSelector} from '../../user_multiselector/user_multiselector';
+import {UserSelector} from '../../user_multiselector/user_multiselector';
import './team_reviewers_section.scss';
@@ -141,10 +141,11 @@ export default function TeamReviewers({teamReviewersSetting, onChange}: Props):
),
reviewers: (
-
),
enabled: (
diff --git a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_multiselect.scss b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_multiselect.scss
index 5b4ac977d0d..b67d23f9466 100644
--- a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_multiselect.scss
+++ b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_multiselect.scss
@@ -13,4 +13,10 @@
display: flex;
gap: 8px;
}
+
+ &.singleSelect {
+ .UserMultiSelector__value-container {
+ display: flex;
+ }
+ }
}
diff --git a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_multiselector.tsx b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_multiselector.tsx
index 9ec451d7a47..d3a32e0ab65 100644
--- a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_multiselector.tsx
+++ b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_multiselector.tsx
@@ -5,7 +5,7 @@ import classNames from 'classnames';
import React, {type ReactElement, useCallback, useEffect, useMemo, useRef} from 'react';
import {useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
-import type {MultiValue} from 'react-select';
+import type {MultiValue, SingleValue} from 'react-select';
import AsyncSelect from 'react-select/async';
import type {UserProfile} from '@mattermost/types/users';
@@ -16,9 +16,9 @@ import {getUsersByIDs} from 'mattermost-redux/selectors/entities/users';
import type {GlobalState} from 'types/store';
-import {UserProfilePill} from './user_profile_pill';
+import {MultiUserProfilePill, SingleUserProfilePill} from './user_profile_pill';
-import {UserOptionComponent} from '../../content_flagging/user_multiselector/user_profile_option';
+import {MultiUserOptionComponent, SingleUserOptionComponent} from '../../content_flagging/user_multiselector/user_profile_option';
import {LoadingIndicator} from '../../system_users/system_users_filters_popover/system_users_filter_team';
import './user_multiselect.scss';
@@ -29,29 +29,51 @@ export type AutocompleteOptionType = {
raw?: T;
}
-type Props = {
- id: string;
- className?: string;
- onChange: (selectedUserIds: string[]) => void;
- initialValue?: string[];
- hasError?: boolean;
+const BASE_SELECT_COMPONENTS = {
+ LoadingIndicator,
+ DropdownIndicator: () => null,
+ IndicatorSeparator: () => null,
+};
+
+type MultiSelectProps = {
+ multiSelectOnChange?: (selectedUserIds: string[]) => void;
+ multiSelectInitialValue?: string[];
}
-export function UserMultiSelector({id, className, onChange, initialValue, hasError}: Props) {
+type SingleSelectProps = {
+ singleSelectOnChange?: (selectedUserIds: string) => void;
+ singleSelectInitialValue?: string;
+}
+
+type Props = MultiSelectProps & SingleSelectProps & {
+ id: string;
+ isMulti: boolean;
+ className?: string;
+ hasError?: boolean;
+ placeholder?: React.ReactNode;
+ showDropdownIndicator?: boolean;
+};
+
+export function UserSelector({id, isMulti, className, multiSelectOnChange, multiSelectInitialValue, singleSelectOnChange, singleSelectInitialValue, hasError, placeholder, showDropdownIndicator}: Props) {
const dispatch = useDispatch();
const {formatMessage} = useIntl();
const initialDataLoaded = useRef(false);
+ const initialValue = useMemo(() => {
+ return isMulti ? multiSelectInitialValue : [singleSelectInitialValue || ''];
+ }, [isMulti, multiSelectInitialValue, singleSelectInitialValue]);
+
useEffect(() => {
const fetchInitialData = async () => {
- await dispatch(getMissingProfilesByIds(initialValue || []));
+ const param = isMulti ? multiSelectInitialValue : [singleSelectInitialValue || ''];
+ await dispatch(getMissingProfilesByIds(param || []));
initialDataLoaded.current = true;
};
- if (initialValue && !initialDataLoaded.current) {
+ if (Boolean(initialValue) && !initialDataLoaded.current) {
fetchInitialData();
}
- }, [dispatch, initialValue]);
+ }, [dispatch, initialValue, isMulti, multiSelectInitialValue, singleSelectInitialValue]);
const initialUsers = useSelector((state: GlobalState) => getUsersByIDs(state, initialValue || []));
const selectInitialValue = initialUsers.
@@ -64,7 +86,7 @@ export function UserMultiSelector({id, className, onChange, initialValue, hasErr
const userLoadingMessage = useCallback(() => formatMessage({id: 'admin.userMultiSelector.loading', defaultMessage: 'Loading users'}), [formatMessage]);
const noUsersMessage = useCallback(() => formatMessage({id: 'admin.userMultiSelector.noUsers', defaultMessage: 'No users found'}), [formatMessage]);
- const placeholder = formatMessage({id: 'admin.userMultiSelector.placeholder', defaultMessage: 'Start typing to search for users...'});
+ const defaultPlaceholder = formatMessage({id: 'admin.userMultiSelector.placeholder', defaultMessage: 'Start typing to search for users...'});
const searchUsers = useMemo(() => debounce(async (searchTerm: string, callback) => {
try {
@@ -89,35 +111,87 @@ export function UserMultiSelector({id, className, onChange, initialValue, hasErr
}
}, 200), [dispatch]);
- function handleOnChange(value: MultiValue>) {
+ const multiSelectHandleOnChange = useCallback((value: MultiValue>) => {
const selectedUserIds = value.map((option) => option.value);
- onChange?.(selectedUserIds);
+ multiSelectOnChange?.(selectedUserIds);
+ }, [multiSelectOnChange]);
+
+ const singleSelectHandleOnChange = useCallback((value: SingleValue>) => {
+ const selectedUserIds = value?.value || '';
+ singleSelectOnChange?.(selectedUserIds);
+ }, [singleSelectOnChange]);
+
+ const multiSelectComponents = useMemo(() => {
+ const componentObj = {
+ ...BASE_SELECT_COMPONENTS,
+ Option: MultiUserOptionComponent,
+ MultiValue: MultiUserProfilePill,
+ };
+
+ if (showDropdownIndicator) {
+ // @ts-expect-error doing this any other way runs into TypeScript nightmares due to very complex ReactSelect types
+ delete componentObj.DropdownIndicator;
+ }
+
+ return componentObj;
+ }, [showDropdownIndicator]);
+
+ const singleSelectComponents = useMemo(() => {
+ const componentObj = {
+ ...BASE_SELECT_COMPONENTS,
+ Option: SingleUserOptionComponent,
+ SingleValue: SingleUserProfilePill,
+ };
+
+ if (showDropdownIndicator) {
+ // @ts-expect-error doing this any other way runs into TypeScript nightmares due to very complex ReactSelect types
+ delete componentObj.DropdownIndicator;
+ }
+
+ return componentObj;
+ }, [showDropdownIndicator]);
+
+ const baseProps = useMemo(() => {
+ return {
+ id,
+ inputId: `${id}_input`,
+ classNamePrefix: 'UserMultiSelector',
+ className: classNames('Input Input__focus', className, {error: hasError}),
+ isClearable: false,
+ hideSelectedOptions: true,
+ cacheOptions: true,
+ placeholder: placeholder || defaultPlaceholder,
+ loadingMessage: userLoadingMessage,
+ noOptionsMessage: noUsersMessage,
+ loadOptions: searchUsers,
+ menuPortalTarget: document.body,
+ };
+ }, [className, defaultPlaceholder, hasError, id, noUsersMessage, placeholder, searchUsers, userLoadingMessage]);
+
+ const containerClassName = classNames('UserMultiSelector', {multiSelect: isMulti, singleSelect: !isMulti});
+
+ if (isMulti) {
+ return (
+
+ );
}
return (
-
+
null,
- IndicatorSeparator: () => null,
- Option: UserOptionComponent,
- MultiValue: UserProfilePill,
- }}
+ {...baseProps}
+ isMulti={false}
+ onChange={singleSelectHandleOnChange}
+ value={selectInitialValue ? selectInitialValue[0] : null}
+ components={singleSelectComponents}
/>
);
diff --git a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_option.tsx b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_option.tsx
index 83124747dae..2ce251270b7 100644
--- a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_option.tsx
+++ b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_option.tsx
@@ -17,7 +17,29 @@ import type {AutocompleteOptionType} from './user_multiselector';
import './user_profile_option.scss';
-export function UserOptionComponent(props: OptionProps
, true>) {
+export function MultiUserOptionComponent(props: OptionProps, true>) {
+ const {data, innerProps} = props;
+
+ const userProfile = data.raw;
+ const userDisplayName = useSelector((state: GlobalState) => getDisplayNameByUser(state, userProfile));
+
+ return (
+
+
+
+ {userDisplayName}
+
+ );
+}
+
+export function SingleUserOptionComponent(props: OptionProps, false>) {
const {data, innerProps} = props;
const userProfile = data.raw;
diff --git a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_pill.test.tsx b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_pill.test.tsx
index 505888dcebb..650587396eb 100644
--- a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_pill.test.tsx
+++ b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_pill.test.tsx
@@ -10,7 +10,7 @@ import {fireEvent, renderWithContext} from 'tests/react_testing_utils';
import {TestHelper} from 'utils/test_helper';
import type {AutocompleteOptionType} from './user_multiselector';
-import {UserProfilePill} from './user_profile_pill';
+import {MultiUserProfilePill} from './user_profile_pill';
describe('components/admin_console/content_flagging/user_multiselector/UserProfilePill', () => {
const baseProps = {
@@ -70,7 +70,7 @@ describe('components/admin_console/content_flagging/user_multiselector/UserProfi
test('should render user profile pill with avatar and display name', () => {
const {container} = renderWithContext(
- ,
+ ,
initialState,
);
@@ -79,7 +79,7 @@ describe('components/admin_console/content_flagging/user_multiselector/UserProfi
test('should render with correct user display name', () => {
const {container} = renderWithContext(
- ,
+ ,
initialState,
);
@@ -90,7 +90,7 @@ describe('components/admin_console/content_flagging/user_multiselector/UserProfi
test('should render Avatar component with correct props', () => {
const {container} = renderWithContext(
- ,
+ ,
initialState,
);
@@ -100,7 +100,7 @@ describe('components/admin_console/content_flagging/user_multiselector/UserProfi
test('should render Remove component with close icon', () => {
const {container} = renderWithContext(
- ,
+ ,
initialState,
);
@@ -118,7 +118,7 @@ describe('components/admin_console/content_flagging/user_multiselector/UserProfi
};
const {container} = renderWithContext(
- ,
+ ,
initialState,
);
@@ -143,7 +143,7 @@ describe('components/admin_console/content_flagging/user_multiselector/UserProfi
} as unknown as MultiValueProps, true>;
const {container} = renderWithContext(
- ,
+ ,
initialState,
);
@@ -165,7 +165,7 @@ describe('components/admin_console/content_flagging/user_multiselector/UserProfi
};
const {container} = renderWithContext(
- ,
+ ,
stateWithUsernameDisplay,
);
@@ -175,7 +175,7 @@ describe('components/admin_console/content_flagging/user_multiselector/UserProfi
test('should apply correct CSS classes', () => {
const {container} = renderWithContext(
- ,
+ ,
initialState,
);
diff --git a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_pill.tsx b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_pill.tsx
index b0e3f5e6961..d6b058e9f71 100644
--- a/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_pill.tsx
+++ b/webapp/channels/src/components/admin_console/content_flagging/user_multiselector/user_profile_pill.tsx
@@ -1,8 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
+import type {JSX} from 'react';
import React from 'react';
import {useSelector} from 'react-redux';
+import type {SingleValueProps} from 'react-select';
import type {MultiValueProps} from 'react-select/dist/declarations/src/components/MultiValue';
import type {UserProfile} from '@mattermost/types/users';
@@ -32,9 +34,39 @@ function Remove(props: any) {
);
}
-export function UserProfilePill(props: MultiValueProps, true>) {
+export function MultiUserProfilePill(props: MultiValueProps, true>) {
const {data, innerProps, selectProps, removeProps} = props;
+ return (
+
+ );
+}
+
+export function SingleUserProfilePill(props: SingleValueProps, false>) {
+ const {data, innerProps, selectProps} = props;
+
+ return (
+
+ );
+}
+
+type Props = {
+ data: AutocompleteOptionType;
+ innerProps: JSX.IntrinsicElements['div'];
+ selectProps: unknown;
+ removeProps?: JSX.IntrinsicElements['div'];
+}
+
+function BaseUserProfilePill({data, innerProps, selectProps, removeProps}: Props) {
const userProfile = data.raw;
const userDisplayName = useSelector((state: GlobalState) => getDisplayNameByUser(state, userProfile));
@@ -51,12 +83,15 @@ export function UserProfilePill(props: MultiValueProps
+ {
+ removeProps &&
+
+ }
);
}
diff --git a/webapp/channels/src/components/channel_invite_modal/group_option/__snapshots__/group_option.test.tsx.snap b/webapp/channels/src/components/channel_invite_modal/group_option/__snapshots__/group_option.test.tsx.snap
index 5aebc108033..258367774a2 100644
--- a/webapp/channels/src/components/channel_invite_modal/group_option/__snapshots__/group_option.test.tsx.snap
+++ b/webapp/channels/src/components/channel_invite_modal/group_option/__snapshots__/group_option.test.tsx.snap
@@ -184,6 +184,13 @@ Object {
"queryByTitle": [Function],
"replaceStoreState": [Function],
"rerender": [Function],
+ "store": Object {
+ "@@observable": [Function],
+ "dispatch": [Function],
+ "getState": [Function],
+ "replaceReducer": [Function],
+ "subscribe": [Function],
+ },
"unmount": [Function],
"updateStoreState": [Function],
}
diff --git a/webapp/channels/src/components/channel_notifications_modal/__snapshots__/channel_notifications_modal.test.tsx.snap b/webapp/channels/src/components/channel_notifications_modal/__snapshots__/channel_notifications_modal.test.tsx.snap
deleted file mode 100644
index c8635b16c00..00000000000
--- a/webapp/channels/src/components/channel_notifications_modal/__snapshots__/channel_notifications_modal.test.tsx.snap
+++ /dev/null
@@ -1,3181 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ChannelNotificationsModal should check the options in the desktop notifications 1`] = `
-Object {
- "asFragment": [Function],
- "baseElement":
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ,
- "container": ,
- "debug": [Function],
- "findAllByAltText": [Function],
- "findAllByDisplayValue": [Function],
- "findAllByLabelText": [Function],
- "findAllByPlaceholderText": [Function],
- "findAllByRole": [Function],
- "findAllByTestId": [Function],
- "findAllByText": [Function],
- "findAllByTitle": [Function],
- "findByAltText": [Function],
- "findByDisplayValue": [Function],
- "findByLabelText": [Function],
- "findByPlaceholderText": [Function],
- "findByRole": [Function],
- "findByTestId": [Function],
- "findByText": [Function],
- "findByTitle": [Function],
- "getAllByAltText": [Function],
- "getAllByDisplayValue": [Function],
- "getAllByLabelText": [Function],
- "getAllByPlaceholderText": [Function],
- "getAllByRole": [Function],
- "getAllByTestId": [Function],
- "getAllByText": [Function],
- "getAllByTitle": [Function],
- "getByAltText": [Function],
- "getByDisplayValue": [Function],
- "getByLabelText": [Function],
- "getByPlaceholderText": [Function],
- "getByRole": [Function],
- "getByTestId": [Function],
- "getByText": [Function],
- "getByTitle": [Function],
- "queryAllByAltText": [Function],
- "queryAllByDisplayValue": [Function],
- "queryAllByLabelText": [Function],
- "queryAllByPlaceholderText": [Function],
- "queryAllByRole": [Function],
- "queryAllByTestId": [Function],
- "queryAllByText": [Function],
- "queryAllByTitle": [Function],
- "queryByAltText": [Function],
- "queryByDisplayValue": [Function],
- "queryByLabelText": [Function],
- "queryByPlaceholderText": [Function],
- "queryByRole": [Function],
- "queryByTestId": [Function],
- "queryByText": [Function],
- "queryByTitle": [Function],
- "replaceStoreState": [Function],
- "rerender": [Function],
- "unmount": [Function],
- "updateStoreState": [Function],
-}
-`;
-
-exports[`ChannelNotificationsModal should check the options in the mobile notifications 1`] = `
-Object {
- "asFragment": [Function],
- "baseElement":
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Sounds
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ,
- "container": ,
- "debug": [Function],
- "findAllByAltText": [Function],
- "findAllByDisplayValue": [Function],
- "findAllByLabelText": [Function],
- "findAllByPlaceholderText": [Function],
- "findAllByRole": [Function],
- "findAllByTestId": [Function],
- "findAllByText": [Function],
- "findAllByTitle": [Function],
- "findByAltText": [Function],
- "findByDisplayValue": [Function],
- "findByLabelText": [Function],
- "findByPlaceholderText": [Function],
- "findByRole": [Function],
- "findByTestId": [Function],
- "findByText": [Function],
- "findByTitle": [Function],
- "getAllByAltText": [Function],
- "getAllByDisplayValue": [Function],
- "getAllByLabelText": [Function],
- "getAllByPlaceholderText": [Function],
- "getAllByRole": [Function],
- "getAllByTestId": [Function],
- "getAllByText": [Function],
- "getAllByTitle": [Function],
- "getByAltText": [Function],
- "getByDisplayValue": [Function],
- "getByLabelText": [Function],
- "getByPlaceholderText": [Function],
- "getByRole": [Function],
- "getByTestId": [Function],
- "getByText": [Function],
- "getByTitle": [Function],
- "queryAllByAltText": [Function],
- "queryAllByDisplayValue": [Function],
- "queryAllByLabelText": [Function],
- "queryAllByPlaceholderText": [Function],
- "queryAllByRole": [Function],
- "queryAllByTestId": [Function],
- "queryAllByText": [Function],
- "queryAllByTitle": [Function],
- "queryByAltText": [Function],
- "queryByDisplayValue": [Function],
- "queryByLabelText": [Function],
- "queryByPlaceholderText": [Function],
- "queryByRole": [Function],
- "queryByTestId": [Function],
- "queryByText": [Function],
- "queryByTitle": [Function],
- "replaceStoreState": [Function],
- "rerender": [Function],
- "unmount": [Function],
- "updateStoreState": [Function],
-}
-`;
-
-exports[`ChannelNotificationsModal should not show other settings if channel is mute 1`] = `
-Object {
- "asFragment": [Function],
- "baseElement":
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- This channel is muted
-
-
- All other notification preferences for this channel are disabled
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ,
- "container": ,
- "debug": [Function],
- "findAllByAltText": [Function],
- "findAllByDisplayValue": [Function],
- "findAllByLabelText": [Function],
- "findAllByPlaceholderText": [Function],
- "findAllByRole": [Function],
- "findAllByTestId": [Function],
- "findAllByText": [Function],
- "findAllByTitle": [Function],
- "findByAltText": [Function],
- "findByDisplayValue": [Function],
- "findByLabelText": [Function],
- "findByPlaceholderText": [Function],
- "findByRole": [Function],
- "findByTestId": [Function],
- "findByText": [Function],
- "findByTitle": [Function],
- "getAllByAltText": [Function],
- "getAllByDisplayValue": [Function],
- "getAllByLabelText": [Function],
- "getAllByPlaceholderText": [Function],
- "getAllByRole": [Function],
- "getAllByTestId": [Function],
- "getAllByText": [Function],
- "getAllByTitle": [Function],
- "getByAltText": [Function],
- "getByDisplayValue": [Function],
- "getByLabelText": [Function],
- "getByPlaceholderText": [Function],
- "getByRole": [Function],
- "getByTestId": [Function],
- "getByText": [Function],
- "getByTitle": [Function],
- "queryAllByAltText": [Function],
- "queryAllByDisplayValue": [Function],
- "queryAllByLabelText": [Function],
- "queryAllByPlaceholderText": [Function],
- "queryAllByRole": [Function],
- "queryAllByTestId": [Function],
- "queryAllByText": [Function],
- "queryAllByTitle": [Function],
- "queryByAltText": [Function],
- "queryByDisplayValue": [Function],
- "queryByLabelText": [Function],
- "queryByPlaceholderText": [Function],
- "queryByRole": [Function],
- "queryByTestId": [Function],
- "queryByText": [Function],
- "queryByTitle": [Function],
- "replaceStoreState": [Function],
- "rerender": [Function],
- "unmount": [Function],
- "updateStoreState": [Function],
-}
-`;
-
-exports[`ChannelNotificationsModal should save correctly for 'Ignore mentions for @channel, @here and @all' 1`] = `
-Object {
- "asFragment": [Function],
- "baseElement":
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Sounds
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ,
- "container": ,
- "debug": [Function],
- "findAllByAltText": [Function],
- "findAllByDisplayValue": [Function],
- "findAllByLabelText": [Function],
- "findAllByPlaceholderText": [Function],
- "findAllByRole": [Function],
- "findAllByTestId": [Function],
- "findAllByText": [Function],
- "findAllByTitle": [Function],
- "findByAltText": [Function],
- "findByDisplayValue": [Function],
- "findByLabelText": [Function],
- "findByPlaceholderText": [Function],
- "findByRole": [Function],
- "findByTestId": [Function],
- "findByText": [Function],
- "findByTitle": [Function],
- "getAllByAltText": [Function],
- "getAllByDisplayValue": [Function],
- "getAllByLabelText": [Function],
- "getAllByPlaceholderText": [Function],
- "getAllByRole": [Function],
- "getAllByTestId": [Function],
- "getAllByText": [Function],
- "getAllByTitle": [Function],
- "getByAltText": [Function],
- "getByDisplayValue": [Function],
- "getByLabelText": [Function],
- "getByPlaceholderText": [Function],
- "getByRole": [Function],
- "getByTestId": [Function],
- "getByText": [Function],
- "getByTitle": [Function],
- "queryAllByAltText": [Function],
- "queryAllByDisplayValue": [Function],
- "queryAllByLabelText": [Function],
- "queryAllByPlaceholderText": [Function],
- "queryAllByRole": [Function],
- "queryAllByTestId": [Function],
- "queryAllByText": [Function],
- "queryAllByTitle": [Function],
- "queryByAltText": [Function],
- "queryByDisplayValue": [Function],
- "queryByLabelText": [Function],
- "queryByPlaceholderText": [Function],
- "queryByRole": [Function],
- "queryByTestId": [Function],
- "queryByText": [Function],
- "queryByTitle": [Function],
- "replaceStoreState": [Function],
- "rerender": [Function],
- "unmount": [Function],
- "updateStoreState": [Function],
-}
-`;
-
-exports[`ChannelNotificationsModal should save the options exactly same as Desktop for mobile if use same as desktop checkbox is checked 1`] = `
-Object {
- "asFragment": [Function],
- "baseElement":
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Sounds
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ,
- "container": ,
- "debug": [Function],
- "findAllByAltText": [Function],
- "findAllByDisplayValue": [Function],
- "findAllByLabelText": [Function],
- "findAllByPlaceholderText": [Function],
- "findAllByRole": [Function],
- "findAllByTestId": [Function],
- "findAllByText": [Function],
- "findAllByTitle": [Function],
- "findByAltText": [Function],
- "findByDisplayValue": [Function],
- "findByLabelText": [Function],
- "findByPlaceholderText": [Function],
- "findByRole": [Function],
- "findByTestId": [Function],
- "findByText": [Function],
- "findByTitle": [Function],
- "getAllByAltText": [Function],
- "getAllByDisplayValue": [Function],
- "getAllByLabelText": [Function],
- "getAllByPlaceholderText": [Function],
- "getAllByRole": [Function],
- "getAllByTestId": [Function],
- "getAllByText": [Function],
- "getAllByTitle": [Function],
- "getByAltText": [Function],
- "getByDisplayValue": [Function],
- "getByLabelText": [Function],
- "getByPlaceholderText": [Function],
- "getByRole": [Function],
- "getByTestId": [Function],
- "getByText": [Function],
- "getByTitle": [Function],
- "queryAllByAltText": [Function],
- "queryAllByDisplayValue": [Function],
- "queryAllByLabelText": [Function],
- "queryAllByPlaceholderText": [Function],
- "queryAllByRole": [Function],
- "queryAllByTestId": [Function],
- "queryAllByText": [Function],
- "queryAllByTitle": [Function],
- "queryByAltText": [Function],
- "queryByDisplayValue": [Function],
- "queryByLabelText": [Function],
- "queryByPlaceholderText": [Function],
- "queryByRole": [Function],
- "queryByTestId": [Function],
- "queryByText": [Function],
- "queryByTitle": [Function],
- "replaceStoreState": [Function],
- "rerender": [Function],
- "unmount": [Function],
- "updateStoreState": [Function],
-}
-`;
-
-exports[`ChannelNotificationsModal should show not auto follow, desktop threads and mobile threads settings if collapsed reply threads is enabled 1`] = `
-Object {
- "asFragment": [Function],
- "baseElement":
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Sounds
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ,
- "container": ,
- "debug": [Function],
- "findAllByAltText": [Function],
- "findAllByDisplayValue": [Function],
- "findAllByLabelText": [Function],
- "findAllByPlaceholderText": [Function],
- "findAllByRole": [Function],
- "findAllByTestId": [Function],
- "findAllByText": [Function],
- "findAllByTitle": [Function],
- "findByAltText": [Function],
- "findByDisplayValue": [Function],
- "findByLabelText": [Function],
- "findByPlaceholderText": [Function],
- "findByRole": [Function],
- "findByTestId": [Function],
- "findByText": [Function],
- "findByTitle": [Function],
- "getAllByAltText": [Function],
- "getAllByDisplayValue": [Function],
- "getAllByLabelText": [Function],
- "getAllByPlaceholderText": [Function],
- "getAllByRole": [Function],
- "getAllByTestId": [Function],
- "getAllByText": [Function],
- "getAllByTitle": [Function],
- "getByAltText": [Function],
- "getByDisplayValue": [Function],
- "getByLabelText": [Function],
- "getByPlaceholderText": [Function],
- "getByRole": [Function],
- "getByTestId": [Function],
- "getByText": [Function],
- "getByTitle": [Function],
- "queryAllByAltText": [Function],
- "queryAllByDisplayValue": [Function],
- "queryAllByLabelText": [Function],
- "queryAllByPlaceholderText": [Function],
- "queryAllByRole": [Function],
- "queryAllByTestId": [Function],
- "queryAllByText": [Function],
- "queryAllByTitle": [Function],
- "queryByAltText": [Function],
- "queryByDisplayValue": [Function],
- "queryByLabelText": [Function],
- "queryByPlaceholderText": [Function],
- "queryByRole": [Function],
- "queryByTestId": [Function],
- "queryByText": [Function],
- "queryByTitle": [Function],
- "replaceStoreState": [Function],
- "rerender": [Function],
- "unmount": [Function],
- "updateStoreState": [Function],
-}
-`;
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 d2e8b02d91f..217bf9bd642 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,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {screen, fireEvent, waitFor} from '@testing-library/react';
+import {act, screen, waitFor} from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import React from 'react';
import type {ChannelMembership} from '@mattermost/types/channels';
@@ -53,23 +54,25 @@ describe('ChannelNotificationsModal', () => {
};
test('should not show other settings if channel is mute', async () => {
- const wrapper = renderWithContext(
+ renderWithContext(
,
);
const muteChannel = screen.getByTestId('muteChannel');
- fireEvent.click(muteChannel);
+ await userEvent.click(muteChannel);
expect(muteChannel).toBeChecked();
- const AlertBanner = screen.queryByText('This channel is muted');
- expect(AlertBanner).toBeVisible();
- expect(screen.queryByText('Desktop Notifications')).toBeNull();
+ const alertBanner = screen.getByText('This channel is muted');
+ expect(alertBanner).toBeInTheDocument();
- expect(screen.queryByText('Mobile Notifications')).toBeNull();
- expect(screen.queryByText('Follow all threads in this channel')).not.toBeNull();
+ expect(screen.queryByText('Desktop Notifications')).not.toBeInTheDocument();
+ expect(screen.queryByText('Mobile Notifications')).not.toBeInTheDocument();
+ expect(screen.getByText('Follow all threads in this channel')).toBeInTheDocument();
- fireEvent.click(screen.getByRole('button', {name: /Save/i}));
+ await act(async () => {
+ await userEvent.click(screen.getByRole('button', {name: /Save/i}));
+ });
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
@@ -88,18 +91,25 @@ describe('ChannelNotificationsModal', () => {
},
),
);
- expect(wrapper).toMatchSnapshot();
});
test('should save correctly for \'Ignore mentions for @channel, @here and @all\'', async () => {
- const wrapper = renderWithContext(
+ renderWithContext(
,
);
+
const ignoreChannel = screen.getByTestId('ignoreMentions');
- fireEvent.click(ignoreChannel);
+ await act(async () => {
+ await userEvent.click(ignoreChannel);
+ });
expect(ignoreChannel).toBeChecked();
- fireEvent.click(screen.getByRole('button', {name: /Save/i}));
+ // 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 waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
@@ -117,35 +127,50 @@ describe('ChannelNotificationsModal', () => {
},
),
);
- expect(wrapper).toMatchSnapshot();
});
test('should check the options in the desktop notifications', async () => {
- const wrapper = renderWithContext(
+ renderWithContext(
,
);
- expect(screen.queryByText('Desktop Notifications')).toBeVisible();
+ expect(screen.getByText('Desktop Notifications')).toBeInTheDocument();
- const AlllabelRadio: HTMLInputElement = screen.getByTestId(
- 'desktopNotification-all',
- );
- fireEvent.click(AlllabelRadio);
- expect(AlllabelRadio.checked).toEqual(true);
+ const allRadio: HTMLInputElement = screen.getByTestId('desktopNotification-all');
+ const mentionsRadio: HTMLInputElement = screen.getByTestId('desktopNotification-mention');
+ const nothingRadio: HTMLInputElement = screen.getByTestId('desktopNotification-none');
- const MentionslabelRadio: HTMLInputElement = screen.getByTestId(
- 'desktopNotification-mention',
- );
- fireEvent.click(MentionslabelRadio);
- expect(MentionslabelRadio.checked).toEqual(true);
+ // Verify all radio options are present
+ expect(allRadio).toBeInTheDocument();
+ expect(mentionsRadio).toBeInTheDocument();
+ expect(nothingRadio).toBeInTheDocument();
- const NothinglabelRadio: HTMLInputElement = screen.getByTestId(
- 'desktopNotification-none',
- );
- fireEvent.click(NothinglabelRadio);
- expect(NothinglabelRadio.checked).toEqual(true);
+ // Test clicking through the options
+ await act(async () => {
+ await userEvent.click(allRadio);
+ });
+ expect(allRadio).toBeChecked();
- fireEvent.click(screen.getByRole('button', {name: /Save/i}));
+ await act(async () => {
+ await userEvent.click(mentionsRadio);
+ });
+ expect(mentionsRadio).toBeChecked();
+ expect(allRadio).not.toBeChecked();
+
+ await act(async () => {
+ await userEvent.click(nothingRadio);
+ });
+ expect(nothingRadio).toBeChecked();
+ expect(mentionsRadio).not.toBeChecked();
+
+ // Verify the labels are present
+ expect(screen.getByText(/All new messages/)).toBeInTheDocument();
+ 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 waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
@@ -163,17 +188,20 @@ describe('ChannelNotificationsModal', () => {
},
),
);
- expect(wrapper).toMatchSnapshot();
});
test('should disable message notification sound if option is unchecked', async () => {
renderWithContext();
// Since the default value is checked, we will uncheck the checkbox
- fireEvent.click(screen.getByTestId('desktopNotificationSoundsCheckbox'));
+ await act(async () => {
+ await userEvent.click(screen.getByTestId('desktopNotificationSoundsCheckbox'));
+ });
expect(screen.getByTestId('desktopNotificationSoundsCheckbox')).not.toBeChecked();
- fireEvent.click(screen.getByRole('button', {name: /Save/i}));
+ await act(async () => {
+ await userEvent.click(screen.getByRole('button', {name: /Save/i}));
+ });
await waitFor(() => {
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
@@ -197,31 +225,36 @@ describe('ChannelNotificationsModal', () => {
renderWithContext();
// Since the default value is on, we will uncheck the checkbox
- fireEvent.click(screen.getByTestId('desktopNotificationSoundsCheckbox'));
+ await userEvent.click(screen.getByTestId('desktopNotificationSoundsCheckbox'));
expect(screen.getByTestId('desktopNotificationSoundsCheckbox')).not.toBeChecked();
// Reset to default button is clicked
- fireEvent.click(screen.getByTestId('resetToDefaultButton-desktop'));
+ await userEvent.click(screen.getByTestId('resetToDefaultButton-desktop'));
// Verify that the checkbox is checked to default to user desktop notification sound
expect(screen.getByTestId('desktopNotificationSoundsCheckbox')).toBeChecked();
});
test('should save the options exactly same as Desktop for mobile if use same as desktop checkbox is checked', async () => {
- const wrapper = renderWithContext(
+ renderWithContext(
,
);
- expect(screen.queryByText('Desktop Notifications')).toBeVisible();
+ expect(screen.getByText('Desktop Notifications')).toBeInTheDocument();
+ expect(screen.getByText('Mobile Notifications')).toBeInTheDocument();
- const sameAsDesktop: HTMLInputElement = screen.getByTestId(
- 'sameMobileSettingsDesktop',
- );
- expect(sameAsDesktop.checked).toEqual(true);
+ const sameAsDesktop: HTMLInputElement = screen.getByTestId('sameMobileSettingsDesktop');
+ expect(sameAsDesktop).toBeChecked();
- expect(screen.queryByText('All new messages')).toBeNull();
+ // Verify the checkbox label is present
+ expect(screen.getByText('Use the same notification settings as desktop')).toBeInTheDocument();
- fireEvent.click(screen.getByRole('button', {name: /Save/i}));
+ // 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 waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
@@ -239,7 +272,6 @@ describe('ChannelNotificationsModal', () => {
},
),
);
- expect(wrapper).toMatchSnapshot();
});
test('should check the options in the mobile notifications', async () => {
@@ -252,20 +284,42 @@ describe('ChannelNotificationsModal', () => {
},
} as unknown as ChannelMembership,
};
- const wrapper = renderWithContext(
+ renderWithContext(
,
);
- fireEvent.click(screen.getByTestId('MobileNotification-all'));
- expect(screen.getByTestId('MobileNotification-all')).toBeChecked();
+ // First uncheck "same as desktop" to show mobile options
+ const sameAsDesktop = screen.getByTestId('sameMobileSettingsDesktop');
+ expect(sameAsDesktop).not.toBeChecked();
- fireEvent.click(screen.getByTestId('MobileNotification-mention'));
- expect(screen.getByTestId('MobileNotification-mention')).toBeChecked();
+ // Now mobile notification options should be visible
+ expect(screen.getByTestId('mobile-notify-me-radio-section')).toBeInTheDocument();
- fireEvent.click(screen.getByTestId('MobileNotification-none'));
- expect(screen.getByTestId('MobileNotification-none')).toBeChecked();
+ const allRadio = screen.getByTestId('MobileNotification-all');
+ const mentionRadio = screen.getByTestId('MobileNotification-mention');
+ const noneRadio = screen.getByTestId('MobileNotification-none');
- fireEvent.click(screen.getByRole('button', {name: /Save/i}));
+ // Test clicking through the mobile notification options
+ await act(async () => {
+ await userEvent.click(allRadio);
+ });
+ expect(allRadio).toBeChecked();
+
+ await act(async () => {
+ await userEvent.click(mentionRadio);
+ });
+ expect(mentionRadio).toBeChecked();
+ expect(allRadio).not.toBeChecked();
+
+ await act(async () => {
+ await userEvent.click(noneRadio);
+ });
+ expect(noneRadio).toBeChecked();
+ expect(mentionRadio).not.toBeChecked();
+
+ await act(async () => {
+ await userEvent.click(screen.getByRole('button', {name: /Save/i}));
+ });
await waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
'current_user_id',
@@ -283,21 +337,28 @@ describe('ChannelNotificationsModal', () => {
},
),
);
- expect(wrapper).toMatchSnapshot();
});
- test('should show not auto follow, desktop threads and mobile threads settings if collapsed reply threads is enabled', async () => {
+ test('should not show auto follow threads section if collapsed reply threads is disabled', async () => {
const props = {
...baseProps,
collapsedReplyThreads: false,
};
- const wrapper = renderWithContext(
+ renderWithContext(
,
);
- expect(screen.queryByText('Follow all threads in this channel')).toBeNull();
+ // Auto follow threads section should not be visible when collapsed threads is disabled
+ expect(screen.queryByText('Follow all threads in this channel')).not.toBeInTheDocument();
- fireEvent.click(screen.getByRole('button', {name: /Save/i}));
+ // But other sections should still be present
+ expect(screen.getByText('Desktop Notifications')).toBeInTheDocument();
+ 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 waitFor(() =>
expect(baseProps.actions.updateChannelNotifyProps).toHaveBeenCalledWith(
@@ -316,7 +377,6 @@ describe('ChannelNotificationsModal', () => {
},
),
);
- expect(wrapper).toMatchSnapshot();
});
});
diff --git a/webapp/channels/src/components/common/hooks/useChannel.test.ts b/webapp/channels/src/components/common/hooks/useChannel.test.ts
new file mode 100644
index 00000000000..3e19b8e964a
--- /dev/null
+++ b/webapp/channels/src/components/common/hooks/useChannel.test.ts
@@ -0,0 +1,207 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import nock from 'nock';
+import * as ReactRedux from 'react-redux';
+
+import {Client4} from 'mattermost-redux/client';
+
+import {renderHookWithContext} from 'tests/react_testing_utils';
+import {TestHelper} from 'utils/test_helper';
+
+import {useChannel} from './useChannel';
+
+describe('useChannel', () => {
+ const channel1 = TestHelper.getChannelMock({id: 'channel1'});
+ const channel2 = TestHelper.getChannelMock({id: 'channel2'});
+
+ describe('with fake dispatch', () => {
+ const dispatchMock = jest.fn();
+
+ beforeAll(() => {
+ jest.spyOn(ReactRedux, 'useDispatch').mockImplementation(() => dispatchMock);
+ });
+
+ afterAll(() => {
+ jest.restoreAllMocks();
+ });
+
+ test("should return the channel if it's already in the store", () => {
+ const {result} = renderHookWithContext(
+ () => useChannel('channel1'),
+ {
+ entities: {
+ channels: {
+ channels: {
+ channel1,
+ },
+ },
+ },
+ },
+ );
+
+ expect(result.current).toBe(channel1);
+ expect(dispatchMock).not.toHaveBeenCalled();
+ });
+
+ test("should fetch the channel if it's not in the store", () => {
+ const {result} = renderHookWithContext(
+ () => useChannel('channel1'),
+ );
+
+ expect(result.current).toBe(undefined);
+ expect(dispatchMock).toHaveBeenCalledTimes(1);
+ });
+
+ test('should only attempt to fetch the channel once regardless of how many times the hook is used', () => {
+ const {result, rerender} = renderHookWithContext(
+ () => useChannel('channel1'),
+ );
+
+ expect(result.current).toBe(undefined);
+ expect(dispatchMock).toHaveBeenCalledTimes(1);
+
+ for (let i = 0; i < 10; i++) {
+ rerender();
+ }
+
+ expect(result.current).toBe(undefined);
+ expect(dispatchMock).toHaveBeenCalledTimes(1);
+ });
+
+ test('should attempt to fetch different channels if the channel ID changes', () => {
+ let channelId = 'channel1';
+ const {result, rerender} = renderHookWithContext(
+ () => useChannel(channelId),
+ );
+
+ expect(result.current).toBe(undefined);
+ expect(dispatchMock).toHaveBeenCalledTimes(1);
+
+ channelId = 'channel2';
+ rerender();
+
+ expect(result.current).toBe(undefined);
+ expect(dispatchMock).toHaveBeenCalledTimes(2);
+ });
+
+ test("should only attempt to fetch each channel once when they aren't loaded", () => {
+ let channelId = 'channel1';
+ const {result, replaceStoreState, rerender} = renderHookWithContext(
+ () => useChannel(channelId),
+ );
+
+ // Initial state without channel1 loaded
+ expect(result.current).toBe(undefined);
+ expect(dispatchMock).toHaveBeenCalledTimes(1);
+
+ // Simulate the response to loading channel1
+ replaceStoreState({
+ entities: {
+ channels: {
+ channels: {
+ channel1,
+ },
+ },
+ },
+ });
+
+ expect(result.current).toBe(channel1);
+ expect(dispatchMock).toHaveBeenCalledTimes(1);
+
+ // Switch to channel2
+ channelId = 'channel2';
+
+ rerender();
+
+ expect(result.current).toBe(undefined);
+ expect(dispatchMock).toHaveBeenCalledTimes(2);
+
+ // Simulate the response to loading channel2
+ replaceStoreState({
+ entities: {
+ channels: {
+ channels: {
+ channel1,
+ channel2,
+ },
+ },
+ },
+ });
+
+ expect(result.current).toBe(channel2);
+ expect(dispatchMock).toHaveBeenCalledTimes(2);
+
+ // Switch back to channel1 which has already been loaded
+ channelId = 'channel1';
+
+ rerender();
+
+ expect(result.current).toBe(channel1);
+ expect(dispatchMock).toHaveBeenCalledTimes(2);
+ });
+
+ test("shouldn't attempt to load anything when given an empty channel ID", () => {
+ const {result} = renderHookWithContext(
+ () => useChannel(''),
+ );
+
+ expect(result.current).toBe(undefined);
+ expect(dispatchMock).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe('with real dispatch', () => {
+ beforeAll(() => {
+ Client4.setUrl('http://localhost:8065');
+ });
+
+ test("should only attempt to fetch each channel once when they aren't loaded", async () => {
+ const channel1Mock = nock(Client4.getBaseRoute()).
+ get(`/channels/${channel1.id}`).
+ once().
+ reply(200, channel1);
+ const channel2Mock = nock(Client4.getBaseRoute()).
+ get(`/channels/${channel2.id}`).
+ once().
+ reply(200, channel2);
+
+ let channelId = 'channel1';
+ const {result, rerender, waitForNextUpdate} = renderHookWithContext(
+ () => useChannel(channelId),
+ );
+
+ // Initial state without channel1 loaded
+ expect(result.current).toEqual(undefined);
+ expect(channel1Mock.isDone()).toBe(false);
+ expect(channel2Mock.isDone()).toBe(false);
+
+ // Wait for the response with channel1
+
+ await waitForNextUpdate();
+
+ expect(channel1Mock.isDone()).toBe(true);
+ expect(channel2Mock.isDone()).toBe(false);
+ expect(result.current).toEqual(channel1);
+
+ // Switch to channel2
+ channelId = 'channel2';
+ rerender();
+
+ expect(result.current).toEqual(undefined);
+
+ // Wait for the response with channel2
+ await waitForNextUpdate();
+
+ expect(channel1Mock.isDone()).toBe(true);
+ expect(channel2Mock.isDone()).toBe(true);
+ expect(result.current).toEqual(channel2);
+
+ // Switch back to channel1 which has already been loaded
+ channelId = 'channel1';
+ rerender();
+
+ expect(result.current).toEqual(channel1);
+ });
+ });
+});
diff --git a/webapp/channels/src/components/common/hooks/useChannel.ts b/webapp/channels/src/components/common/hooks/useChannel.ts
new file mode 100644
index 00000000000..a374869bb31
--- /dev/null
+++ b/webapp/channels/src/components/common/hooks/useChannel.ts
@@ -0,0 +1,15 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import type {Channel} from '@mattermost/types/channels';
+
+import {fetchMissingChannels} from 'mattermost-redux/actions/channels';
+import {getChannel} from 'mattermost-redux/selectors/entities/channels';
+
+import {makeUseEntity} from 'components/common/hooks/useEntity';
+
+export const useChannel = makeUseEntity({
+ name: 'useChannel',
+ fetch: (channelId: string) => fetchMissingChannels([channelId]),
+ selector: getChannel,
+});
diff --git a/webapp/channels/src/components/common/hooks/use_team.test.ts b/webapp/channels/src/components/common/hooks/use_team.test.ts
new file mode 100644
index 00000000000..ce95cb4ed26
--- /dev/null
+++ b/webapp/channels/src/components/common/hooks/use_team.test.ts
@@ -0,0 +1,229 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import nock from 'nock';
+import * as ReactRedux from 'react-redux';
+
+import {Client4} from 'mattermost-redux/client';
+
+import {renderHookWithContext} from 'tests/react_testing_utils';
+import {TestHelper} from 'utils/test_helper';
+
+import {useTeam} from './use_team';
+
+describe('useTeam', () => {
+ const team1 = TestHelper.getTeamMock({id: 'team1'});
+ const team2 = TestHelper.getTeamMock({id: 'team2'});
+
+ describe('with fake dispatch', () => {
+ const dispatchMock = jest.fn();
+
+ beforeAll(() => {
+ jest.spyOn(ReactRedux, 'useDispatch').mockImplementation(() => dispatchMock);
+ });
+
+ afterAll(() => {
+ jest.restoreAllMocks();
+ });
+
+ test("should return the team if it's already in the store", () => {
+ const {result} = renderHookWithContext(
+ () => useTeam('team1'),
+ {
+ entities: {
+ teams: {
+ teams: {
+ team1,
+ },
+ },
+ },
+ },
+ );
+
+ expect(result.current).toBe(team1);
+ expect(dispatchMock).not.toHaveBeenCalled();
+ });
+
+ test("should fetch the team if it's not in the store", () => {
+ const {result} = renderHookWithContext(
+ () => useTeam('team1'),
+ );
+
+ expect(result.current).toBe(undefined);
+ expect(dispatchMock).toHaveBeenCalledTimes(1);
+ });
+
+ test('should only attempt to fetch the team once regardless of how many times the hook is used', () => {
+ const {result, rerender} = renderHookWithContext(
+ () => useTeam('team1'),
+ );
+
+ expect(result.current).toBe(undefined);
+ expect(dispatchMock).toHaveBeenCalledTimes(1);
+
+ for (let i = 0; i < 10; i++) {
+ rerender();
+ }
+
+ expect(result.current).toBe(undefined);
+ expect(dispatchMock).toHaveBeenCalledTimes(1);
+ });
+
+ test('should attempt to fetch different teams if the team ID changes', () => {
+ let teamId = 'team1';
+ const {result, rerender} = renderHookWithContext(
+ () => useTeam(teamId),
+ );
+
+ expect(result.current).toBe(undefined);
+ expect(dispatchMock).toHaveBeenCalledTimes(1);
+
+ teamId = 'team2';
+ rerender();
+
+ expect(result.current).toBe(undefined);
+ expect(dispatchMock).toHaveBeenCalledTimes(2);
+ });
+
+ test("should only attempt to fetch each team once when they aren't loaded", () => {
+ let teamId = 'team1';
+ const {result, replaceStoreState, rerender} = renderHookWithContext(
+ () => useTeam(teamId),
+ );
+
+ // Initial state without team1 loaded
+ expect(result.current).toBe(undefined);
+ expect(dispatchMock).toHaveBeenCalledTimes(1);
+
+ // Simulate the response to loading team1
+ replaceStoreState({
+ entities: {
+ teams: {
+ teams: {
+ team1,
+ },
+ },
+ },
+ });
+
+ expect(result.current).toBe(team1);
+ expect(dispatchMock).toHaveBeenCalledTimes(1);
+
+ // Switch to team2
+ teamId = 'team2';
+
+ rerender();
+
+ expect(result.current).toBe(undefined);
+ expect(dispatchMock).toHaveBeenCalledTimes(2);
+
+ // Simulate the response to loading team2
+ replaceStoreState({
+ entities: {
+ teams: {
+ teams: {
+ team1,
+ team2,
+ },
+ },
+ },
+ });
+
+ expect(result.current).toBe(team2);
+ expect(dispatchMock).toHaveBeenCalledTimes(2);
+
+ // Switch back to team1 which has already been loaded
+ teamId = 'team1';
+
+ rerender();
+
+ expect(result.current).toBe(team1);
+ expect(dispatchMock).toHaveBeenCalledTimes(2);
+ });
+
+ test("shouldn't attempt to load anything when given an empty team ID", () => {
+ const {result} = renderHookWithContext(
+ () => useTeam(''),
+ );
+
+ expect(result.current).toBe(undefined);
+ expect(dispatchMock).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe('with real dispatch', () => {
+ beforeAll(() => {
+ Client4.setUrl('http://localhost:8065');
+ });
+
+ test("should fetch team when it's not loaded", async () => {
+ const teamMock = nock(Client4.getBaseRoute()).
+ get(`/teams/${team1.id}`).
+ once().
+ reply(200, team1);
+
+ const {result, waitForNextUpdate} = renderHookWithContext(
+ () => useTeam('team1'),
+ );
+
+ // Initial state without team1 loaded
+ expect(result.current).toEqual(undefined);
+ expect(teamMock.isDone()).toBe(false);
+
+ // Wait for the response with team1
+ await waitForNextUpdate();
+
+ expect(teamMock.isDone()).toBe(true);
+ expect(result.current).toEqual(team1);
+ });
+
+ test("should only attempt to fetch each team once when they aren't loaded", async () => {
+ const team1Mock = nock(Client4.getBaseRoute()).
+ get(`/teams/${team1.id}`).
+ once().
+ reply(200, team1);
+ const team2Mock = nock(Client4.getBaseRoute()).
+ get(`/teams/${team2.id}`).
+ once().
+ reply(200, team2);
+
+ let teamId = 'team1';
+ const {result, rerender, waitForNextUpdate} = renderHookWithContext(
+ () => useTeam(teamId),
+ );
+
+ // Initial state without team1 loaded
+ expect(result.current).toEqual(undefined);
+ expect(team1Mock.isDone()).toBe(false);
+ expect(team2Mock.isDone()).toBe(false);
+
+ // Wait for the response with team1
+ await waitForNextUpdate();
+
+ expect(team1Mock.isDone()).toBe(true);
+ expect(team2Mock.isDone()).toBe(false);
+ expect(result.current).toEqual(team1);
+
+ // Switch to team2
+ teamId = 'team2';
+ rerender();
+
+ expect(result.current).toEqual(undefined);
+
+ // Wait for the response with team2
+ await waitForNextUpdate();
+
+ expect(team1Mock.isDone()).toBe(true);
+ expect(team2Mock.isDone()).toBe(true);
+ expect(result.current).toEqual(team2);
+
+ // Switch back to team1 which has already been loaded
+ teamId = 'team1';
+ rerender();
+
+ expect(result.current).toEqual(team1);
+
+ // We know there's no second call because nock is set to only mock the first request for each team
+ });
+ });
+});
diff --git a/webapp/channels/src/components/common/hooks/use_team.ts b/webapp/channels/src/components/common/hooks/use_team.ts
new file mode 100644
index 00000000000..4e9912a7901
--- /dev/null
+++ b/webapp/channels/src/components/common/hooks/use_team.ts
@@ -0,0 +1,15 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import type {Team} from '@mattermost/types/teams';
+
+import {getTeam as getTeamAction} from 'mattermost-redux/actions/teams';
+import {getTeam as getTeamSelector} from 'mattermost-redux/selectors/entities/teams';
+
+import {makeUseEntity} from 'components/common/hooks/useEntity';
+
+export const useTeam = makeUseEntity({
+ name: 'useTeam',
+ fetch: getTeamAction,
+ selector: getTeamSelector,
+});
diff --git a/webapp/channels/src/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap b/webapp/channels/src/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap
deleted file mode 100644
index 9d6839e0d71..00000000000
--- a/webapp/channels/src/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap
+++ /dev/null
@@ -1,607 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`components/dot_menu/DotMenu should match snapshot, can move 1`] = `
-Object {
- "asFragment": [Function],
- "baseElement":
-
-
-
- ,
- "container":
-
-
,
- "debug": [Function],
- "findAllByAltText": [Function],
- "findAllByDisplayValue": [Function],
- "findAllByLabelText": [Function],
- "findAllByPlaceholderText": [Function],
- "findAllByRole": [Function],
- "findAllByTestId": [Function],
- "findAllByText": [Function],
- "findAllByTitle": [Function],
- "findByAltText": [Function],
- "findByDisplayValue": [Function],
- "findByLabelText": [Function],
- "findByPlaceholderText": [Function],
- "findByRole": [Function],
- "findByTestId": [Function],
- "findByText": [Function],
- "findByTitle": [Function],
- "getAllByAltText": [Function],
- "getAllByDisplayValue": [Function],
- "getAllByLabelText": [Function],
- "getAllByPlaceholderText": [Function],
- "getAllByRole": [Function],
- "getAllByTestId": [Function],
- "getAllByText": [Function],
- "getAllByTitle": [Function],
- "getByAltText": [Function],
- "getByDisplayValue": [Function],
- "getByLabelText": [Function],
- "getByPlaceholderText": [Function],
- "getByRole": [Function],
- "getByTestId": [Function],
- "getByText": [Function],
- "getByTitle": [Function],
- "queryAllByAltText": [Function],
- "queryAllByDisplayValue": [Function],
- "queryAllByLabelText": [Function],
- "queryAllByPlaceholderText": [Function],
- "queryAllByRole": [Function],
- "queryAllByTestId": [Function],
- "queryAllByText": [Function],
- "queryAllByTitle": [Function],
- "queryByAltText": [Function],
- "queryByDisplayValue": [Function],
- "queryByLabelText": [Function],
- "queryByPlaceholderText": [Function],
- "queryByRole": [Function],
- "queryByTestId": [Function],
- "queryByText": [Function],
- "queryByTitle": [Function],
- "replaceStoreState": [Function],
- "rerender": [Function],
- "unmount": [Function],
- "updateStoreState": [Function],
-}
-`;
-
-exports[`components/dot_menu/DotMenu should match snapshot, canDelete 1`] = `
-Object {
- "asFragment": [Function],
- "baseElement":
-
-
-
- ,
- "container":
-
-
,
- "debug": [Function],
- "findAllByAltText": [Function],
- "findAllByDisplayValue": [Function],
- "findAllByLabelText": [Function],
- "findAllByPlaceholderText": [Function],
- "findAllByRole": [Function],
- "findAllByTestId": [Function],
- "findAllByText": [Function],
- "findAllByTitle": [Function],
- "findByAltText": [Function],
- "findByDisplayValue": [Function],
- "findByLabelText": [Function],
- "findByPlaceholderText": [Function],
- "findByRole": [Function],
- "findByTestId": [Function],
- "findByText": [Function],
- "findByTitle": [Function],
- "getAllByAltText": [Function],
- "getAllByDisplayValue": [Function],
- "getAllByLabelText": [Function],
- "getAllByPlaceholderText": [Function],
- "getAllByRole": [Function],
- "getAllByTestId": [Function],
- "getAllByText": [Function],
- "getAllByTitle": [Function],
- "getByAltText": [Function],
- "getByDisplayValue": [Function],
- "getByLabelText": [Function],
- "getByPlaceholderText": [Function],
- "getByRole": [Function],
- "getByTestId": [Function],
- "getByText": [Function],
- "getByTitle": [Function],
- "queryAllByAltText": [Function],
- "queryAllByDisplayValue": [Function],
- "queryAllByLabelText": [Function],
- "queryAllByPlaceholderText": [Function],
- "queryAllByRole": [Function],
- "queryAllByTestId": [Function],
- "queryAllByText": [Function],
- "queryAllByTitle": [Function],
- "queryByAltText": [Function],
- "queryByDisplayValue": [Function],
- "queryByLabelText": [Function],
- "queryByPlaceholderText": [Function],
- "queryByRole": [Function],
- "queryByTestId": [Function],
- "queryByText": [Function],
- "queryByTitle": [Function],
- "replaceStoreState": [Function],
- "rerender": [Function],
- "unmount": [Function],
- "updateStoreState": [Function],
-}
-`;
-
-exports[`components/dot_menu/DotMenu should match snapshot, cannot move 1`] = `
-Object {
- "asFragment": [Function],
- "baseElement":
-
-
-
- ,
- "container":
-
-
,
- "debug": [Function],
- "findAllByAltText": [Function],
- "findAllByDisplayValue": [Function],
- "findAllByLabelText": [Function],
- "findAllByPlaceholderText": [Function],
- "findAllByRole": [Function],
- "findAllByTestId": [Function],
- "findAllByText": [Function],
- "findAllByTitle": [Function],
- "findByAltText": [Function],
- "findByDisplayValue": [Function],
- "findByLabelText": [Function],
- "findByPlaceholderText": [Function],
- "findByRole": [Function],
- "findByTestId": [Function],
- "findByText": [Function],
- "findByTitle": [Function],
- "getAllByAltText": [Function],
- "getAllByDisplayValue": [Function],
- "getAllByLabelText": [Function],
- "getAllByPlaceholderText": [Function],
- "getAllByRole": [Function],
- "getAllByTestId": [Function],
- "getAllByText": [Function],
- "getAllByTitle": [Function],
- "getByAltText": [Function],
- "getByDisplayValue": [Function],
- "getByLabelText": [Function],
- "getByPlaceholderText": [Function],
- "getByRole": [Function],
- "getByTestId": [Function],
- "getByText": [Function],
- "getByTitle": [Function],
- "queryAllByAltText": [Function],
- "queryAllByDisplayValue": [Function],
- "queryAllByLabelText": [Function],
- "queryAllByPlaceholderText": [Function],
- "queryAllByRole": [Function],
- "queryAllByTestId": [Function],
- "queryAllByText": [Function],
- "queryAllByTitle": [Function],
- "queryByAltText": [Function],
- "queryByDisplayValue": [Function],
- "queryByLabelText": [Function],
- "queryByPlaceholderText": [Function],
- "queryByRole": [Function],
- "queryByTestId": [Function],
- "queryByText": [Function],
- "queryByTitle": [Function],
- "replaceStoreState": [Function],
- "rerender": [Function],
- "unmount": [Function],
- "updateStoreState": [Function],
-}
-`;
-
-exports[`components/dot_menu/DotMenu should match snapshot, on Center 1`] = `
-,
- "class": "post-menu__item",
- "dataTestId": "PostDotMenu-Button-post_id_1",
- "id": "CENTER_button_post_id_1",
- }
- }
- menuButtonTooltip={
- Object {
- "class": "hidden-xs",
- "text": "More",
- }
- }
->
-
- }
- leadingElement={
-
- }
- onClick={[Function]}
- trailingElements={
-
- }
- />
-