mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-03-06 07:49:08 +05:30
Cover change username page with e2e tests and fix minor bugs
This commit is contained in:
parent
b2c072e5e1
commit
f284664818
@ -118,6 +118,21 @@ export function authenticate(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-fetch user data for currently active account
|
||||||
|
*/
|
||||||
|
export function refreshUserData(): ThunkAction<Promise<void>> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const activeAccount = getActiveAccount(getState());
|
||||||
|
|
||||||
|
if (!activeAccount) {
|
||||||
|
throw new Error('Can not fetch user data. No user.id available');
|
||||||
|
}
|
||||||
|
|
||||||
|
await dispatch(authenticate(activeAccount));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function findAccountIdFromToken(token: string): number {
|
function findAccountIdFromToken(token: string): number {
|
||||||
const { sub, jti } = getJwtPayloads(token);
|
const { sub, jti } = getJwtPayloads(token);
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ type Props = {
|
|||||||
interfaceLocale: string;
|
interfaceLocale: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Profile extends React.Component<Props> {
|
class Profile extends React.PureComponent<Props> {
|
||||||
UUID: HTMLElement | null;
|
UUID: HTMLElement | null;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -1,52 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import styles from './profile.scss';
|
|
||||||
|
|
||||||
export default class ProfileField extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.element])
|
|
||||||
.isRequired,
|
|
||||||
link: PropTypes.string,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.element])
|
|
||||||
.isRequired,
|
|
||||||
warningMessage: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { label, value, warningMessage, link, onChange } = this.props;
|
|
||||||
|
|
||||||
let Action = null;
|
|
||||||
|
|
||||||
if (link) {
|
|
||||||
Action = props => <Link to={link} {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onChange) {
|
|
||||||
Action = props => <a onClick={onChange} {...props} href="#" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.paramItem}>
|
|
||||||
<div className={styles.paramRow}>
|
|
||||||
<div className={styles.paramName}>{label}</div>
|
|
||||||
<div className={styles.paramValue}>{value}</div>
|
|
||||||
|
|
||||||
{Action ? (
|
|
||||||
<Action to={link} className={styles.paramAction}>
|
|
||||||
<span className={styles.paramEditIcon} />
|
|
||||||
</Action>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{warningMessage ? (
|
|
||||||
<div className={styles.paramMessage}>{warningMessage}</div>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
53
packages/app/components/profile/ProfileField.tsx
Normal file
53
packages/app/components/profile/ProfileField.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import styles from './profile.scss';
|
||||||
|
|
||||||
|
function ProfileField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
warningMessage,
|
||||||
|
link,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: React.ReactNode;
|
||||||
|
link?: string;
|
||||||
|
onChange?: () => void;
|
||||||
|
value: React.ReactNode;
|
||||||
|
warningMessage?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
let Action: React.ElementType | null = null;
|
||||||
|
|
||||||
|
if (link) {
|
||||||
|
Action = props => <Link to={link} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onChange) {
|
||||||
|
Action = props => <a {...props} onClick={onChange} href="#" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.paramItem} data-testid="profile-item">
|
||||||
|
<div className={styles.paramRow}>
|
||||||
|
<div className={styles.paramName}>{label}</div>
|
||||||
|
<div className={styles.paramValue}>{value}</div>
|
||||||
|
|
||||||
|
{Action && (
|
||||||
|
<Action
|
||||||
|
to={link}
|
||||||
|
className={styles.paramAction}
|
||||||
|
data-testid="profile-action"
|
||||||
|
>
|
||||||
|
<span className={styles.paramEditIcon} />
|
||||||
|
</Action>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{warningMessage && (
|
||||||
|
<div className={styles.paramMessage}>{warningMessage}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProfileField;
|
@ -21,6 +21,7 @@ export class BackButton extends FormComponent<{
|
|||||||
className={styles.backButton}
|
className={styles.backButton}
|
||||||
to={to}
|
to={to}
|
||||||
title={this.formatMessage(messages.back)}
|
title={this.formatMessage(messages.back)}
|
||||||
|
data-testid="back-to-profile"
|
||||||
>
|
>
|
||||||
<span className={styles.backIcon} />
|
<span className={styles.backIcon} />
|
||||||
<span className={styles.backText}>
|
<span className={styles.backText}>
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage as Message } from 'react-intl';
|
|
||||||
|
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { Form, Button, Input, FormModel } from 'app/components/ui/form';
|
|
||||||
import popupStyles from 'app/components/ui/popup/popup.scss';
|
|
||||||
import styles from './passwordRequestForm.scss';
|
|
||||||
|
|
||||||
import messages from './PasswordRequestForm.intl.json';
|
|
||||||
|
|
||||||
export default class PasswordRequestForm extends Component {
|
|
||||||
static displayName = 'PasswordRequestForm';
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
form: PropTypes.instanceOf(FormModel).isRequired,
|
|
||||||
onSubmit: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { form } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.requestPasswordForm}>
|
|
||||||
<div className={popupStyles.popup}>
|
|
||||||
<Form onSubmit={this.onFormSubmit} form={form}>
|
|
||||||
<div className={popupStyles.header}>
|
|
||||||
<h2 className={popupStyles.headerTitle}>
|
|
||||||
<Message {...messages.title} />
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={clsx(popupStyles.body, styles.body)}>
|
|
||||||
<span className={styles.lockIcon} />
|
|
||||||
|
|
||||||
<div className={styles.description}>
|
|
||||||
<Message {...messages.description} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
{...form.bindField('password')}
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
color="green"
|
|
||||||
skin="light"
|
|
||||||
center
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
color="green"
|
|
||||||
label={messages.continue}
|
|
||||||
block
|
|
||||||
type="submit"
|
|
||||||
/>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onFormSubmit = () => {
|
|
||||||
this.props.onSubmit(this.props.form);
|
|
||||||
};
|
|
||||||
}
|
|
@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage as Message } from 'react-intl';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { Form, Button, Input, FormModel } from 'app/components/ui/form';
|
||||||
|
import popupStyles from 'app/components/ui/popup/popup.scss';
|
||||||
|
|
||||||
|
import styles from './passwordRequestForm.scss';
|
||||||
|
import messages from './PasswordRequestForm.intl.json';
|
||||||
|
|
||||||
|
function PasswordRequestForm({
|
||||||
|
form,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
form: FormModel;
|
||||||
|
onSubmit: (form: FormModel) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.requestPasswordForm}
|
||||||
|
data-testid="password-request-form"
|
||||||
|
>
|
||||||
|
<div className={popupStyles.popup}>
|
||||||
|
<Form onSubmit={() => onSubmit(form)} form={form}>
|
||||||
|
<div className={popupStyles.header}>
|
||||||
|
<h2 className={popupStyles.headerTitle}>
|
||||||
|
<Message {...messages.title} />
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={clsx(popupStyles.body, styles.body)}>
|
||||||
|
<span className={styles.lockIcon} />
|
||||||
|
|
||||||
|
<div className={styles.description}>
|
||||||
|
<Message {...messages.description} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
{...form.bindField('password')}
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
color="green"
|
||||||
|
skin="light"
|
||||||
|
center
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button color="green" label={messages.continue} block type="submit" />
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PasswordRequestForm;
|
@ -35,7 +35,7 @@ export class PopupStack extends React.Component<{
|
|||||||
return (
|
return (
|
||||||
<CSSTransition
|
<CSSTransition
|
||||||
key={index}
|
key={index}
|
||||||
clsx={{
|
classNames={{
|
||||||
enter: styles.trEnter,
|
enter: styles.trEnter,
|
||||||
enterActive: styles.trEnterActive,
|
enterActive: styles.trEnterActive,
|
||||||
exit: styles.trExit,
|
exit: styles.trExit,
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
getInfo as getInfoEndpoint,
|
|
||||||
changeLang as changeLangEndpoint,
|
changeLang as changeLangEndpoint,
|
||||||
acceptRules as acceptRulesEndpoint,
|
acceptRules as acceptRulesEndpoint,
|
||||||
UserResponse,
|
|
||||||
} from 'app/services/api/accounts';
|
} from 'app/services/api/accounts';
|
||||||
import { setLocale } from 'app/components/i18n/actions';
|
import { setLocale } from 'app/components/i18n/actions';
|
||||||
import { ThunkAction } from 'app/reducers';
|
import { ThunkAction } from 'app/reducers';
|
||||||
@ -39,9 +37,9 @@ export function setUser(payload: Partial<User>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CHANGE_LANG = 'USER_CHANGE_LANG';
|
export const CHANGE_LANG = 'USER_CHANGE_LANG';
|
||||||
export function changeLang(lang: string): ThunkAction<Promise<void>> {
|
export function changeLang(targetLang: string): ThunkAction<Promise<void>> {
|
||||||
return (dispatch, getState) =>
|
return async (dispatch, getState) =>
|
||||||
dispatch(setLocale(lang)).then((lang: string) => {
|
dispatch(setLocale(targetLang)).then((lang: string) => {
|
||||||
const { id, isGuest, lang: oldLang } = getState().user;
|
const { id, isGuest, lang: oldLang } = getState().user;
|
||||||
|
|
||||||
if (oldLang === lang) {
|
if (oldLang === lang) {
|
||||||
@ -72,28 +70,6 @@ export function setGuest(): ThunkAction<Promise<void>> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchUserData(): ThunkAction<Promise<UserResponse>> {
|
|
||||||
return async (dispatch, getState) => {
|
|
||||||
const { id } = getState().user;
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
throw new Error('Can not fetch user data. No user.id available');
|
|
||||||
}
|
|
||||||
|
|
||||||
const resp = await getInfoEndpoint(id);
|
|
||||||
|
|
||||||
dispatch(
|
|
||||||
updateUser({
|
|
||||||
isGuest: false,
|
|
||||||
...resp,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
dispatch(changeLang(resp.lang));
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function acceptRules(): ThunkAction<Promise<{ success: boolean }>> {
|
export function acceptRules(): ThunkAction<Promise<{ success: boolean }>> {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const { id } = getState().user;
|
const { id } = getState().user;
|
||||||
|
@ -36,7 +36,7 @@ export function factory(store: Store): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject();
|
throw new Error('No active account found');
|
||||||
})
|
})
|
||||||
.catch(async () => {
|
.catch(async () => {
|
||||||
// the user is guest or user authentication failed
|
// the user is guest or user authentication failed
|
||||||
|
@ -18,7 +18,7 @@ export interface User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type State = {
|
export type State = {
|
||||||
user: User; // TODO: replace with centralized global state
|
user: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaults: User = {
|
const defaults: User = {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Route, Switch, Redirect } from 'react-router-dom';
|
import { Route, Switch, Redirect } from 'react-router-dom';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { fetchUserData } from 'app/components/user/actions';
|
import { refreshUserData } from 'app/components/accounts/actions';
|
||||||
import { create as createPopup } from 'app/components/ui/popup/actions';
|
import { create as createPopup } from 'app/components/ui/popup/actions';
|
||||||
import PasswordRequestForm from 'app/components/profile/passwordRequestForm/PasswordRequestForm';
|
import PasswordRequestForm from 'app/components/profile/passwordRequestForm/PasswordRequestForm';
|
||||||
import logger from 'app/services/logger';
|
import logger from 'app/services/logger';
|
||||||
@ -24,7 +24,7 @@ interface Props {
|
|||||||
form: FormModel;
|
form: FormModel;
|
||||||
sendData: () => Promise<any>;
|
sendData: () => Promise<any>;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
fetchUserData: () => Promise<any>;
|
refreshUserData: () => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProfilePage extends React.Component<Props> {
|
class ProfilePage extends React.Component<Props> {
|
||||||
@ -74,7 +74,7 @@ class ProfilePage extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
goToProfile = async () => {
|
goToProfile = async () => {
|
||||||
await this.props.fetchUserData();
|
await this.props.refreshUserData();
|
||||||
|
|
||||||
browserHistory.push('/');
|
browserHistory.push('/');
|
||||||
};
|
};
|
||||||
@ -85,7 +85,7 @@ export default connect(
|
|||||||
userId: state.user.id,
|
userId: state.user.id,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
fetchUserData,
|
refreshUserData,
|
||||||
onSubmit: ({
|
onSubmit: ({
|
||||||
form,
|
form,
|
||||||
sendData,
|
sendData,
|
||||||
@ -158,11 +158,11 @@ export default connect(
|
|||||||
).length > 0;
|
).length > 0;
|
||||||
|
|
||||||
if (parentFormHasErrors) {
|
if (parentFormHasErrors) {
|
||||||
// something wrong with parent form, hidding popup and show that form
|
// something wrong with parent form, hiding popup and show that form
|
||||||
props.onClose();
|
props.onClose();
|
||||||
reject(resp);
|
reject(resp);
|
||||||
logger.warn(
|
logger.warn(
|
||||||
'Profile: can not submit pasword popup due to errors in source form',
|
'Profile: can not submit password popup due to errors in source form',
|
||||||
{ resp },
|
{ resp },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
describe('Change username', () => {
|
||||||
|
it('should change username', () => {
|
||||||
|
cy.server();
|
||||||
|
|
||||||
|
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
|
||||||
|
cy.route({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/api/v1/accounts/${account.id}`,
|
||||||
|
response: {
|
||||||
|
id: 7,
|
||||||
|
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
|
||||||
|
username: 'FooBar',
|
||||||
|
isOtpEnabled: false,
|
||||||
|
registeredAt: 1475568334,
|
||||||
|
lang: 'en',
|
||||||
|
elyProfileLink: 'http://ely.by/u7',
|
||||||
|
email: 'danilenkos@auroraglobal.com',
|
||||||
|
isActive: true,
|
||||||
|
passwordChangedAt: 1476075696,
|
||||||
|
hasMojangUsernameCollision: true,
|
||||||
|
shouldAcceptRules: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.route({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/api/v1/accounts/${account.id}/username`,
|
||||||
|
}).as('username');
|
||||||
|
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
cy.getByTestId('profile-item')
|
||||||
|
.contains('Nickname')
|
||||||
|
.closest('[data-testid="profile-item"]')
|
||||||
|
.getByTestId('profile-action')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.location('pathname').should('eq', '/profile/change-username');
|
||||||
|
|
||||||
|
cy.get('[name=username]').type(`{selectall}${account.username}{enter}`);
|
||||||
|
|
||||||
|
cy.wait('@username')
|
||||||
|
.its('requestBody')
|
||||||
|
.should(
|
||||||
|
'eq',
|
||||||
|
new URLSearchParams({
|
||||||
|
username: account.username,
|
||||||
|
password: '',
|
||||||
|
}).toString(),
|
||||||
|
);
|
||||||
|
cy.getByTestId('password-request-form').should('be.visible');
|
||||||
|
|
||||||
|
// unmock accounts route
|
||||||
|
cy.route({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/api/v1/accounts/${account.id}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('[name=password]').type(account.password);
|
||||||
|
cy.getByTestId('password-request-form')
|
||||||
|
.find('[type=submit]')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
cy.wait('@username')
|
||||||
|
.its('requestBody')
|
||||||
|
.should(
|
||||||
|
'eq',
|
||||||
|
new URLSearchParams({
|
||||||
|
username: account.username,
|
||||||
|
password: account.password,
|
||||||
|
}).toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
cy.location('pathname').should('eq', '/');
|
||||||
|
cy.getByTestId('profile-item').should('contain', account.username);
|
||||||
|
cy.getByTestId('toolbar')
|
||||||
|
.contains(account.username)
|
||||||
|
.click();
|
||||||
|
cy.getByTestId('active-account').should('contain', account.username);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should go back to profile', () => {
|
||||||
|
cy.login({ accounts: ['default'] });
|
||||||
|
|
||||||
|
cy.visit('/profile/change-username');
|
||||||
|
|
||||||
|
cy.getByTestId('back-to-profile').click();
|
||||||
|
|
||||||
|
cy.location('pathname').should('eq', '/');
|
||||||
|
});
|
||||||
|
});
|
@ -65,6 +65,7 @@ Cypress.Commands.add(
|
|||||||
return {
|
return {
|
||||||
id: credentials.id,
|
id: credentials.id,
|
||||||
username: credentials.username,
|
username: credentials.username,
|
||||||
|
password: credentials.password,
|
||||||
email: credentials.email,
|
email: credentials.email,
|
||||||
token: resp.access_token,
|
token: resp.access_token,
|
||||||
refreshToken: resp.refresh_token,
|
refreshToken: resp.refresh_token,
|
||||||
|
1
tests-e2e/cypress/support/index.d.ts
vendored
1
tests-e2e/cypress/support/index.d.ts
vendored
@ -5,6 +5,7 @@ type AccountAlias = 'default' | 'default2';
|
|||||||
interface Account {
|
interface Account {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
password: string;
|
||||||
email: string;
|
email: string;
|
||||||
token: string;
|
token: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user