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) { export function sendDesktopNotification(post, msgProps) {
return async (dispatch, getState) => { 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) => { export const notifyMe = (title, body, channel, teamId, silent, soundName, url) => async (dispatch) => {
// handle notifications in desktop app // handle notifications in desktop app
if (isDesktopApp()) { if (isDesktopApp()) {
@@ -332,7 +335,7 @@ export const notifyMe = (title, body, channel, teamId, silent, soundName, url) =
} }
try { try {
return await showNotification({ return await dispatch(showNotification({
title, title,
body, body,
requireInteraction: false, requireInteraction: false,
@@ -341,7 +344,7 @@ export const notifyMe = (title, body, channel, teamId, silent, soundName, url) =
window.focus(); window.focus();
getHistory().push(url); getHistory().push(url);
}, },
}); }));
} catch (error) { } catch (error) {
dispatch(logError(error)); dispatch(logError(error));
return {status: 'error', reason: 'notification_api', data: String(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,68 +1,54 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {shallow} from 'enzyme'; import {createMemoryHistory} from 'history';
import React from 'react'; 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 {RequestStatus} from 'mattermost-redux/constants';
import LocalStorageStore from 'stores/local_storage_store'; 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 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 Constants, {WindowSizes} from 'utils/constants';
import type {GlobalState} from 'types/store'; import type {GlobalState} from 'types/store';
let mockState: GlobalState; jest.unmock('react-router-dom');
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,
}));
describe('components/login/Login', () => { describe('components/login/Login', () => {
beforeEach(() => { const baseState = {
mockLocation = {pathname: '', search: '', hash: ''};
LocalStorageStore.setWasLoggedIn(false);
mockState = {
entities: { entities: {
general: { general: {
config: {}, config: {
license: {}, 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',
},
license: {
IsLicensed: 'false',
},
}, },
users: { users: {
currentUserId: '', currentUserId: '',
@@ -104,204 +90,253 @@ describe('components/login/Login', () => {
}, },
} as unknown as GlobalState; } as unknown as GlobalState;
mockConfig = { beforeEach(() => {
EnableLdap: 'false', LocalStorageStore.setWasLoggedIn(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',
};
}); });
it('should match snapshot', () => { it('should match snapshot', () => {
const wrapper = shallow( renderWithContext(
<Login/>, <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', () => { it('should match snapshot with base login', () => {
mockConfig.EnableSignInWithEmail = 'true'; const state = mergeObjects(baseState, {
entities: {
const wrapper = shallow( general: {
<Login/>, config: {
); EnableSignInWithEmail: 'true',
},
expect(wrapper).toMatchSnapshot(); },
},
}); });
it('should handle session expired', () => { renderWithContext(
LocalStorageStore.setWasLoggedIn(true); <Login/>,
mockConfig.EnableSignInWithEmail = 'true'; state,
const wrapper = mountWithIntl(
<MemoryRouter><Login/></MemoryRouter>,
); );
const alertBanner = wrapper.find(AlertBanner).first(); expect(screen.queryByText('This server doesnt have any sign-in methods enabled')).not.toBeInTheDocument();
expect(alertBanner.props().mode).toEqual('warning'); expect(screen.queryByText('Log in to your account')).toBeVisible();
expect(alertBanner.props().title).toEqual('Your session has expired. Please log in again.'); });
alertBanner.find('button').first().simulate('click'); it('should handle session expired', async () => {
LocalStorageStore.setWasLoggedIn(true);
expect(wrapper.find(AlertBanner)).toEqual({}); const state = mergeObjects(baseState, {
entities: {
general: {
config: {
EnableSignInWithEmail: 'true',
},
},
},
});
renderWithContext(
<Login/>,
state,
);
expect(await screen.findByText('Your session has expired. Please log in again.')).toBeVisible();
screen.getByLabelText('Close').click();
expect(screen.queryByText('Your session has expired. Please log in again.')).not.toBeInTheDocument();
}); });
it('should handle initializing when logout status success', () => { 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 = { renderWithContext(
defaultLocale: 'en', <Login/>,
locale: 'en', state,
messages: {},
};
const wrapper = mountWithIntl(
<IntlProvider {...intlProviderProps}>
<MemoryRouter>
<Login/>
</MemoryRouter>
</IntlProvider>,
); );
// eslint-disable-next-line react/jsx-key, react/jsx-no-literals expect(screen.getByText('Loading')).toBeVisible();
expect(wrapper.contains([<p>Loading</p>])).toEqual(true);
}); });
it('should handle initializing when storage not initalized', () => { it('should handle initializing when storage not initalized', () => {
mockState.storage.initialized = false; const state = mergeObjects(baseState, {
storage: {
const intlProviderProps = { initialized: false,
defaultLocale: 'en', },
locale: 'en',
messages: {},
};
const wrapper = mountWithIntl(
<IntlProvider {...intlProviderProps}>
<Login/>
</IntlProvider>,
);
// eslint-disable-next-line react/jsx-no-literals, react/jsx-key
expect(wrapper.contains([<p>Loading</p>])).toEqual(true);
}); });
it('should handle suppress session expired notification on sign in change', () => { renderWithContext(
mockLocation.search = '?extra=' + Constants.SIGNIN_CHANGE; <Login/>,
LocalStorageStore.setWasLoggedIn(true); state,
mockConfig.EnableSignInWithEmail = 'true'; );
const wrapper = mountWithIntl( expect(screen.getByText('Loading')).toBeVisible();
<MemoryRouter> });
<Login/>
</MemoryRouter>, it('should handle suppress session expired notification on sign in change', async () => {
LocalStorageStore.setWasLoggedIn(true);
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); expect(LocalStorageStore.getWasLoggedIn()).toEqual(false);
const alertBanner = wrapper.find(AlertBanner).first(); expect(await screen.findByText('Sign-in method changed successfully')).toBeVisible();
expect(alertBanner.props().mode).toEqual('success');
expect(alertBanner.props().title).toEqual('Sign-in method changed successfully');
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); LocalStorageStore.setWasLoggedIn(true);
mockConfig.EnableSignInWithEmail = 'true';
const wrapper = mountWithIntl( const state = mergeObjects(baseState, {
<MemoryRouter> entities: {
<Login/> general: {
</MemoryRouter>, config: {
EnableSignInWithEmail: 'true',
},
},
},
});
renderWithContext(
<Login/>,
state,
); );
let alertBanner = wrapper.find(AlertBanner).first(); expect(await screen.findByText('Your session has expired. Please log in again.')).toBeVisible();
expect(alertBanner.props().mode).toEqual('warning');
expect(alertBanner.props().title).toEqual('Your session has expired. Please log in again.');
const input = wrapper.find(Input).first().find('input').first(); const emailInput = screen.getByLabelText('Email');
input.simulate('change', {target: {value: 'user1'}}); userEvent.type(emailInput, 'user1');
const passwordInput = wrapper.find(PasswordInput).first().find('input').first(); const passwordInput = screen.getByLabelText('Password');
passwordInput.simulate('change', {target: {value: 'passw'}}); userEvent.type(passwordInput, 'passw');
const saveButton = wrapper.find(SaveButton).first(); screen.getByRole('button', {name: 'Log in'}).click();
expect(saveButton.props().disabled).toEqual(false);
saveButton.find('button').first().simulate('click'); expect(screen.queryByText('Your session has expired. Please log in again.')).not.toBeInTheDocument();
setTimeout(() => {
alertBanner = wrapper.find(AlertBanner).first();
expect(alertBanner.props().mode).toEqual('danger');
expect(alertBanner.props().title).toEqual('The email/username or password is invalid.');
});
}); });
it('should handle gitlab text and color props', () => { it('should handle gitlab text and color props', () => {
mockConfig.EnableSignInWithEmail = 'true'; const state = mergeObjects(baseState, {
mockConfig.EnableSignUpWithGitLab = 'true'; entities: {
mockConfig.GitLabButtonText = 'GitLab 2'; general: {
mockConfig.GitLabButtonColor = '#00ff00'; config: {
EnableSignInWithEmail: 'true',
EnableSignUpWithGitLab: 'true',
GitLabButtonText: 'GitLab 2',
GitLabButtonColor: '#00ff00',
},
},
},
});
const wrapper = shallow( renderWithContext(
<Login/>, <Login/>,
state,
); );
const externalLoginButton = wrapper.find(ExternalLoginButton).first(); const button = screen.getByRole('link', {name: 'Gitlab Icon GitLab 2'});
expect(externalLoginButton.props().url).toEqual('/oauth/gitlab/login');
expect(externalLoginButton.props().label).toEqual('GitLab 2'); expect(button.style).toMatchObject({
expect(externalLoginButton.props().style).toEqual({color: '#00ff00', borderColor: '#00ff00'}); color: 'rgb(0, 255, 0)',
borderColor: '#00ff00',
});
}); });
it('should handle openid text and color props', () => { it('should handle openid text and color props', () => {
mockConfig.EnableSignInWithEmail = 'true'; const state = mergeObjects(baseState, {
mockConfig.EnableSignUpWithOpenId = 'true'; entities: {
mockConfig.OpenIdButtonText = 'OpenID 2'; general: {
mockConfig.OpenIdButtonColor = '#00ff00'; config: {
EnableSignInWithEmail: 'true',
const wrapper = shallow( EnableSignUpWithOpenId: 'true',
<Login/>, OpenIdButtonText: 'OpenID 2',
); OpenIdButtonColor: '#00ff00',
},
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'});
}); });
it('should redirect on login', () => { renderWithContext(
mockState.entities.users.currentUserId = 'user1'; <Login/>,
LocalStorageStore.setWasLoggedIn(true); state,
mockConfig.EnableSignInWithEmail = 'true';
const redirectPath = '/boards/team/teamID/boardID';
mockLocation.search = '?redirect_to=' + redirectPath;
mountWithIntl(
<MemoryRouter>
<Login/>
</MemoryRouter>,
); );
expect(mockHistoryPush).toHaveBeenCalledWith(redirectPath);
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', async () => {
LocalStorageStore.setWasLoggedIn(true);
const redirectPath = '/boards/team/teamID/boardID';
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(history.push).toHaveBeenCalledWith(redirectPath);
}); });
}); });

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

@@ -252,7 +252,7 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
const showSessionExpiredNotificationIfNeeded = useCallback(() => { const showSessionExpiredNotificationIfNeeded = useCallback(() => {
if (sessionExpired && !closeSessionExpiredNotification!.current) { if (sessionExpired && !closeSessionExpiredNotification!.current) {
showNotification({ dispatch(showNotification({
title: siteName, title: siteName,
body: formatMessage({ body: formatMessage({
id: 'login.session_expired.notification', id: 'login.session_expired.notification',
@@ -267,7 +267,7 @@ const Login = ({onCustomizeHeader}: LoginProps) => {
closeSessionExpiredNotification.current = undefined; closeSessionExpiredNotification.current = undefined;
} }
}, },
}).then(({callback: closeNotification}) => { })).then(({callback: closeNotification}) => {
closeSessionExpiredNotification.current = closeNotification; closeSessionExpiredNotification.current = closeNotification;
}).catch(() => { }).catch(() => {
// Ignore the failure to display the notification. // Ignore the failure to display the notification.

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

@@ -3,6 +3,7 @@
import {render} from '@testing-library/react'; import {render} from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import type {History} from 'history';
import {createBrowserHistory} from 'history'; import {createBrowserHistory} from 'history';
import React from 'react'; import React from 'react';
import {IntlProvider} from 'react-intl'; import {IntlProvider} from 'react-intl';
@@ -28,6 +29,7 @@ export type FullContextOptions = {
locale?: string; locale?: string;
useMockedStore?: boolean; useMockedStore?: boolean;
pluginReducers?: string[]; pluginReducers?: string[];
history?: History<unknown>;
} }
export const renderWithContext = ( export const renderWithContext = (
@@ -46,7 +48,7 @@ export const renderWithContext = (
// Store these in an object so that they can be maintained through rerenders // Store these in an object so that they can be maintained through rerenders
const renderState = { const renderState = {
component, component,
history: createBrowserHistory(), history: partialOptions?.history ?? createBrowserHistory(),
options, options,
store: testStore, store: testStore,
}; };

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

@@ -5,6 +5,8 @@
// to enable being a typescript file // to enable being a typescript file
export const a = ''; export const a = '';
import configureStore from 'tests/test_store';
import type {showNotification} from './notifications'; import type {showNotification} from './notifications';
declare global { declare global {
@@ -16,25 +18,29 @@ declare global {
describe('Notifications.showNotification', () => { describe('Notifications.showNotification', () => {
let Notifications: {showNotification: typeof showNotification}; let Notifications: {showNotification: typeof showNotification};
let store: ReturnType<typeof configureStore>;
beforeEach(() => { beforeEach(() => {
jest.resetModules(); jest.resetModules();
Notifications = require('utils/notifications'); Notifications = require('utils/notifications');
store = configureStore();
}); });
it('should throw an exception if Notification is not defined on window', async () => { 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 () => { it('should throw an exception if Notification.requestPermission is not defined', async () => {
window.Notification = {}; 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 () => { it('should throw an exception if Notification.requestPermission is not a function', async () => {
window.Notification = { window.Notification = {
requestPermission: true, 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 () => { it('should request permissions, promise style, if not previously requested, do nothing', async () => {
@@ -42,7 +48,7 @@ describe('Notifications.showNotification', () => {
requestPermission: () => Promise.resolve('denied'), requestPermission: () => Promise.resolve('denied'),
permission: '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 () => { it('should request permissions, callback style, if not previously requested, do nothing', async () => {
@@ -54,7 +60,7 @@ describe('Notifications.showNotification', () => {
}, },
permission: 'denied', 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 () => { it('should request permissions, promise style, if not previously requested, handling success', async () => {
@@ -65,12 +71,12 @@ describe('Notifications.showNotification', () => {
const n = {}; const n = {};
window.Notification.mockReturnValueOnce(n); window.Notification.mockReturnValueOnce(n);
await expect(Notifications.showNotification({ await expect(store.dispatch(Notifications.showNotification({
body: 'body', body: 'body',
requireInteraction: true, requireInteraction: true,
silent: false, silent: false,
title: '', title: '',
})).resolves.toBeTruthy(); }))).resolves.toBeTruthy();
await expect(window.Notification.mock.calls.length).toBe(1); await expect(window.Notification.mock.calls.length).toBe(1);
const call = window.Notification.mock.calls[0]; const call = window.Notification.mock.calls[0];
expect(call[1]).toEqual({ expect(call[1]).toEqual({
@@ -94,12 +100,12 @@ describe('Notifications.showNotification', () => {
const n = {}; const n = {};
window.Notification.mockReturnValueOnce(n); window.Notification.mockReturnValueOnce(n);
await expect(Notifications.showNotification({ await expect(store.dispatch(Notifications.showNotification({
body: 'body', body: 'body',
requireInteraction: true, requireInteraction: true,
silent: false, silent: false,
title: '', title: '',
})).resolves.toBeTruthy(); }))).resolves.toBeTruthy();
await expect(window.Notification.mock.calls.length).toBe(1); await expect(window.Notification.mock.calls.length).toBe(1);
const call = window.Notification.mock.calls[0]; const call = window.Notification.mock.calls[0];
expect(call[1]).toEqual({ expect(call[1]).toEqual({
@@ -118,9 +124,9 @@ describe('Notifications.showNotification', () => {
}; };
// Call one to deny and mark as already requested, do nothing, throw nothing // 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 // 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. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import type {ThunkActionFunc} from 'mattermost-redux/types/actions';
import icon50 from 'images/icon50x50.png'; import icon50 from 'images/icon50x50.png';
import iconWS from 'images/icon_WS.png'; import iconWS from 'images/icon_WS.png';
import Constants from 'utils/constants'; import Constants from 'utils/constants';
import * as UserAgent from 'utils/user_agent'; import * as UserAgent from 'utils/user_agent';
export type NotificationResult = {
status: 'error' | 'not_sent' | 'success' | 'unsupported';
reason?: string;
data?: string;
}
let requestedNotificationPermission = false; let requestedNotificationPermission = false;
// showNotification displays a platform notification with the configured parameters. // showNotification displays a platform notification with the configured parameters.
@@ -24,7 +32,7 @@ export interface ShowNotificationParams {
onClick?: (this: Notification, e: Event) => any | null; onClick?: (this: Notification, e: Event) => any | null;
} }
export async function showNotification( export function showNotification(
{ {
title, title,
body, body,
@@ -37,7 +45,8 @@ export async function showNotification(
requireInteraction: false, requireInteraction: false,
silent: false, silent: false,
}, },
) { ): ThunkActionFunc<Promise<NotificationResult & {callback: () => void}>> {
return async () => {
let icon = icon50; let icon = icon50;
if (UserAgent.isEdge()) { if (UserAgent.isEdge()) {
icon = iconWS; icon = iconWS;
@@ -100,4 +109,5 @@ export async function showNotification(
notification.close(); notification.close();
}, },
}; };
};
} }