diff --git a/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.tsx b/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.tsx index ae8f3d44ea..9c0e70b751 100644 --- a/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.tsx +++ b/webapp/channels/src/components/channel_notifications_modal/channel_notifications_modal.tsx @@ -16,7 +16,7 @@ import CheckboxWithSelectSettingItem from 'components/widgets/modals/components/ import ModalHeader from 'components/widgets/modals/components/modal_header'; import ModalSection from 'components/widgets/modals/components/modal_section'; import RadioSettingItem from 'components/widgets/modals/components/radio_setting_item'; -import type {Option} from 'components/widgets/modals/components/react_select_item'; +import type {SelectOption} from 'components/widgets/modals/components/react_select_item'; import {focusElement} from 'utils/a11y_utils'; import {NotificationLevels, DesktopSound, IgnoreChannelMentions} from 'utils/constants'; @@ -162,11 +162,11 @@ export default function ChannelNotificationsModal(props: Props) { ); - const handleChangeForMessageNotificationSoundSelect = (selectedOption: OnChangeValue) => { + const handleChangeForMessageNotificationSoundSelect = (selectedOption: OnChangeValue) => { stopTryNotificationRing(); if (selectedOption && 'value' in selectedOption) { - handleChange({desktop_notification_sound: ((selectedOption as Option).value)}); + handleChange({desktop_notification_sound: ((selectedOption as SelectOption).value)}); tryNotificationSound(selectedOption.value); } }; diff --git a/webapp/channels/src/components/user_settings/notifications/desktop_and_mobile_notification_setting/index.test.tsx b/webapp/channels/src/components/user_settings/notifications/desktop_and_mobile_notification_setting/index.test.tsx index 88aff7cc7c..76f22e4a1a 100644 --- a/webapp/channels/src/components/user_settings/notifications/desktop_and_mobile_notification_setting/index.test.tsx +++ b/webapp/channels/src/components/user_settings/notifications/desktop_and_mobile_notification_setting/index.test.tsx @@ -3,10 +3,12 @@ import React from 'react'; +import type {SelectOption} from 'components/widgets/modals/components/react_select_item'; + import {renderWithContext, screen} from 'tests/react_testing_utils'; import Constants, {NotificationLevels} from 'utils/constants'; -import type {SelectOption, Props} from './index'; +import type {Props} from './index'; import DesktopNotificationSettings, { shouldShowDesktopThreadsSection, shouldShowMobileThreadsSection, @@ -177,6 +179,88 @@ describe('DesktopNotificationSettings', () => { expect(screen.queryByText('Trigger mobile notifications when I am:')).toBeNull(); }); + + // New accessibility tests for the option labels fix + describe('Accessibility - Option Labels', () => { + test('should render notification option labels as proper text for screen readers', () => { + renderWithContext( + , + ); + + // Check that the radio button labels are properly rendered as text + expect(screen.getByText('All new messages')).toBeInTheDocument(); + expect(screen.getByText('Mentions, direct messages, and group messages')).toBeInTheDocument(); + expect(screen.getByText('Nothing')).toBeInTheDocument(); + }); + + test('should render mobile notification option labels correctly in select dropdown', () => { + const props = {...baseProps, desktopAndMobileSettingsDifferent: true}; + renderWithContext( + , + ); + + // The select component should be present - use more specific selector since there are multiple comboboxes + const selectElement = screen.getByLabelText(/Send mobile notifications for:/); + expect(selectElement).toBeInTheDocument(); + + // The aria-labelledby should reference the correct label + expect(selectElement).toHaveAttribute('aria-labelledby', 'sendMobileNotificationsLabel'); + }); + + test('should render mobile notification status options correctly', () => { + const props = {...baseProps, desktopActivity: NotificationLevels.MENTION, desktopAndMobileSettingsDifferent: false}; + renderWithContext( + , + ); + + // The trigger mobile notifications select should be present + const selectElement = screen.getByLabelText(/Trigger mobile notifications when I am:/); + expect(selectElement).toBeInTheDocument(); + }); + + test('should ensure option labels are MessageDescriptor or string, not React elements', () => { + // This test ensures we don't regress to the [object,object] issue + const result = getValueOfSendMobileNotificationForSelect(NotificationLevels.ALL); + if (result && typeof result === 'object' && 'label' in result) { + // The label should be a MessageDescriptor (object) or string, not a React element + if (typeof result.label === 'object') { + // If it's an object, it should be a MessageDescriptor with id and defaultMessage + expect(result.label).toHaveProperty('id'); + expect(result.label).toHaveProperty('defaultMessage'); + + // Should NOT have React element properties + expect(result.label).not.toHaveProperty('$$typeof'); + expect(result.label).not.toHaveProperty('type'); + expect(result.label).not.toHaveProperty('props'); + } else { + // If it's not an object, it should be a string + expect(typeof result.label).toBe('string'); + } + expect(result.label).not.toBeNull(); + } + }); + + test('should ensure mobile notification status option labels are properly formatted', () => { + const result = getValueOfSendMobileNotificationWhenSelect(Constants.UserStatuses.ONLINE); + if (result && typeof result === 'object' && 'label' in result) { + // The label should be a MessageDescriptor (object) or string, not a React element + if (typeof result.label === 'object') { + // If it's an object, it should be a MessageDescriptor with id and defaultMessage + expect(result.label).toHaveProperty('id'); + expect(result.label).toHaveProperty('defaultMessage'); + + // Should NOT have React element properties that would cause [object,object] + expect(result.label).not.toHaveProperty('$$typeof'); + expect(result.label).not.toHaveProperty('type'); + expect(result.label).not.toHaveProperty('props'); + } else { + // If it's not an object, it should be a string + expect(typeof result.label).toBe('string'); + } + expect(result.label).not.toBeNull(); + } + }); + }); }); describe('shouldShowDesktopThreadsSection', () => { diff --git a/webapp/channels/src/components/user_settings/notifications/desktop_and_mobile_notification_setting/index.tsx b/webapp/channels/src/components/user_settings/notifications/desktop_and_mobile_notification_setting/index.tsx index 8f4fed2511..0eab936bae 100644 --- a/webapp/channels/src/components/user_settings/notifications/desktop_and_mobile_notification_setting/index.tsx +++ b/webapp/channels/src/components/user_settings/notifications/desktop_and_mobile_notification_setting/index.tsx @@ -3,7 +3,7 @@ import React, {Fragment, useCallback, useEffect, useMemo, useRef, memo} from 'react'; import type {ChangeEvent, ReactNode} from 'react'; -import {FormattedMessage} from 'react-intl'; +import {FormattedMessage, useIntl, defineMessage} from 'react-intl'; import ReactSelect from 'react-select'; import type {OnChangeValue, Options} from 'react-select'; @@ -14,16 +14,13 @@ import SettingItemMin from 'components/setting_item_min'; import type SettingItemMinComponent from 'components/setting_item_min'; import NotificationPermissionSectionNotice from 'components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_section_notice'; import NotificationPermissionTitleTag from 'components/user_settings/notifications/desktop_and_mobile_notification_setting/notification_permission_title_tag'; +import {getOptionLabel, type SelectOption} from 'components/widgets/modals/components/react_select_item'; import Constants, {NotificationLevels, UserSettingsNotificationSections} from 'utils/constants'; +import {formatAsComponent} from 'utils/i18n'; import type {Props as UserSettingsNotificationsProps} from '../user_settings_notifications'; -export type SelectOption = { - label: ReactNode; - value: string; -}; - export type Props = { active: boolean; updateSection: (section: string) => void; @@ -63,6 +60,7 @@ function DesktopAndMobileNotificationSettings({ }: Props) { const editButtonRef = useRef(null); const previousActiveRef = useRef(active); + const intl = useIntl(); // Focus back on the edit button, after this section was closed after it was opened useEffect(() => { @@ -131,7 +129,7 @@ function DesktopAndMobileNotificationSettings({ value={optionOfSendNotifications.value} onChange={handleChangeForSendDesktopNotificationsRadio} /> - {optionOfSendNotifications.label} + {formatAsComponent(optionOfSendNotifications.label)} ))} @@ -208,6 +206,8 @@ function DesktopAndMobileNotificationSettings({ components={{IndicatorSeparator: NoIndicatorSeparatorComponent}} value={getValueOfSendMobileNotificationForSelect(pushActivity)} onChange={handleChangeForSendMobileNotificationsSelect} + getOptionLabel={(option) => getOptionLabel(option, intl)} + /> ); @@ -261,6 +261,7 @@ function DesktopAndMobileNotificationSettings({ components={{IndicatorSeparator: NoIndicatorSeparatorComponent}} value={getValueOfSendMobileNotificationWhenSelect(pushStatus)} onChange={handleChangeForTriggerMobileNotificationsSelect} + getOptionLabel={(option) => getOptionLabel(option, intl)} /> ); @@ -352,30 +353,24 @@ function NoIndicatorSeparatorComponent() { const optionsOfSendNotifications = [ { - label: ( - - ), + label: defineMessage({ + id: 'user.settings.notifications.desktopAndMobile.allNewMessages', + defaultMessage: 'All new messages', + }), value: NotificationLevels.ALL, }, { - label: ( - - ), + label: defineMessage({ + id: 'user.settings.notifications.desktopAndMobile.onlyMentions', + defaultMessage: 'Mentions, direct messages, and group messages', + }), value: NotificationLevels.MENTION, }, { - label: ( - - ), + label: defineMessage({ + id: 'user.settings.notifications.desktopAndMobile.nothing', + defaultMessage: 'Nothing', + }), value: NotificationLevels.NONE, }, ]; @@ -462,30 +457,24 @@ export function shouldShowTriggerMobileNotificationsSection(sendPushNotification const optionsOfSendMobileNotificationsWhenSelect: Options = [ { - label: ( - - ), + label: defineMessage({ + id: 'user.settings.notifications.desktopAndMobile.online', + defaultMessage: 'Online, away, or offline', + }), value: Constants.UserStatuses.ONLINE, }, { - label: ( - - ), + label: defineMessage({ + id: 'user.settings.notifications.desktopAndMobile.away', + defaultMessage: 'Away or offline', + }), value: Constants.UserStatuses.AWAY, }, { - label: ( - - ), + label: defineMessage({ + id: 'user.settings.notifications.desktopAndMobile.offline', + defaultMessage: 'Offline', + }), value: Constants.UserStatuses.OFFLINE, }, ]; diff --git a/webapp/channels/src/components/user_settings/notifications/desktop_notification_sounds_setting/index.tsx b/webapp/channels/src/components/user_settings/notifications/desktop_notification_sounds_setting/index.tsx index 8d22acd968..a2a1076c13 100644 --- a/webapp/channels/src/components/user_settings/notifications/desktop_notification_sounds_setting/index.tsx +++ b/webapp/channels/src/components/user_settings/notifications/desktop_notification_sounds_setting/index.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import type {ChangeEvent, ReactNode} from 'react'; +import type {ChangeEvent} from 'react'; import React, {memo, useEffect, useRef, Fragment, useMemo, useCallback} from 'react'; import {FormattedMessage, useIntl} from 'react-intl'; import type {OnChangeValue} from 'react-select'; @@ -12,6 +12,7 @@ import type {UserNotifyProps} from '@mattermost/types/users'; import SettingItemMax from 'components/setting_item_max'; import SettingItemMin from 'components/setting_item_min'; import type SettingItemMinComponent from 'components/setting_item_min'; +import {getOptionLabel, type SelectOption} from 'components/widgets/modals/components/react_select_item'; import {UserSettingsNotificationSections} from 'utils/constants'; import { @@ -28,11 +29,6 @@ import { import type {Props as UserSettingsNotificationsProps} from '../user_settings_notifications'; -export type SelectOption = { - value: string; - label: ReactNode; -}; - export type Props = { active: boolean; updateSection: (section: string) => void; @@ -151,6 +147,8 @@ function DesktopNotificationSoundsSettings({ value={getValueOfNotificationSoundsSelect(desktopNotificationSound)} onChange={handleChangeForMessageNotificationSoundSelect} aria-labelledby='messageNotificationSoundLabel' + getOptionLabel={(option) => getOptionLabel(option, intl)} + /> @@ -190,6 +188,8 @@ function DesktopNotificationSoundsSettings({ })} value={getValueOfIncomingCallSoundsSelect(callsNotificationSound)} onChange={handleChangeForIncomingCallSoundSelect} + getOptionLabel={(option) => getOptionLabel(option, intl)} + /> diff --git a/webapp/channels/src/components/widgets/modals/components/checkbox_with_select_item.tsx b/webapp/channels/src/components/widgets/modals/components/checkbox_with_select_item.tsx index 5e0d3efccb..5414f84f80 100644 --- a/webapp/channels/src/components/widgets/modals/components/checkbox_with_select_item.tsx +++ b/webapp/channels/src/components/widgets/modals/components/checkbox_with_select_item.tsx @@ -3,13 +3,14 @@ import type {ReactNode} from 'react'; import React from 'react'; +import {useIntl} from 'react-intl'; import ReactSelect from 'react-select'; import type {OnChangeValue} from 'react-select'; import type {BaseSettingItemProps} from './base_setting_item'; import BaseSettingItem from './base_setting_item'; import type {FieldsetCheckbox} from './checkbox_setting_item'; -import type {FieldsetReactSelect, Option} from './react_select_item'; +import {getOptionLabel, type FieldsetReactSelect, type SelectOption} from './react_select_item'; type Props = BaseSettingItemProps & { containerClassName?: string; @@ -19,8 +20,8 @@ type Props = BaseSettingItemProps & { checkboxFieldValue: boolean; handleCheckboxChange: (e: boolean) => void; selectFieldData: FieldsetReactSelect; - selectFieldValue?: Option; - handleSelectChange: (selected: OnChangeValue) => void; + selectFieldValue?: SelectOption; + handleSelectChange: (selected: OnChangeValue) => void; isSelectDisabled?: boolean; selectPlaceholder?: string; } @@ -40,6 +41,8 @@ export default function CheckboxWithSelectSettingItem({ isSelectDisabled, selectPlaceholder, }: Props) { + const intl = useIntl(); + const content = ( <>
handleSelectChange(value)} value={selectFieldValue} components={{IndicatorSeparator: NoIndicatorSeparatorComponent}} + getOptionLabel={(option) => getOptionLabel(option, intl)} />
@@ -93,8 +97,3 @@ export default function CheckboxWithSelectSettingItem({ function NoIndicatorSeparatorComponent() { return null; } - -export type SelectOption = { - value: string; - label: ReactNode; -}; diff --git a/webapp/channels/src/components/widgets/modals/components/react_select_item.test.tsx b/webapp/channels/src/components/widgets/modals/components/react_select_item.test.tsx new file mode 100644 index 0000000000..408390a6f3 --- /dev/null +++ b/webapp/channels/src/components/widgets/modals/components/react_select_item.test.tsx @@ -0,0 +1,86 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {defineMessage} from 'react-intl'; + +import {getOptionLabel, type SelectOption} from './react_select_item'; + +// Mock intl object for testing +const mockIntl = { + formatMessage: jest.fn((descriptor) => { + if (typeof descriptor === 'object' && descriptor.defaultMessage) { + return descriptor.defaultMessage; + } + return String(descriptor); + }), +}; + +describe('getOptionLabel', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should return string label as-is', () => { + const option: SelectOption = { + value: 'test', + label: 'Test Label', + }; + + const result = getOptionLabel(option, mockIntl as any); + expect(result).toBe('Test Label'); + + // formatMessage should NOT be called for string labels - they're returned directly + expect(mockIntl.formatMessage).not.toHaveBeenCalled(); + }); + + test('should handle MessageDescriptor label correctly', () => { + const messageDescriptor = defineMessage({ + id: 'test1', + defaultMessage: 'Help Text', + }); + + const option: SelectOption = { + value: 'test', + label: messageDescriptor, + }; + + const result = getOptionLabel(option, mockIntl as any); + expect(result).toBe('Help Text'); + expect(mockIntl.formatMessage).toHaveBeenCalledWith(messageDescriptor); + }); + + test('should return empty string for undefined label', () => { + const option: SelectOption = { + value: 'test', + label: undefined as any, + }; + + const result = getOptionLabel(option, mockIntl as any); + expect(result).toBe(''); + + // formatMessage should NOT be called for undefined - formatAsString returns undefined, then we || '' + expect(mockIntl.formatMessage).not.toHaveBeenCalled(); + }); + + test('should handle accessibility scenarios - no more [object,object]', () => { + // This test ensures we don't get "[object,object]" for screen readers + const messageDescriptor = defineMessage({ + id: 'test1', + defaultMessage: 'Help Text', + }); + + const option: SelectOption = { + value: 'all', + label: messageDescriptor, + }; + + const result = getOptionLabel(option, mockIntl as any); + + // Should return proper text, not "[object,object]" + expect(result).toBe('Help Text'); + expect(result).not.toBe('[object,object]'); + expect(result).not.toBe('[object Object]'); + expect(typeof result).toBe('string'); + expect(mockIntl.formatMessage).toHaveBeenCalledWith(messageDescriptor); + }); +}); diff --git a/webapp/channels/src/components/widgets/modals/components/react_select_item.tsx b/webapp/channels/src/components/widgets/modals/components/react_select_item.tsx index 30cf712f74..64809a7bbb 100644 --- a/webapp/channels/src/components/widgets/modals/components/react_select_item.tsx +++ b/webapp/channels/src/components/widgets/modals/components/react_select_item.tsx @@ -1,17 +1,20 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import type {ReactNode} from 'react'; import React from 'react'; +import {useIntl} from 'react-intl'; +import type {MessageDescriptor} from 'react-intl'; import type {OnChangeValue} from 'react-select'; import ReactSelect from 'react-select'; +import {formatAsString} from 'utils/i18n'; + import type {BaseSettingItemProps} from './base_setting_item'; import BaseSettingItem from './base_setting_item'; -export type Option = { +export type SelectOption = { value: string; - label: ReactNode; + label: string | MessageDescriptor; }; export type FieldsetReactSelect = { @@ -21,15 +24,20 @@ export type FieldsetReactSelect = { dataTestId?: string; ariaLabelledby?: string; clearable?: boolean; - options: Option[]; + options: SelectOption[]; } type Props = BaseSettingItemProps & { inputFieldData: FieldsetReactSelect; - inputFieldValue: Option; - handleChange: (selected: OnChangeValue) => void; + inputFieldValue: SelectOption; + handleChange: (selected: OnChangeValue) => void; } +// Function to extract text from MessageDescriptor or return string as-is +export const getOptionLabel = (option: SelectOption, intl: ReturnType): string => { + return formatAsString(intl.formatMessage, option.label) || ''; +}; + function ReactSelectItemCreator({ title, description, @@ -37,6 +45,7 @@ function ReactSelectItemCreator({ inputFieldValue, handleChange, }: Props): JSX.Element { + const intl = useIntl(); const content = (
getOptionLabel(option, intl)} + />
); diff --git a/webapp/channels/src/utils/notification_sounds.test.tsx b/webapp/channels/src/utils/notification_sounds.test.tsx new file mode 100644 index 0000000000..abec9ef8aa --- /dev/null +++ b/webapp/channels/src/utils/notification_sounds.test.tsx @@ -0,0 +1,156 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {SelectOption} from 'components/widgets/modals/components/react_select_item'; + +import { + optionsOfMessageNotificationSoundsSelect, + optionsOfIncomingCallSoundsSelect, + getValueOfNotificationSoundsSelect, + getValueOfIncomingCallSoundsSelect, + DesktopNotificationSounds, +} from './notification_sounds'; + +describe('notification sounds accessibility', () => { + describe('optionsOfMessageNotificationSoundsSelect', () => { + test('should have proper structure for all sound options', () => { + expect(optionsOfMessageNotificationSoundsSelect).toHaveLength(6); + + optionsOfMessageNotificationSoundsSelect.forEach((option: SelectOption) => { + expect(option).toHaveProperty('value'); + expect(option).toHaveProperty('label'); + expect(typeof option.value).toBe('string'); + + // Label should be MessageDescriptor or string, not React element + if (typeof option.label === 'object') { + expect(option.label).toHaveProperty('id'); + expect(option.label).toHaveProperty('defaultMessage'); + expect(typeof option.label.id).toBe('string'); + expect(typeof option.label.defaultMessage).toBe('string'); + } else { + expect(typeof option.label).toBe('string'); + } + }); + }); + + test('should have correct MessageDescriptor for each sound type', () => { + const bingOption = optionsOfMessageNotificationSoundsSelect.find((o) => o.value === DesktopNotificationSounds.BING); + expect(bingOption).toBeDefined(); + if (bingOption && typeof bingOption.label === 'object') { + expect(bingOption.label.id).toBe('user.settings.notifications.desktopNotificationSound.soundBing'); + expect(bingOption.label.defaultMessage).toBe('Bing'); + } + + const crackleOption = optionsOfMessageNotificationSoundsSelect.find((o) => o.value === DesktopNotificationSounds.CRACKLE); + expect(crackleOption).toBeDefined(); + if (crackleOption && typeof crackleOption.label === 'object') { + expect(crackleOption.label.id).toBe('user.settings.notifications.desktopNotificationSound.soundCrackle'); + expect(crackleOption.label.defaultMessage).toBe('Crackle'); + } + }); + + test('should not contain React elements that would cause [object,object]', () => { + optionsOfMessageNotificationSoundsSelect.forEach((option: SelectOption) => { + // Ensure label is not a React element + expect(option.label).not.toHaveProperty('$$typeof'); + expect(option.label).not.toHaveProperty('type'); + expect(option.label).not.toHaveProperty('props'); + }); + }); + }); + + describe('optionsOfIncomingCallSoundsSelect', () => { + test('should have proper structure for all call sound options', () => { + expect(optionsOfIncomingCallSoundsSelect).toHaveLength(4); + + optionsOfIncomingCallSoundsSelect.forEach((option: SelectOption) => { + expect(option).toHaveProperty('value'); + expect(option).toHaveProperty('label'); + expect(typeof option.value).toBe('string'); + + // Label should be MessageDescriptor or string, not React element + if (typeof option.label === 'object') { + expect(option.label).toHaveProperty('id'); + expect(option.label).toHaveProperty('defaultMessage'); + expect(typeof option.label.id).toBe('string'); + expect(typeof option.label.defaultMessage).toBe('string'); + } else { + expect(typeof option.label).toBe('string'); + } + }); + }); + + test('should have correct MessageDescriptor for each call sound type', () => { + const dynamicOption = optionsOfIncomingCallSoundsSelect.find((o) => o.value === 'Dynamic'); + expect(dynamicOption).toBeDefined(); + if (dynamicOption && typeof dynamicOption.label === 'object') { + expect(dynamicOption.label.id).toBe('user.settings.notifications.desktopNotificationSound.soundDynamic'); + expect(dynamicOption.label.defaultMessage).toBe('Dynamic'); + } + + const calmOption = optionsOfIncomingCallSoundsSelect.find((o) => o.value === 'Calm'); + expect(calmOption).toBeDefined(); + if (calmOption && typeof calmOption.label === 'object') { + expect(calmOption.label.id).toBe('user.settings.notifications.desktopNotificationSound.soundCalm'); + expect(calmOption.label.defaultMessage).toBe('Calm'); + } + }); + + test('should not contain React elements that would cause [object,object]', () => { + optionsOfIncomingCallSoundsSelect.forEach((option: SelectOption) => { + // Ensure label is not a React element + expect(option.label).not.toHaveProperty('$$typeof'); + expect(option.label).not.toHaveProperty('type'); + expect(option.label).not.toHaveProperty('props'); + }); + }); + }); + + describe('getValueOfNotificationSoundsSelect', () => { + test('should return valid option for known sound names', () => { + const result = getValueOfNotificationSoundsSelect(DesktopNotificationSounds.BING); + expect(result).toBeDefined(); + expect(result?.value).toBe(DesktopNotificationSounds.BING); + + // Ensure the returned option has proper label structure + if (result && typeof result.label === 'object') { + expect(result.label).toHaveProperty('id'); + expect(result.label).toHaveProperty('defaultMessage'); + } + }); + + test('should return undefined for unknown sound names', () => { + const result = getValueOfNotificationSoundsSelect('UnknownSound'); + expect(result).toBeUndefined(); + }); + + test('should return undefined for undefined input', () => { + const result = getValueOfNotificationSoundsSelect(undefined); + expect(result).toBeUndefined(); + }); + }); + + describe('getValueOfIncomingCallSoundsSelect', () => { + test('should return valid option for known call sound names', () => { + const result = getValueOfIncomingCallSoundsSelect('Dynamic'); + expect(result).toBeDefined(); + expect(result?.value).toBe('Dynamic'); + + // Ensure the returned option has proper label structure + if (result && typeof result.label === 'object') { + expect(result.label).toHaveProperty('id'); + expect(result.label).toHaveProperty('defaultMessage'); + } + }); + + test('should return undefined for unknown call sound names', () => { + const result = getValueOfIncomingCallSoundsSelect('UnknownCallSound'); + expect(result).toBeUndefined(); + }); + + test('should return undefined for undefined input', () => { + const result = getValueOfIncomingCallSoundsSelect(undefined); + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/webapp/channels/src/utils/notification_sounds.tsx b/webapp/channels/src/utils/notification_sounds.tsx index 496b5672d8..66e424cf0d 100644 --- a/webapp/channels/src/utils/notification_sounds.tsx +++ b/webapp/channels/src/utils/notification_sounds.tsx @@ -1,13 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import type {ReactNode} from 'react'; -import React from 'react'; -import {FormattedMessage} from 'react-intl'; +import {defineMessage} from 'react-intl'; import type {ChannelNotifyProps} from '@mattermost/types/channels'; import type {UserNotifyProps} from '@mattermost/types/users'; +import type {SelectOption} from 'components/widgets/modals/components/react_select_item'; + import bing from 'sounds/bing.mp3'; import calls_calm from 'sounds/calls_calm.mp3'; import calls_cheerful from 'sounds/calls_cheerful.mp3'; @@ -42,66 +42,54 @@ export const notificationSounds = new Map([ export const notificationSoundKeys = Array.from(notificationSounds.keys()); -export const optionsOfMessageNotificationSoundsSelect: Array<{value: string; label: ReactNode}> = notificationSoundKeys.map((soundName) => { +export const optionsOfMessageNotificationSoundsSelect: SelectOption[] = notificationSoundKeys.map((soundName) => { if (soundName === DesktopNotificationSounds.BING) { return { value: soundName, - label: ( - - ), + label: defineMessage({ + id: 'user.settings.notifications.desktopNotificationSound.soundBing', + defaultMessage: 'Bing', + }), }; } else if (soundName === DesktopNotificationSounds.CRACKLE) { return { value: soundName, - label: ( - - ), + label: defineMessage({ + id: 'user.settings.notifications.desktopNotificationSound.soundCrackle', + defaultMessage: 'Crackle', + }), }; } else if (soundName === DesktopNotificationSounds.DOWN) { return { value: soundName, - label: ( - - ), + label: defineMessage({ + id: 'user.settings.notifications.desktopNotificationSound.soundDown', + defaultMessage: 'Down', + }), }; } else if (soundName === DesktopNotificationSounds.HELLO) { return { value: soundName, - label: ( - - ), + label: defineMessage({ + id: 'user.settings.notifications.desktopNotificationSound.soundHello', + defaultMessage: 'Hello', + }), }; } else if (soundName === DesktopNotificationSounds.RIPPLE) { return { value: soundName, - label: ( - - ), + label: defineMessage({ + id: 'user.settings.notifications.desktopNotificationSound.soundRipple', + defaultMessage: 'Ripple', + }), }; } else if (soundName === DesktopNotificationSounds.UPSTAIRS) { return { value: soundName, - label: ( - - ), + label: defineMessage({ + id: 'user.settings.notifications.desktopNotificationSound.soundUpstairs', + defaultMessage: 'Upstairs', + }), }; } return { @@ -129,46 +117,38 @@ export const callsNotificationSounds = new Map([ export const callNotificationSoundKeys = Array.from(callsNotificationSounds.keys()); -export const optionsOfIncomingCallSoundsSelect: Array<{value: string; label: ReactNode}> = callNotificationSoundKeys.map((soundName) => { +export const optionsOfIncomingCallSoundsSelect: SelectOption[] = callNotificationSoundKeys.map((soundName) => { if (soundName === 'Dynamic') { return { value: soundName, - label: ( - - ), + label: defineMessage({ + id: 'user.settings.notifications.desktopNotificationSound.soundDynamic', + defaultMessage: 'Dynamic', + }), }; } else if (soundName === 'Calm') { return { value: soundName, - label: ( - - ), + label: defineMessage({ + id: 'user.settings.notifications.desktopNotificationSound.soundCalm', + defaultMessage: 'Calm', + }), }; } else if (soundName === 'Urgent') { return { value: soundName, - label: ( - - ), + label: defineMessage({ + id: 'user.settings.notifications.desktopNotificationSound.soundUrgent', + defaultMessage: 'Urgent', + }), }; } else if (soundName === 'Cheerful') { return { value: soundName, - label: ( - - ), + label: defineMessage({ + id: 'user.settings.notifications.desktopNotificationSound.soundCheerful', + defaultMessage: 'Cheerful', + }), }; } return {