mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-02-02 08:49:30 +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 {
|
||||
const { sub, jti } = getJwtPayloads(token);
|
||||
|
||||
|
@ -19,7 +19,7 @@ type Props = {
|
||||
interfaceLocale: string;
|
||||
};
|
||||
|
||||
class Profile extends React.Component<Props> {
|
||||
class Profile extends React.PureComponent<Props> {
|
||||
UUID: HTMLElement | null;
|
||||
|
||||
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}
|
||||
to={to}
|
||||
title={this.formatMessage(messages.back)}
|
||||
data-testid="back-to-profile"
|
||||
>
|
||||
<span className={styles.backIcon} />
|
||||
<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 (
|
||||
<CSSTransition
|
||||
key={index}
|
||||
clsx={{
|
||||
classNames={{
|
||||
enter: styles.trEnter,
|
||||
enterActive: styles.trEnterActive,
|
||||
exit: styles.trExit,
|
||||
|
@ -1,8 +1,6 @@
|
||||
import {
|
||||
getInfo as getInfoEndpoint,
|
||||
changeLang as changeLangEndpoint,
|
||||
acceptRules as acceptRulesEndpoint,
|
||||
UserResponse,
|
||||
} from 'app/services/api/accounts';
|
||||
import { setLocale } from 'app/components/i18n/actions';
|
||||
import { ThunkAction } from 'app/reducers';
|
||||
@ -39,9 +37,9 @@ export function setUser(payload: Partial<User>) {
|
||||
}
|
||||
|
||||
export const CHANGE_LANG = 'USER_CHANGE_LANG';
|
||||
export function changeLang(lang: string): ThunkAction<Promise<void>> {
|
||||
return (dispatch, getState) =>
|
||||
dispatch(setLocale(lang)).then((lang: string) => {
|
||||
export function changeLang(targetLang: string): ThunkAction<Promise<void>> {
|
||||
return async (dispatch, getState) =>
|
||||
dispatch(setLocale(targetLang)).then((lang: string) => {
|
||||
const { id, isGuest, lang: oldLang } = getState().user;
|
||||
|
||||
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 }>> {
|
||||
return (dispatch, getState) => {
|
||||
const { id } = getState().user;
|
||||
|
@ -36,7 +36,7 @@ export function factory(store: Store): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.reject();
|
||||
throw new Error('No active account found');
|
||||
})
|
||||
.catch(async () => {
|
||||
// the user is guest or user authentication failed
|
||||
|
@ -18,7 +18,7 @@ export interface User {
|
||||
}
|
||||
|
||||
export type State = {
|
||||
user: User; // TODO: replace with centralized global state
|
||||
user: User;
|
||||
};
|
||||
|
||||
const defaults: User = {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Route, Switch, Redirect } from 'react-router-dom';
|
||||
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 PasswordRequestForm from 'app/components/profile/passwordRequestForm/PasswordRequestForm';
|
||||
import logger from 'app/services/logger';
|
||||
@ -24,7 +24,7 @@ interface Props {
|
||||
form: FormModel;
|
||||
sendData: () => Promise<any>;
|
||||
}) => Promise<void>;
|
||||
fetchUserData: () => Promise<any>;
|
||||
refreshUserData: () => Promise<any>;
|
||||
}
|
||||
|
||||
class ProfilePage extends React.Component<Props> {
|
||||
@ -74,7 +74,7 @@ class ProfilePage extends React.Component<Props> {
|
||||
}
|
||||
|
||||
goToProfile = async () => {
|
||||
await this.props.fetchUserData();
|
||||
await this.props.refreshUserData();
|
||||
|
||||
browserHistory.push('/');
|
||||
};
|
||||
@ -85,7 +85,7 @@ export default connect(
|
||||
userId: state.user.id,
|
||||
}),
|
||||
{
|
||||
fetchUserData,
|
||||
refreshUserData,
|
||||
onSubmit: ({
|
||||
form,
|
||||
sendData,
|
||||
@ -158,11 +158,11 @@ export default connect(
|
||||
).length > 0;
|
||||
|
||||
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();
|
||||
reject(resp);
|
||||
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 },
|
||||
);
|
||||
}
|
||||
|
@ -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 {
|
||||
id: credentials.id,
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
email: credentials.email,
|
||||
token: resp.access_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 {
|
||||
id: string;
|
||||
username: string;
|
||||
password: string;
|
||||
email: string;
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
|
Loading…
x
Reference in New Issue
Block a user