From 80e67ace860e6d811cc70125256cefaf574623ed Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Thu, 25 Apr 2024 17:24:03 -0400 Subject: [PATCH] Migrate tests for Login to Testing Library and convert showNotification to a thunk (#26848) * Migrate tests for Login to Testing Library * Convert showNotification to a thunk --- .../src/actions/notification_actions.jsx | 9 +- .../login/__snapshots__/login.test.tsx.snap | 118 ----- .../src/components/login/login.test.tsx | 445 ++++++++++-------- .../channels/src/components/login/login.tsx | 4 +- .../src/tests/react_testing_utils.tsx | 4 +- .../channels/src/utils/notifications.test.ts | 28 +- webapp/channels/src/utils/notifications.ts | 116 ++--- 7 files changed, 331 insertions(+), 393 deletions(-) delete mode 100644 webapp/channels/src/components/login/__snapshots__/login.test.tsx.snap diff --git a/webapp/channels/src/actions/notification_actions.jsx b/webapp/channels/src/actions/notification_actions.jsx index 7c2df05d0a..88eed240a2 100644 --- a/webapp/channels/src/actions/notification_actions.jsx +++ b/webapp/channels/src/actions/notification_actions.jsx @@ -48,7 +48,7 @@ const getNotificationSoundFromChannelMemberAndUser = (member, user) => { }; /** - * @returns {import('mattermost-redux/types/actions').ThunkActionFunc, GlobalState>} + * @returns {import('mattermost-redux/types/actions').ThunkActionFunc, GlobalState>} */ export function sendDesktopNotification(post, msgProps) { return async (dispatch, getState) => { @@ -325,6 +325,9 @@ export function sendDesktopNotification(post, msgProps) { }; } +/** + * @returns {import('mattermost-redux/types/actions').ThunkActionFunc, GlobalState>} + */ export const notifyMe = (title, body, channel, teamId, silent, soundName, url) => async (dispatch) => { // handle notifications in desktop app if (isDesktopApp()) { @@ -332,7 +335,7 @@ export const notifyMe = (title, body, channel, teamId, silent, soundName, url) = } try { - return await showNotification({ + return await dispatch(showNotification({ title, body, requireInteraction: false, @@ -341,7 +344,7 @@ export const notifyMe = (title, body, channel, teamId, silent, soundName, url) = window.focus(); getHistory().push(url); }, - }); + })); } catch (error) { dispatch(logError(error)); return {status: 'error', reason: 'notification_api', data: String(error)}; diff --git a/webapp/channels/src/components/login/__snapshots__/login.test.tsx.snap b/webapp/channels/src/components/login/__snapshots__/login.test.tsx.snap deleted file mode 100644 index 389a8affed..0000000000 --- a/webapp/channels/src/components/login/__snapshots__/login.test.tsx.snap +++ /dev/null @@ -1,118 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/login/Login should match snapshot 1`] = ` -
-
- -
-
-`; - -exports[`components/login/Login should match snapshot with base login 1`] = ` -
-
-
-

- Log in to your account -

-

- Collaborate with your team in real-time -

-
- -
-
-
- -
-
-

- Log in -

-
-
- - -
- - Forgot your password? - -
- -
-
-
-
-
-
-
-`; diff --git a/webapp/channels/src/components/login/login.test.tsx b/webapp/channels/src/components/login/login.test.tsx index 3808c12999..5ee156514a 100644 --- a/webapp/channels/src/components/login/login.test.tsx +++ b/webapp/channels/src/components/login/login.test.tsx @@ -1,307 +1,342 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {shallow} from 'enzyme'; +import {createMemoryHistory} from 'history'; import React from 'react'; -import {IntlProvider} from 'react-intl'; -import {MemoryRouter} from 'react-router-dom'; - -import type {ClientConfig} from '@mattermost/types/config'; import {RequestStatus} from 'mattermost-redux/constants'; import LocalStorageStore from 'stores/local_storage_store'; -import AlertBanner from 'components/alert_banner'; -import ExternalLoginButton from 'components/external_login_button/external_login_button'; import Login from 'components/login/login'; -import SaveButton from 'components/save_button'; -import Input from 'components/widgets/inputs/input/input'; -import PasswordInput from 'components/widgets/inputs/password_input/password_input'; -import {mountWithIntl} from 'tests/helpers/intl-test-helper'; +import mergeObjects from 'packages/mattermost-redux/test/merge_objects'; +import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils'; import Constants, {WindowSizes} from 'utils/constants'; import type {GlobalState} from 'types/store'; -let mockState: GlobalState; -let mockLocation = {pathname: '', search: '', hash: ''}; -const mockHistoryReplace = jest.fn(); -const mockHistoryPush = jest.fn(); -const mockLicense = {IsLicensed: 'false'}; -let mockConfig: Partial; - -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux') as typeof import('react-redux'), - useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState), - useDispatch: jest.fn(() => (action: unknown) => action), -})); - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom') as typeof import('react-router-dom'), - useLocation: () => mockLocation, - useHistory: () => ({ - replace: mockHistoryReplace, - push: mockHistoryPush, - }), -})); - -jest.mock('mattermost-redux/selectors/entities/general', () => ({ - ...jest.requireActual('mattermost-redux/selectors/entities/general') as typeof import('mattermost-redux/selectors/entities/general'), - getLicense: () => mockLicense, - getConfig: () => mockConfig, -})); +jest.unmock('react-router-dom'); describe('components/login/Login', () => { - beforeEach(() => { - mockLocation = {pathname: '', search: '', hash: ''}; - - LocalStorageStore.setWasLoggedIn(false); - - mockState = { - entities: { - general: { - config: {}, - license: {}, + const baseState = { + entities: { + general: { + config: { + EnableLdap: 'false', + EnableSaml: 'false', + EnableSignInWithEmail: 'false', + EnableSignInWithUsername: 'false', + EnableSignUpWithEmail: 'false', + EnableSignUpWithGitLab: 'false', + EnableSignUpWithOffice365: 'false', + EnableSignUpWithGoogle: 'false', + EnableSignUpWithOpenId: 'false', + EnableOpenServer: 'false', + LdapLoginFieldName: '', + GitLabButtonText: '', + GitLabButtonColor: '', + OpenIdButtonText: '', + OpenIdButtonColor: '', + SamlLoginButtonText: '', + EnableCustomBrand: 'false', + CustomBrandText: '', + CustomDescriptionText: '', + SiteName: 'Mattermost', + ExperimentalPrimaryTeam: '', + PasswordEnableForgotLink: 'true', }, - users: { - currentUserId: '', - profiles: { - user1: { - id: 'user1', - roles: '', - }, + license: { + IsLicensed: 'false', + }, + }, + users: { + currentUserId: '', + profiles: { + user1: { + id: 'user1', + roles: '', }, }, + }, + teams: { + currentTeamId: 'team1', teams: { - currentTeamId: 'team1', - teams: { - team1: { - id: 'team1', - name: 'team-1', - displayName: 'Team 1', - }, - }, - myMembers: { - team1: {roles: 'team_role'}, + team1: { + id: 'team1', + name: 'team-1', + displayName: 'Team 1', }, }, - }, - requests: { - users: { - logout: { - status: RequestStatus.NOT_STARTED, - }, + myMembers: { + team1: {roles: 'team_role'}, }, }, - storage: { - initialized: true, - }, - views: { - browser: { - windowSize: WindowSizes.DESKTOP_VIEW, + }, + requests: { + users: { + logout: { + status: RequestStatus.NOT_STARTED, }, }, - } as unknown as GlobalState; + }, + storage: { + initialized: true, + }, + views: { + browser: { + windowSize: WindowSizes.DESKTOP_VIEW, + }, + }, + } as unknown as GlobalState; - mockConfig = { - EnableLdap: 'false', - EnableSaml: 'false', - EnableSignInWithEmail: 'false', - EnableSignInWithUsername: 'false', - EnableSignUpWithEmail: 'false', - EnableSignUpWithGitLab: 'false', - EnableSignUpWithOffice365: 'false', - EnableSignUpWithGoogle: 'false', - EnableSignUpWithOpenId: 'false', - EnableOpenServer: 'false', - LdapLoginFieldName: '', - GitLabButtonText: '', - GitLabButtonColor: '', - OpenIdButtonText: '', - OpenIdButtonColor: '', - SamlLoginButtonText: '', - EnableCustomBrand: 'false', - CustomBrandText: '', - CustomDescriptionText: '', - SiteName: 'Mattermost', - ExperimentalPrimaryTeam: '', - PasswordEnableForgotLink: 'true', - }; + beforeEach(() => { + LocalStorageStore.setWasLoggedIn(false); }); it('should match snapshot', () => { - const wrapper = shallow( + renderWithContext( , + baseState, ); - expect(wrapper).toMatchSnapshot(); + expect(screen.queryByText('This server doesn’t have any sign-in methods enabled')).toBeVisible(); + expect(screen.queryByText('Log in to your account')).not.toBeInTheDocument(); }); it('should match snapshot with base login', () => { - mockConfig.EnableSignInWithEmail = 'true'; + const state = mergeObjects(baseState, { + entities: { + general: { + config: { + EnableSignInWithEmail: 'true', + }, + }, + }, + }); - const wrapper = shallow( + renderWithContext( , + state, ); - expect(wrapper).toMatchSnapshot(); + expect(screen.queryByText('This server doesn’t have any sign-in methods enabled')).not.toBeInTheDocument(); + expect(screen.queryByText('Log in to your account')).toBeVisible(); }); - it('should handle session expired', () => { + it('should handle session expired', async () => { LocalStorageStore.setWasLoggedIn(true); - mockConfig.EnableSignInWithEmail = 'true'; - const wrapper = mountWithIntl( - , + const state = mergeObjects(baseState, { + entities: { + general: { + config: { + EnableSignInWithEmail: 'true', + }, + }, + }, + }); + + renderWithContext( + , + state, ); - const alertBanner = wrapper.find(AlertBanner).first(); - expect(alertBanner.props().mode).toEqual('warning'); - expect(alertBanner.props().title).toEqual('Your session has expired. Please log in again.'); + expect(await screen.findByText('Your session has expired. Please log in again.')).toBeVisible(); - alertBanner.find('button').first().simulate('click'); + screen.getByLabelText('Close').click(); - expect(wrapper.find(AlertBanner)).toEqual({}); + expect(screen.queryByText('Your session has expired. Please log in again.')).not.toBeInTheDocument(); }); it('should handle initializing when logout status success', () => { - mockState.requests.users.logout.status = RequestStatus.SUCCESS; + const state = mergeObjects(baseState, { + requests: { + users: { + logout: { + status: RequestStatus.SUCCESS, + }, + }, + }, + }); - const intlProviderProps = { - defaultLocale: 'en', - locale: 'en', - messages: {}, - }; - - const wrapper = mountWithIntl( - - - - - , + renderWithContext( + , + state, ); - // eslint-disable-next-line react/jsx-key, react/jsx-no-literals - expect(wrapper.contains([

Loading

])).toEqual(true); + expect(screen.getByText('Loading')).toBeVisible(); }); it('should handle initializing when storage not initalized', () => { - mockState.storage.initialized = false; + const state = mergeObjects(baseState, { + storage: { + initialized: false, + }, + }); - const intlProviderProps = { - defaultLocale: 'en', - locale: 'en', - messages: {}, - }; - - const wrapper = mountWithIntl( - - - , + renderWithContext( + , + state, ); - // eslint-disable-next-line react/jsx-no-literals, react/jsx-key - expect(wrapper.contains([

Loading

])).toEqual(true); + expect(screen.getByText('Loading')).toBeVisible(); }); - it('should handle suppress session expired notification on sign in change', () => { - mockLocation.search = '?extra=' + Constants.SIGNIN_CHANGE; + it('should handle suppress session expired notification on sign in change', async () => { LocalStorageStore.setWasLoggedIn(true); - mockConfig.EnableSignInWithEmail = 'true'; - const wrapper = mountWithIntl( - - - , + const history = createMemoryHistory({ + initialEntries: [ + {search: '?extra=' + Constants.SIGNIN_CHANGE}, + ], + }); + + const state = mergeObjects(baseState, { + entities: { + general: { + config: { + EnableSignInWithEmail: 'true', + }, + }, + }, + }); + + renderWithContext( + , + state, + { + history, + }, ); expect(LocalStorageStore.getWasLoggedIn()).toEqual(false); - const alertBanner = wrapper.find(AlertBanner).first(); - expect(alertBanner.props().mode).toEqual('success'); - expect(alertBanner.props().title).toEqual('Sign-in method changed successfully'); + expect(await screen.findByText('Sign-in method changed successfully')).toBeVisible(); - alertBanner.find('button').first().simulate('click'); + screen.getByLabelText('Close').click(); - expect(wrapper.find(AlertBanner)).toEqual({}); + expect(screen.queryByText('Sign-in method changed successfully')).not.toBeInTheDocument(); }); - it('should handle discard session expiry notification on failed sign in', () => { + it('should handle discard session expiry notification on sign in attempt', async () => { LocalStorageStore.setWasLoggedIn(true); - mockConfig.EnableSignInWithEmail = 'true'; - const wrapper = mountWithIntl( - - - , + const state = mergeObjects(baseState, { + entities: { + general: { + config: { + EnableSignInWithEmail: 'true', + }, + }, + }, + }); + + renderWithContext( + , + state, ); - let alertBanner = wrapper.find(AlertBanner).first(); - expect(alertBanner.props().mode).toEqual('warning'); - expect(alertBanner.props().title).toEqual('Your session has expired. Please log in again.'); + expect(await screen.findByText('Your session has expired. Please log in again.')).toBeVisible(); - const input = wrapper.find(Input).first().find('input').first(); - input.simulate('change', {target: {value: 'user1'}}); + const emailInput = screen.getByLabelText('Email'); + userEvent.type(emailInput, 'user1'); - const passwordInput = wrapper.find(PasswordInput).first().find('input').first(); - passwordInput.simulate('change', {target: {value: 'passw'}}); + const passwordInput = screen.getByLabelText('Password'); + userEvent.type(passwordInput, 'passw'); - const saveButton = wrapper.find(SaveButton).first(); - expect(saveButton.props().disabled).toEqual(false); + screen.getByRole('button', {name: 'Log in'}).click(); - saveButton.find('button').first().simulate('click'); - - setTimeout(() => { - alertBanner = wrapper.find(AlertBanner).first(); - expect(alertBanner.props().mode).toEqual('danger'); - expect(alertBanner.props().title).toEqual('The email/username or password is invalid.'); - }); + expect(screen.queryByText('Your session has expired. Please log in again.')).not.toBeInTheDocument(); }); it('should handle gitlab text and color props', () => { - mockConfig.EnableSignInWithEmail = 'true'; - mockConfig.EnableSignUpWithGitLab = 'true'; - mockConfig.GitLabButtonText = 'GitLab 2'; - mockConfig.GitLabButtonColor = '#00ff00'; + const state = mergeObjects(baseState, { + entities: { + general: { + config: { + EnableSignInWithEmail: 'true', + EnableSignUpWithGitLab: 'true', + GitLabButtonText: 'GitLab 2', + GitLabButtonColor: '#00ff00', + }, + }, + }, + }); - const wrapper = shallow( + renderWithContext( , + state, ); - const externalLoginButton = wrapper.find(ExternalLoginButton).first(); - expect(externalLoginButton.props().url).toEqual('/oauth/gitlab/login'); - expect(externalLoginButton.props().label).toEqual('GitLab 2'); - expect(externalLoginButton.props().style).toEqual({color: '#00ff00', borderColor: '#00ff00'}); + const button = screen.getByRole('link', {name: 'Gitlab Icon GitLab 2'}); + + expect(button.style).toMatchObject({ + color: 'rgb(0, 255, 0)', + borderColor: '#00ff00', + }); }); it('should handle openid text and color props', () => { - mockConfig.EnableSignInWithEmail = 'true'; - mockConfig.EnableSignUpWithOpenId = 'true'; - mockConfig.OpenIdButtonText = 'OpenID 2'; - mockConfig.OpenIdButtonColor = '#00ff00'; + const state = mergeObjects(baseState, { + entities: { + general: { + config: { + EnableSignInWithEmail: 'true', + EnableSignUpWithOpenId: 'true', + OpenIdButtonText: 'OpenID 2', + OpenIdButtonColor: '#00ff00', + }, + }, + }, + }); - const wrapper = shallow( + renderWithContext( , + state, ); - const externalLoginButton = wrapper.find(ExternalLoginButton).first(); - expect(externalLoginButton.props().url).toEqual('/oauth/openid/login'); - expect(externalLoginButton.props().label).toEqual('OpenID 2'); - expect(externalLoginButton.props().style).toEqual({color: '#00ff00', borderColor: '#00ff00'}); + const button = screen.getByRole('link', {name: 'OpenID Icon OpenID 2'}); + + expect(button.style).toMatchObject({ + color: 'rgb(0, 255, 0)', + borderColor: '#00ff00', + }); }); - it('should redirect on login', () => { - mockState.entities.users.currentUserId = 'user1'; + it('should redirect on login', async () => { LocalStorageStore.setWasLoggedIn(true); - mockConfig.EnableSignInWithEmail = 'true'; + const redirectPath = '/boards/team/teamID/boardID'; - mockLocation.search = '?redirect_to=' + redirectPath; - mountWithIntl( - - - , + + const history = createMemoryHistory({ + initialEntries: [ + {search: '?redirect_to=' + redirectPath}, + ], + }); + history.push = jest.fn().mockImplementation(history.push); + + const state = mergeObjects(baseState, { + entities: { + general: { + config: { + EnableSignInWithEmail: 'true', + }, + }, + users: { + currentUserId: 'user1', + }, + }, + }); + + renderWithContext( + , + state, + { + history, + }, ); - expect(mockHistoryPush).toHaveBeenCalledWith(redirectPath); + + expect(history.push).toHaveBeenCalledWith(redirectPath); }); }); diff --git a/webapp/channels/src/components/login/login.tsx b/webapp/channels/src/components/login/login.tsx index a1fc08a814..0cec10ef00 100644 --- a/webapp/channels/src/components/login/login.tsx +++ b/webapp/channels/src/components/login/login.tsx @@ -252,7 +252,7 @@ const Login = ({onCustomizeHeader}: LoginProps) => { const showSessionExpiredNotificationIfNeeded = useCallback(() => { if (sessionExpired && !closeSessionExpiredNotification!.current) { - showNotification({ + dispatch(showNotification({ title: siteName, body: formatMessage({ id: 'login.session_expired.notification', @@ -267,7 +267,7 @@ const Login = ({onCustomizeHeader}: LoginProps) => { closeSessionExpiredNotification.current = undefined; } }, - }).then(({callback: closeNotification}) => { + })).then(({callback: closeNotification}) => { closeSessionExpiredNotification.current = closeNotification; }).catch(() => { // Ignore the failure to display the notification. diff --git a/webapp/channels/src/tests/react_testing_utils.tsx b/webapp/channels/src/tests/react_testing_utils.tsx index 938b8f328d..de0455a320 100644 --- a/webapp/channels/src/tests/react_testing_utils.tsx +++ b/webapp/channels/src/tests/react_testing_utils.tsx @@ -3,6 +3,7 @@ import {render} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import type {History} from 'history'; import {createBrowserHistory} from 'history'; import React from 'react'; import {IntlProvider} from 'react-intl'; @@ -28,6 +29,7 @@ export type FullContextOptions = { locale?: string; useMockedStore?: boolean; pluginReducers?: string[]; + history?: History; } export const renderWithContext = ( @@ -46,7 +48,7 @@ export const renderWithContext = ( // Store these in an object so that they can be maintained through rerenders const renderState = { component, - history: createBrowserHistory(), + history: partialOptions?.history ?? createBrowserHistory(), options, store: testStore, }; diff --git a/webapp/channels/src/utils/notifications.test.ts b/webapp/channels/src/utils/notifications.test.ts index 3667f76a37..e53980c12d 100644 --- a/webapp/channels/src/utils/notifications.test.ts +++ b/webapp/channels/src/utils/notifications.test.ts @@ -5,6 +5,8 @@ // to enable being a typescript file export const a = ''; +import configureStore from 'tests/test_store'; + import type {showNotification} from './notifications'; declare global { @@ -16,25 +18,29 @@ declare global { describe('Notifications.showNotification', () => { let Notifications: {showNotification: typeof showNotification}; + let store: ReturnType; + beforeEach(() => { jest.resetModules(); Notifications = require('utils/notifications'); + + store = configureStore(); }); it('should throw an exception if Notification is not defined on window', async () => { - await expect(Notifications.showNotification()).rejects.toThrow('Notification not supported'); + await expect(store.dispatch(Notifications.showNotification())).rejects.toThrow('Notification not supported'); }); it('should throw an exception if Notification.requestPermission is not defined', async () => { window.Notification = {}; - await expect(Notifications.showNotification()).rejects.toThrow('Notification.requestPermission not supported'); + await expect(store.dispatch(Notifications.showNotification())).rejects.toThrow('Notification.requestPermission not supported'); }); it('should throw an exception if Notification.requestPermission is not a function', async () => { window.Notification = { requestPermission: true, }; - await expect(Notifications.showNotification()).rejects.toThrow('Notification.requestPermission not supported'); + await expect(store.dispatch(Notifications.showNotification())).rejects.toThrow('Notification.requestPermission not supported'); }); it('should request permissions, promise style, if not previously requested, do nothing', async () => { @@ -42,7 +48,7 @@ describe('Notifications.showNotification', () => { requestPermission: () => Promise.resolve('denied'), permission: 'denied', }; - await expect(Notifications.showNotification()).resolves.toBeTruthy(); + await expect(store.dispatch(Notifications.showNotification())).resolves.toBeTruthy(); }); it('should request permissions, callback style, if not previously requested, do nothing', async () => { @@ -54,7 +60,7 @@ describe('Notifications.showNotification', () => { }, permission: 'denied', }; - await expect(Notifications.showNotification()).resolves.toBeTruthy(); + await expect(store.dispatch(Notifications.showNotification())).resolves.toBeTruthy(); }); it('should request permissions, promise style, if not previously requested, handling success', async () => { @@ -65,12 +71,12 @@ describe('Notifications.showNotification', () => { const n = {}; window.Notification.mockReturnValueOnce(n); - await expect(Notifications.showNotification({ + await expect(store.dispatch(Notifications.showNotification({ body: 'body', requireInteraction: true, silent: false, title: '', - })).resolves.toBeTruthy(); + }))).resolves.toBeTruthy(); await expect(window.Notification.mock.calls.length).toBe(1); const call = window.Notification.mock.calls[0]; expect(call[1]).toEqual({ @@ -94,12 +100,12 @@ describe('Notifications.showNotification', () => { const n = {}; window.Notification.mockReturnValueOnce(n); - await expect(Notifications.showNotification({ + await expect(store.dispatch(Notifications.showNotification({ body: 'body', requireInteraction: true, silent: false, title: '', - })).resolves.toBeTruthy(); + }))).resolves.toBeTruthy(); await expect(window.Notification.mock.calls.length).toBe(1); const call = window.Notification.mock.calls[0]; expect(call[1]).toEqual({ @@ -118,9 +124,9 @@ describe('Notifications.showNotification', () => { }; // Call one to deny and mark as already requested, do nothing, throw nothing - await expect(Notifications.showNotification()).resolves.toBeTruthy(); + await expect(store.dispatch(Notifications.showNotification())).resolves.toBeTruthy(); // Try again - await expect(Notifications.showNotification()).resolves.toBeTruthy(); + await expect(store.dispatch(Notifications.showNotification())).resolves.toBeTruthy(); }); }); diff --git a/webapp/channels/src/utils/notifications.ts b/webapp/channels/src/utils/notifications.ts index e85f62470d..6e4e80ebbc 100644 --- a/webapp/channels/src/utils/notifications.ts +++ b/webapp/channels/src/utils/notifications.ts @@ -1,11 +1,19 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import type {ThunkActionFunc} from 'mattermost-redux/types/actions'; + import icon50 from 'images/icon50x50.png'; import iconWS from 'images/icon_WS.png'; import Constants from 'utils/constants'; import * as UserAgent from 'utils/user_agent'; +export type NotificationResult = { + status: 'error' | 'not_sent' | 'success' | 'unsupported'; + reason?: string; + data?: string; +} + let requestedNotificationPermission = false; // showNotification displays a platform notification with the configured parameters. @@ -24,7 +32,7 @@ export interface ShowNotificationParams { onClick?: (this: Notification, e: Event) => any | null; } -export async function showNotification( +export function showNotification( { title, body, @@ -37,67 +45,69 @@ export async function showNotification( requireInteraction: false, silent: false, }, -) { - let icon = icon50; - if (UserAgent.isEdge()) { - icon = iconWS; - } +): ThunkActionFunc void}>> { + return async () => { + let icon = icon50; + if (UserAgent.isEdge()) { + icon = iconWS; + } - if (!('Notification' in window)) { - throw new Error('Notification not supported'); - } + if (!('Notification' in window)) { + throw new Error('Notification not supported'); + } - if (typeof Notification.requestPermission !== 'function') { - throw new Error('Notification.requestPermission not supported'); - } + if (typeof Notification.requestPermission !== 'function') { + throw new Error('Notification.requestPermission not supported'); + } - if (Notification.permission !== 'granted' && requestedNotificationPermission) { - // User didn't allow notifications - return {status: 'not_sent', reason: 'notifications_permission_previously_denied', data: Notification.permission, callback: () => {}}; - } + if (Notification.permission !== 'granted' && requestedNotificationPermission) { + // User didn't allow notifications + return {status: 'not_sent', reason: 'notifications_permission_previously_denied', data: Notification.permission, callback: () => {}}; + } - requestedNotificationPermission = true; + requestedNotificationPermission = true; - let permission = await Notification.requestPermission(); - if (typeof permission === 'undefined') { - // Handle browsers that don't support the promise-based syntax. - permission = await new Promise((resolve) => { - Notification.requestPermission(resolve); + let permission = await Notification.requestPermission(); + if (typeof permission === 'undefined') { + // Handle browsers that don't support the promise-based syntax. + permission = await new Promise((resolve) => { + Notification.requestPermission(resolve); + }); + } + + if (permission !== 'granted') { + // User has denied notification for the site + return {status: 'not_sent', reason: 'notifications_permission_denied', data: permission, callback: () => {}}; + } + + const notification = new Notification(title, { + body, + tag: body, + icon, + requireInteraction, + silent, }); - } - if (permission !== 'granted') { - // User has denied notification for the site - return {status: 'not_sent', reason: 'notifications_permission_denied', data: permission, callback: () => {}}; - } + if (onClick) { + notification.onclick = onClick; + } - const notification = new Notification(title, { - body, - tag: body, - icon, - requireInteraction, - silent, - }); + notification.onerror = () => { + throw new Error('Notification failed to show.'); + }; - if (onClick) { - notification.onclick = onClick; - } + // Mac desktop app notification dismissal is handled by the OS + if (!requireInteraction && !UserAgent.isMacApp()) { + setTimeout(() => { + notification.close(); + }, Constants.DEFAULT_NOTIFICATION_DURATION); + } - notification.onerror = () => { - throw new Error('Notification failed to show.'); - }; - - // Mac desktop app notification dismissal is handled by the OS - if (!requireInteraction && !UserAgent.isMacApp()) { - setTimeout(() => { - notification.close(); - }, Constants.DEFAULT_NOTIFICATION_DURATION); - } - - return { - status: 'success', - callback: () => { - notification.close(); - }, + return { + status: 'success', + callback: () => { + notification.close(); + }, + }; }; }