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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
60c15c821f
Коммит
80e67ace86
@@ -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 doesn’t 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 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(
|
||||
<Login/>,
|
||||
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(
|
||||
<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();
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user