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) {
|
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 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.
|
// 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: ''};
|
entities: {
|
||||||
|
general: {
|
||||||
LocalStorageStore.setWasLoggedIn(false);
|
config: {
|
||||||
|
EnableLdap: 'false',
|
||||||
mockState = {
|
EnableSaml: 'false',
|
||||||
entities: {
|
EnableSignInWithEmail: 'false',
|
||||||
general: {
|
EnableSignInWithUsername: 'false',
|
||||||
config: {},
|
EnableSignUpWithEmail: 'false',
|
||||||
license: {},
|
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: {
|
license: {
|
||||||
currentUserId: '',
|
IsLicensed: 'false',
|
||||||
profiles: {
|
},
|
||||||
user1: {
|
},
|
||||||
id: 'user1',
|
users: {
|
||||||
roles: '',
|
currentUserId: '',
|
||||||
},
|
profiles: {
|
||||||
|
user1: {
|
||||||
|
id: 'user1',
|
||||||
|
roles: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
teams: {
|
||||||
|
currentTeamId: 'team1',
|
||||||
teams: {
|
teams: {
|
||||||
currentTeamId: 'team1',
|
team1: {
|
||||||
teams: {
|
id: 'team1',
|
||||||
team1: {
|
name: 'team-1',
|
||||||
id: 'team1',
|
displayName: 'Team 1',
|
||||||
name: 'team-1',
|
|
||||||
displayName: 'Team 1',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
myMembers: {
|
|
||||||
team1: {roles: 'team_role'},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
myMembers: {
|
||||||
requests: {
|
team1: {roles: 'team_role'},
|
||||||
users: {
|
|
||||||
logout: {
|
|
||||||
status: RequestStatus.NOT_STARTED,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
storage: {
|
},
|
||||||
initialized: true,
|
requests: {
|
||||||
},
|
users: {
|
||||||
views: {
|
logout: {
|
||||||
browser: {
|
status: RequestStatus.NOT_STARTED,
|
||||||
windowSize: WindowSizes.DESKTOP_VIEW,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as GlobalState;
|
},
|
||||||
|
storage: {
|
||||||
|
initialized: true,
|
||||||
|
},
|
||||||
|
views: {
|
||||||
|
browser: {
|
||||||
|
windowSize: WindowSizes.DESKTOP_VIEW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} 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 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', () => {
|
it('should match snapshot with base login', () => {
|
||||||
mockConfig.EnableSignInWithEmail = 'true';
|
const state = mergeObjects(baseState, {
|
||||||
|
entities: {
|
||||||
|
general: {
|
||||||
|
config: {
|
||||||
|
EnableSignInWithEmail: 'true',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const wrapper = shallow(
|
renderWithContext(
|
||||||
<Login/>,
|
<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);
|
LocalStorageStore.setWasLoggedIn(true);
|
||||||
mockConfig.EnableSignInWithEmail = 'true';
|
|
||||||
|
|
||||||
const wrapper = mountWithIntl(
|
const state = mergeObjects(baseState, {
|
||||||
<MemoryRouter><Login/></MemoryRouter>,
|
entities: {
|
||||||
|
general: {
|
||||||
|
config: {
|
||||||
|
EnableSignInWithEmail: 'true',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderWithContext(
|
||||||
|
<Login/>,
|
||||||
|
state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const 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.');
|
|
||||||
|
|
||||||
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', () => {
|
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: {
|
||||||
|
initialized: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const intlProviderProps = {
|
renderWithContext(
|
||||||
defaultLocale: 'en',
|
<Login/>,
|
||||||
locale: 'en',
|
state,
|
||||||
messages: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = mountWithIntl(
|
|
||||||
<IntlProvider {...intlProviderProps}>
|
|
||||||
<Login/>
|
|
||||||
</IntlProvider>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line react/jsx-no-literals, react/jsx-key
|
expect(screen.getByText('Loading')).toBeVisible();
|
||||||
expect(wrapper.contains([<p>Loading</p>])).toEqual(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle suppress session expired notification on sign in change', () => {
|
it('should handle suppress session expired notification on sign in change', async () => {
|
||||||
mockLocation.search = '?extra=' + Constants.SIGNIN_CHANGE;
|
|
||||||
LocalStorageStore.setWasLoggedIn(true);
|
LocalStorageStore.setWasLoggedIn(true);
|
||||||
mockConfig.EnableSignInWithEmail = 'true';
|
|
||||||
|
|
||||||
const wrapper = mountWithIntl(
|
const history = createMemoryHistory({
|
||||||
<MemoryRouter>
|
initialEntries: [
|
||||||
<Login/>
|
{search: '?extra=' + Constants.SIGNIN_CHANGE},
|
||||||
</MemoryRouter>,
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
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',
|
||||||
|
EnableSignUpWithOpenId: 'true',
|
||||||
|
OpenIdButtonText: 'OpenID 2',
|
||||||
|
OpenIdButtonColor: '#00ff00',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const wrapper = shallow(
|
renderWithContext(
|
||||||
<Login/>,
|
<Login/>,
|
||||||
|
state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const externalLoginButton = wrapper.find(ExternalLoginButton).first();
|
const button = screen.getByRole('link', {name: 'OpenID Icon OpenID 2'});
|
||||||
expect(externalLoginButton.props().url).toEqual('/oauth/openid/login');
|
|
||||||
expect(externalLoginButton.props().label).toEqual('OpenID 2');
|
expect(button.style).toMatchObject({
|
||||||
expect(externalLoginButton.props().style).toEqual({color: '#00ff00', borderColor: '#00ff00'});
|
color: 'rgb(0, 255, 0)',
|
||||||
|
borderColor: '#00ff00',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect on login', () => {
|
it('should redirect on login', async () => {
|
||||||
mockState.entities.users.currentUserId = 'user1';
|
|
||||||
LocalStorageStore.setWasLoggedIn(true);
|
LocalStorageStore.setWasLoggedIn(true);
|
||||||
mockConfig.EnableSignInWithEmail = 'true';
|
|
||||||
const redirectPath = '/boards/team/teamID/boardID';
|
const redirectPath = '/boards/team/teamID/boardID';
|
||||||
mockLocation.search = '?redirect_to=' + redirectPath;
|
|
||||||
mountWithIntl(
|
const history = createMemoryHistory({
|
||||||
<MemoryRouter>
|
initialEntries: [
|
||||||
<Login/>
|
{search: '?redirect_to=' + redirectPath},
|
||||||
</MemoryRouter>,
|
],
|
||||||
|
});
|
||||||
|
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(() => {
|
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,67 +45,69 @@ export async function showNotification(
|
|||||||
requireInteraction: false,
|
requireInteraction: false,
|
||||||
silent: false,
|
silent: false,
|
||||||
},
|
},
|
||||||
) {
|
): ThunkActionFunc<Promise<NotificationResult & {callback: () => void}>> {
|
||||||
let icon = icon50;
|
return async () => {
|
||||||
if (UserAgent.isEdge()) {
|
let icon = icon50;
|
||||||
icon = iconWS;
|
if (UserAgent.isEdge()) {
|
||||||
}
|
icon = iconWS;
|
||||||
|
}
|
||||||
|
|
||||||
if (!('Notification' in window)) {
|
if (!('Notification' in window)) {
|
||||||
throw new Error('Notification not supported');
|
throw new Error('Notification not supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof Notification.requestPermission !== 'function') {
|
if (typeof Notification.requestPermission !== 'function') {
|
||||||
throw new Error('Notification.requestPermission not supported');
|
throw new Error('Notification.requestPermission not supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Notification.permission !== 'granted' && requestedNotificationPermission) {
|
if (Notification.permission !== 'granted' && requestedNotificationPermission) {
|
||||||
// User didn't allow notifications
|
// User didn't allow notifications
|
||||||
return {status: 'not_sent', reason: 'notifications_permission_previously_denied', data: Notification.permission, callback: () => {}};
|
return {status: 'not_sent', reason: 'notifications_permission_previously_denied', data: Notification.permission, callback: () => {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedNotificationPermission = true;
|
requestedNotificationPermission = true;
|
||||||
|
|
||||||
let permission = await Notification.requestPermission();
|
let permission = await Notification.requestPermission();
|
||||||
if (typeof permission === 'undefined') {
|
if (typeof permission === 'undefined') {
|
||||||
// Handle browsers that don't support the promise-based syntax.
|
// Handle browsers that don't support the promise-based syntax.
|
||||||
permission = await new Promise((resolve) => {
|
permission = await new Promise((resolve) => {
|
||||||
Notification.requestPermission(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') {
|
if (onClick) {
|
||||||
// User has denied notification for the site
|
notification.onclick = onClick;
|
||||||
return {status: 'not_sent', reason: 'notifications_permission_denied', data: permission, callback: () => {}};
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const notification = new Notification(title, {
|
notification.onerror = () => {
|
||||||
body,
|
throw new Error('Notification failed to show.');
|
||||||
tag: body,
|
};
|
||||||
icon,
|
|
||||||
requireInteraction,
|
|
||||||
silent,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (onClick) {
|
// Mac desktop app notification dismissal is handled by the OS
|
||||||
notification.onclick = onClick;
|
if (!requireInteraction && !UserAgent.isMacApp()) {
|
||||||
}
|
setTimeout(() => {
|
||||||
|
notification.close();
|
||||||
|
}, Constants.DEFAULT_NOTIFICATION_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
notification.onerror = () => {
|
return {
|
||||||
throw new Error('Notification failed to show.');
|
status: 'success',
|
||||||
};
|
callback: () => {
|
||||||
|
notification.close();
|
||||||
// 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();
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user