diff --git a/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.test.tsx b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.test.tsx new file mode 100644 index 0000000000..d534ad0194 --- /dev/null +++ b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.test.tsx @@ -0,0 +1,78 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {screen, waitFor} from '@testing-library/react'; +import React from 'react'; + +import {renderWithContext, userEvent} from 'tests/react_testing_utils'; +import {requestNotificationPermission, isNotificationAPISupported} from 'utils/notifications'; + +import NotificationPermissionBar from './index'; + +jest.mock('utils/notifications', () => ({ + requestNotificationPermission: jest.fn(), + isNotificationAPISupported: jest.fn(), +})); + +describe('NotificationPermissionBar', () => { + const initialState = { + entities: { + users: { + currentUserId: 'user-id', + }, + general: { + config: {}, + license: {}, + }, + }, + }; + + beforeEach(() => { + (isNotificationAPISupported as jest.Mock).mockReturnValue(true); + (window as any).Notification = {permission: 'default'}; + }); + + afterEach(() => { + jest.clearAllMocks(); + delete (window as any).Notification; + }); + + test('should render the notification bar when conditions are met', () => { + renderWithContext(, initialState); + + expect(screen.getByText('We need your permission to show desktop notifications.')).toBeInTheDocument(); + expect(screen.getByText('Enable notifications')).toBeInTheDocument(); + }); + + test('should not render the notification bar if user is not logged in', () => { + renderWithContext(); + + expect(screen.queryByText('We need your permission to show desktop notifications.')).not.toBeInTheDocument(); + expect(screen.queryByText('Enable notifications')).not.toBeInTheDocument(); + }); + + test('should not render the notification bar if Notifications are not supported', () => { + delete (window as any).Notification; + (isNotificationAPISupported as jest.Mock).mockReturnValue(false); + + renderWithContext(, initialState); + + expect(screen.queryByText('We need your permission to show desktop notifications.')).not.toBeInTheDocument(); + expect(screen.queryByText('Enable notifications')).not.toBeInTheDocument(); + }); + + test('should call requestNotificationPermission and hide the bar when the button is clicked', async () => { + (requestNotificationPermission as jest.Mock).mockResolvedValue('granted'); + + renderWithContext(, initialState); + + expect(screen.getByText('We need your permission to show desktop notifications.')).toBeInTheDocument(); + + await waitFor(async () => { + userEvent.click(screen.getByText('Enable notifications')); + }); + + expect(requestNotificationPermission).toHaveBeenCalled(); + expect(screen.queryByText('We need your permission to show desktop notifications.')).not.toBeInTheDocument(); + }); +}); diff --git a/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.tsx b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.tsx index 9823eaed80..e4e0c130fd 100644 --- a/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.tsx +++ b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.tsx @@ -10,25 +10,25 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import AnnouncementBar from 'components/announcement_bar/default_announcement_bar'; import {AnnouncementBarTypes} from 'utils/constants'; +import {requestNotificationPermission, isNotificationAPISupported} from 'utils/notifications'; export default function NotificationPermissionBar() { const isLoggedIn = Boolean(useSelector(getCurrentUserId)); - const [show, setShow] = useState(Notification.permission === 'default'); + const [show, setShow] = useState(isNotificationAPISupported() ? Notification.permission === 'default' : false); - const handleClose = useCallback(() => { - // If the user closes the bar, don't show the notification bar any more for the rest of the session, but - // show it again after refresh. + const handleClick = useCallback(async () => { + await requestNotificationPermission(); setShow(false); }, []); - const handleClick = useCallback(() => { - Notification.requestPermission().then(() => { - setShow(false); - }); + const handleClose = useCallback(() => { + // If the user closes the bar, don't show the notification bar any more for the rest of the session, but + // show it again on app refresh. + setShow(false); }, []); - if (!show || !isLoggedIn) { + if (!show || !isLoggedIn || !isNotificationAPISupported()) { return null; } @@ -39,7 +39,7 @@ export default function NotificationPermissionBar() { type={AnnouncementBarTypes.ANNOUNCEMENT} message={ } diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 1a819388f9..2ed2f5313e 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -2814,7 +2814,7 @@ "announcement_bar.error.trial_license_expiring_last_day.short": "This is the last day of your free trial.", "announcement_bar.notification.email_verified": "Email verified", "announcement_bar.notification.enable_notifications": "Enable notifications", - "announcement_bar.notification.needs_permisson": "We need your permission to show desktop notifications.", + "announcement_bar.notification.needs_permission": "We need your permission to show desktop notifications.", "announcement_bar.warn.contact_support_text": "To renew your license, contact support at support@mattermost.com.", "announcement_bar.warn.email_support": "[Contact support](!{email}).", "announcement_bar.warn.no_internet_connection": "Looks like you do not have access to the internet.", diff --git a/webapp/channels/src/utils/notifications.test.ts b/webapp/channels/src/utils/notifications.test.ts index 306232b095..7228db7729 100644 --- a/webapp/channels/src/utils/notifications.test.ts +++ b/webapp/channels/src/utils/notifications.test.ts @@ -3,11 +3,10 @@ /* eslint-disable global-require */ -// to enable being a typescript file -export const a = ''; import configureStore from 'tests/test_store'; import type {showNotification} from './notifications'; +import {isNotificationAPISupported, requestNotificationPermission} from './notifications'; declare global { interface Window { @@ -39,20 +38,20 @@ describe('Notifications.showNotification', () => { it('should throw an exception if Notification is not defined on window', async () => { delete window.Notification; - await expect(store.dispatch(Notifications.showNotification())).rejects.toThrow('Notification not supported'); + await expect(store.dispatch(Notifications.showNotification())).rejects.toThrow('Notification API is not supported'); }); it('should throw an exception if Notification.requestPermission is not defined', async () => { window.Notification = jest.fn(); - await expect(store.dispatch(Notifications.showNotification())).rejects.toThrow('Notification.requestPermission not supported'); + await expect(store.dispatch(Notifications.showNotification())).rejects.toThrow('Notification API is not supported'); }); it('should throw an exception if Notification.requestPermission is not a function', async () => { window.Notification = jest.fn(); window.Notification.requestPermission = true; - await expect(store.dispatch(Notifications.showNotification())).rejects.toThrow('Notification.requestPermission not supported'); + await expect(store.dispatch(Notifications.showNotification())).rejects.toThrow('Notification API is not supported'); expect(window.Notification).not.toHaveBeenCalled(); }); @@ -241,3 +240,64 @@ describe('Notifications.showNotification', () => { expect(window.Notification.requestPermission).toHaveBeenCalledTimes(0); }); }); + +describe('Notifications.isNotificationAPISupported', () => { + beforeEach(() => { + window.Notification = { + requestPermission: jest.fn(), + }; + }); + + afterEach(() => { + delete (window as any).Notification; + }); + + it('should return true if Notification is supported', () => { + expect(isNotificationAPISupported()).toBe(true); + }); + + it('should return false if Notification is not supported', () => { + delete (window as any).Notification; + + expect(isNotificationAPISupported()).toBe(false); + }); + + it('should return false if requestPermission is not a function', () => { + (window as any).Notification = {}; + + expect(isNotificationAPISupported()).toBe(false); + }); +}); + +describe('Notifications.requestNotificationPermission', () => { + beforeEach(() => { + (window as any).Notification = { + requestPermission: jest.fn(), + }; + }); + + afterEach(() => { + delete (window as any).Notification; + }); + + it('should return the permission if Notification.requestPermission resolves', async () => { + (window as any).Notification.requestPermission = jest.fn().mockResolvedValue('granted'); + + const permission = await requestNotificationPermission(); + expect(permission).toBe('granted'); + }); + + it('should return null if Notification is not supported', async () => { + delete (window as any).Notification; + + const permission = await requestNotificationPermission(); + expect(permission).toBeNull(); + }); + + it('should return null if requestPermission throws an error', async () => { + (window as any).Notification.requestPermission = jest.fn().mockRejectedValue('some error'); + + const permission = await requestNotificationPermission(); + expect(permission).toBeNull(); + }); +}); diff --git a/webapp/channels/src/utils/notifications.ts b/webapp/channels/src/utils/notifications.ts index b01f57a736..15095e1a96 100644 --- a/webapp/channels/src/utils/notifications.ts +++ b/webapp/channels/src/utils/notifications.ts @@ -50,12 +50,8 @@ export function showNotification( icon = iconWS; } - if (!('Notification' in window)) { - throw new Error('Notification not supported'); - } - - if (typeof Notification.requestPermission !== 'function') { - throw new Error('Notification.requestPermission not supported'); + if (!isNotificationAPISupported()) { + throw new Error('Notification API is not supported'); } if (Notification.permission !== 'granted') { @@ -104,3 +100,20 @@ export function showNotification( }; }; } + +export function isNotificationAPISupported() { + return ('Notification' in window) && (typeof Notification.requestPermission === 'function'); +} + +export async function requestNotificationPermission() { + if (!isNotificationAPISupported()) { + return null; + } + + try { + const notificationPermission = await Notification.requestPermission(); + return notificationPermission; + } catch (error) { + return null; + } +}