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
Этот коммит содержится в:
Harrison Healey
2024-04-25 17:24:03 -04:00
коммит произвёл GitHub
родитель 60c15c821f
Коммит 80e67ace86
7 изменённых файлов: 331 добавлений и 393 удалений

Просмотреть файл

@@ -48,7 +48,7 @@ const getNotificationSoundFromChannelMemberAndUser = (member, user) => {
};
/**
* @returns {import('mattermost-redux/types/actions').ThunkActionFunc<Promise<NotificationResult>, GlobalState>}
* @returns {import('mattermost-redux/types/actions').ThunkActionFunc<Promise<import('utils/notifications').NotificationResult>, 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<Promise<import('utils/notifications').NotificationResult>, 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)};

Просмотреть файл

@@ -1,118 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/login/Login should match snapshot 1`] = `
<div
className="login-body"
>
<div
className="login-body-content"
>
<Column
message="Please contact your System Administrator to resolve this."
title="This server doesnt have any sign-in methods enabled"
/>
</div>
</div>
`;
exports[`components/login/Login should match snapshot with base login 1`] = `
<div
className="login-body"
>
<div
className="login-body-content"
>
<div
className="login-body-message"
>
<h1
className="login-body-message-title"
>
Log in to your account
</h1>
<p
className="login-body-message-subtitle"
>
Collaborate with your team in real-time
</p>
<div
className="login-body-message-svg"
>
<Svg
width={270}
/>
</div>
</div>
<div
className="login-body-action"
>
<AlternateLink
alternateLinkLabel="Don't have an account?"
alternateLinkPath="/access_problem"
className="login-body-alternate-link"
onClick={[Function]}
/>
<div
className="login-body-card"
>
<div
className="login-body-card-content"
tabIndex={0}
>
<p
className="login-body-card-title"
>
Log in
</p>
<form
onSubmit={[Function]}
>
<div
className="login-body-card-form"
>
<ForwardRef
autoFocus={true}
containerClassName="login-body-card-form-input"
disabled={false}
hasError={false}
inputSize="large"
name="loginId"
onChange={[Function]}
placeholder="Email"
type="text"
value=""
/>
<ForwardRef
className="login-body-card-form-password-input"
disabled={false}
hasError={false}
inputSize="large"
onChange={[Function]}
value=""
/>
<div
className="login-body-card-form-link"
>
<Link
to="/reset_password"
>
Forgot your password?
</Link>
</div>
<SaveButton
btnClass=""
defaultMessage="Log in"
disabled={false}
extraClasses="login-body-card-form-button-submit large"
onClick={[Function]}
saving={false}
savingMessage="Logging in…"
/>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
`;

Просмотреть файл

@@ -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<ClientConfig>;
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(
<Login/>,
baseState,
);
expect(wrapper).toMatchSnapshot();
expect(screen.queryByText('This server doesnt 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(
<Login/>,
state,
);
expect(wrapper).toMatchSnapshot();
expect(screen.queryByText('This server doesnt 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(
<MemoryRouter><Login/></MemoryRouter>,
const state = mergeObjects(baseState, {
entities: {
general: {
config: {
EnableSignInWithEmail: 'true',
},
},
},
});
renderWithContext(
<Login/>,
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(
<IntlProvider {...intlProviderProps}>
<MemoryRouter>
<Login/>
</MemoryRouter>
</IntlProvider>,
renderWithContext(
<Login/>,
state,
);
// eslint-disable-next-line react/jsx-key, react/jsx-no-literals
expect(wrapper.contains([<p>Loading</p>])).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(
<IntlProvider {...intlProviderProps}>
<Login/>
</IntlProvider>,
renderWithContext(
<Login/>,
state,
);
// eslint-disable-next-line react/jsx-no-literals, react/jsx-key
expect(wrapper.contains([<p>Loading</p>])).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(
<MemoryRouter>
<Login/>
</MemoryRouter>,
const history = createMemoryHistory({
initialEntries: [
{search: '?extra=' + Constants.SIGNIN_CHANGE},
],
});
const state = mergeObjects(baseState, {
entities: {
general: {
config: {
EnableSignInWithEmail: 'true',
},
},
},
});
renderWithContext(
<Login/>,
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(
<MemoryRouter>
<Login/>
</MemoryRouter>,
const state = mergeObjects(baseState, {
entities: {
general: {
config: {
EnableSignInWithEmail: 'true',
},
},
},
});
renderWithContext(
<Login/>,
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(
<Login/>,
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(
<Login/>,
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(
<MemoryRouter>
<Login/>
</MemoryRouter>,
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(
<Login/>,
state,
{
history,
},
);
expect(mockHistoryPush).toHaveBeenCalledWith(redirectPath);
expect(history.push).toHaveBeenCalledWith(redirectPath);
});
});

Просмотреть файл

@@ -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.

Просмотреть файл

@@ -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<unknown>;
}
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,
};

Просмотреть файл

@@ -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<typeof configureStore>;
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();
});
});

Просмотреть файл

@@ -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<Promise<NotificationResult & {callback: () => 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();
},
};
};
}