Skip to content

Commit

Permalink
umputun#10 localize emailLoginForm
Browse files Browse the repository at this point in the history
  • Loading branch information
Mavrin committed Feb 22, 2020
1 parent 6277fbe commit db35489
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface State {
honeyPotValue: boolean;
}

const messages = defineMessages({
export const messages = defineMessages({
lengthLimit: {
id: 'anonymousLoginForm.length-limit',
defaultMessage: 'Username must be at least 3 characters long',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,53 +1,52 @@
/** @jsx createElement */
import { createElement } from 'preact';
import { mount } from 'enzyme';
import { EmailLoginForm, Props, State } from './auth-panel__email-login-form';
import { mount, ReactWrapper } from 'enzyme';
import { EmailLoginFormConnected as EmailLoginForm, Props, State } from './auth-panel__email-login-form';
import { User } from '@app/common/types';
import { sleep } from '@app/utils/sleep';
import { validToken } from '@app/testUtils/mocks/jwt';
import { createIntl } from 'react-intl';
import { sendEmailVerificationRequest } from '@app/common/api';
import { IntlProvider } from 'react-intl';
import enMessages from '../../../locales/en.json';

jest.mock('@app/utils/jwt', () => ({
isJwtExpired: jest
.fn()
.mockImplementationOnce(() => true)
.mockImplementationOnce(() => false)
.mockImplementationOnce(() => true),
}));

const intl = createIntl({
locale: `en`,
messages: enMessages,
});
jest.mock('@app/common/api');

function simulateInput(input: ReactWrapper, value: string) {
input.getDOMNode<HTMLTextAreaElement>().value = value;
input.simulate('input');
}

describe('EmailLoginForm', () => {
const testUser = ({} as any) as User;
const onSuccess = jest.fn(async () => {});
const onSignIn = jest.fn(async () => testUser);

it('works', async () => {
const sendEmailVerification = jest.fn(async () => {});
beforeEach(() => {
(sendEmailVerificationRequest as any).mockReset();
});

it('works', async () => {
(sendEmailVerificationRequest as any).mockResolvedValueOnce({});
const el = mount<Props, State>(
<EmailLoginForm
sendEmailVerification={sendEmailVerification}
onSignIn={onSignIn}
onSuccess={onSuccess}
theme="light"
intl={intl}
/>
<IntlProvider locale="en" messages={enMessages}>
<EmailLoginForm onSignIn={onSignIn} onSuccess={onSuccess} theme="light" />
</IntlProvider>
);

await new Promise(resolve =>
el.setState({ usernameValue: 'someone', addressValue: '[email protected]' } as State, resolve)
);

simulateInput(el.find(`input[name="email"]`), '[email protected]');
simulateInput(el.find(`input[name="username"]`), 'someone');
el.find('form').simulate('submit');
await sleep(100);
expect(sendEmailVerification).toBeCalledWith('someone', '[email protected]');
expect(el.state().verificationSent).toBe(true);

await new Promise(resolve => el.setState({ tokenValue: 'abcd' } as State, resolve));
expect(sendEmailVerificationRequest).toBeCalledWith('someone', '[email protected]');
el.update();
simulateInput(el.find(`textarea[name="token"]`), 'abcd');

el.find('form').simulate('submit');
await sleep(100);
Expand All @@ -56,47 +55,36 @@ describe('EmailLoginForm', () => {
});

it('should send form by pasting token', async () => {
const sendEmailVerification = jest.fn(async () => {});
(sendEmailVerificationRequest as any).mockResolvedValueOnce({});
const onSignIn = jest.fn(async () => testUser);

const wrapper = mount<Props, State>(
<EmailLoginForm
sendEmailVerification={sendEmailVerification}
onSignIn={onSignIn}
onSuccess={onSuccess}
theme="light"
intl={intl}
/>
);
await new Promise(resolve =>
wrapper.setState({ usernameValue: 'someone', addressValue: '[email protected]' } as State, resolve)
<IntlProvider locale="en" messages={enMessages}>
<EmailLoginForm onSignIn={onSignIn} onSuccess={onSuccess} theme="light" />
</IntlProvider>
);
simulateInput(wrapper.find(`input[name="email"]`), '[email protected]');
simulateInput(wrapper.find(`input[name="username"]`), 'someone');
wrapper.find('form').simulate('submit');
await sleep(100);
wrapper.update();

wrapper.find('textarea').getDOMNode<HTMLTextAreaElement>().value = validToken;
wrapper.find('textarea').simulate('input');

simulateInput(wrapper.find(`textarea[name="token"]`), validToken);
await sleep(100);
wrapper.update();
expect(onSignIn).toBeCalledWith(validToken);
});

it('should show error "Token is expired" on paste', async () => {
const sendEmailVerification = jest.fn(async () => {});
(sendEmailVerificationRequest as any).mockResolvedValueOnce({});
const onSignIn = jest.fn(async () => testUser);

const wrapper = mount<Props, State>(
<EmailLoginForm
sendEmailVerification={sendEmailVerification}
onSignIn={onSignIn}
onSuccess={onSuccess}
theme="light"
intl={intl}
/>
);
await new Promise(resolve =>
wrapper.setState({ usernameValue: 'someone', addressValue: '[email protected]' } as State, resolve)
<IntlProvider locale="en" messages={enMessages}>
<EmailLoginForm onSignIn={onSignIn} onSuccess={onSuccess} theme="light" />
</IntlProvider>
);
simulateInput(wrapper.find(`input[name="email"]`), '[email protected]');
simulateInput(wrapper.find(`input[name="username"]`), 'someone');
wrapper.find('form').simulate('submit');
await sleep(100);
wrapper.update();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { createElement, Component, createRef } from 'preact';
import { forwardRef } from 'preact/compat';
import b from 'bem-react-helper';
import { useSelector } from 'react-redux';
import { Theme, User } from '@app/common/types';
import { sendEmailVerificationRequest } from '@app/common/api';
import { extractErrorMessageFromResponse } from '@app/utils/errorUtils';
Expand All @@ -12,11 +11,9 @@ import TextareaAutosize from '@app/components/comment-form/textarea-autosize';
import { Input } from '@app/components/input';
import { Button } from '@app/components/button';
import { isJwtExpired } from '@app/utils/jwt';
import { IntlShape, useIntl } from 'react-intl';
import { defineMessages, IntlShape, useIntl, FormattedMessage } from 'react-intl';

const mapStateToProps = () => ({
sendEmailVerification: sendEmailVerificationRequest,
});
import { messages as loginForm } from '../__anonymous-login-form/auth-panel__anonymous-login-form';

interface OwnProps {
onSignIn(token: string): Promise<User | null>;
Expand All @@ -25,7 +22,7 @@ interface OwnProps {
className?: string;
}

export type Props = OwnProps & ReturnType<typeof mapStateToProps> & { intl: IntlShape };
export type Props = OwnProps & { intl: IntlShape; sendEmailVerification: typeof sendEmailVerificationRequest };

export interface State {
usernameValue: string;
Expand All @@ -36,6 +33,37 @@ export interface State {
error: string | null;
}

const messages = defineMessages({
expiredToken: {
id: 'emailLoginForm.expiredToken',
defaultMessage: 'Token is expired',
},
userNotFound: {
id: 'emailLoginForm.user-not-found',
defaultMessage: 'No user was found',
},
loading: {
id: 'emailLoginForm.loading',
defaultMessage: 'Loading...',
},
invalidEmail: {
id: 'emailLoginForm.invalid-email',
defaultMessage: 'Address should be valid email address',
},
emptyToken: {
id: 'emailLoginForm.empty-token',
defaultMessage: 'Token field must not be empty',
},
emailAddress: {
id: 'emailLoginForm.email-address',
defaultMessage: 'Email Address',
},
token: {
id: 'emailLoginForm.token',
defaultMessage: 'Token',
},
});

export class EmailLoginForm extends Component<Props, State> {
static usernameRegex = /^[a-zA-Z][\w ]+$/;
static emailRegex = /[^@]+@[^.]+\..+/;
Expand Down Expand Up @@ -78,15 +106,18 @@ export class EmailLoginForm extends Component<Props, State> {
};

async sendForm(token: string = this.state.tokenValue) {
const intl = this.props.intl;
try {
this.setState({ loading: true });
const user = await this.props.onSignIn(token);
if (!user) {
this.setState({ error: 'No user was found' });
this.setState({ error: intl.formatMessage(messages.userNotFound) });
return;
}
this.setState({ verificationSent: false, tokenValue: '' });
this.props.onSuccess && this.props.onSuccess(user);
if (this.props.onSuccess) {
await this.props.onSuccess(user);
}
} catch (e) {
this.setState({ error: extractErrorMessageFromResponse(e, this.props.intl) });
} finally {
Expand All @@ -108,13 +139,14 @@ export class EmailLoginForm extends Component<Props, State> {
};

onTokenChange = (e: Event) => {
const intl = this.props.intl;
const { value } = e.target as HTMLInputElement;

this.setState({ error: null, tokenValue: value });

try {
if (value.length > 0 && isJwtExpired(value)) {
this.setState({ error: 'Token is expired' });
this.setState({ error: intl.formatMessage(messages.expiredToken) });
return;
}
this.sendForm(value);
Expand All @@ -141,22 +173,24 @@ export class EmailLoginForm extends Component<Props, State> {
};

getForm1InvalidReason(): string | null {
if (this.state.loading) return 'Loading...';
const intl = this.props.intl;
if (this.state.loading) return intl.formatMessage(messages.loading);
const username = this.state.usernameValue;
if (username.length < 3) return 'Username must be at least 3 characters long';
if (!EmailLoginForm.usernameRegex.test(username))
return 'Username must start from the letter and contain only latin letters, numbers, underscores, and spaces';
if (!EmailLoginForm.emailRegex.test(this.state.addressValue)) return 'Address should be valid email address';
if (username.length < 3) return intl.formatMessage(loginForm.lengthLimit);
if (!EmailLoginForm.usernameRegex.test(username)) return intl.formatMessage(loginForm.symbolLimit);
if (!EmailLoginForm.emailRegex.test(this.state.addressValue)) return intl.formatMessage(messages.invalidEmail);
return null;
}

getForm2InvalidReason(): string | null {
if (this.state.loading) return 'Loading...';
if (this.state.tokenValue.length === 0) return 'Token field must not be empty';
const intl = this.props.intl;
if (this.state.loading) return intl.formatMessage(messages.loading);
if (this.state.tokenValue.length === 0) return intl.formatMessage(messages.emptyToken);
return null;
}

render(props: Props) {
const intl = props.intl;
// TODO: will be great to `b` to accept `string | undefined | (string|undefined)[]` as classname
let className = b('auth-panel-email-login-form', {}, { theme: props.theme });
if (props.className) {
Expand All @@ -170,16 +204,18 @@ export class EmailLoginForm extends Component<Props, State> {
<form className={className} onSubmit={this.onVerificationSubmit}>
<Input
autoFocus
name="username"
mix="auth-panel-email-login-form__input"
ref={this.usernameInputRef}
placeholder="Username"
placeholder={intl.formatMessage(loginForm.userName)}
value={this.state.usernameValue}
onInput={this.onUsernameChange}
/>
<Input
mix="auth-panel-email-login-form__input"
type="email"
placeholder="Email Address"
name="email"
placeholder={intl.formatMessage(messages.emailAddress)}
value={this.state.addressValue}
onInput={this.onAddressChange}
/>
Expand All @@ -192,7 +228,7 @@ export class EmailLoginForm extends Component<Props, State> {
title={form1InvalidReason || ''}
disabled={form1InvalidReason !== null}
>
Send Verification
<FormattedMessage id="emailLoginForm.send-verification" defaultMessage="Send Verification" />
</Button>
</form>
);
Expand All @@ -202,13 +238,14 @@ export class EmailLoginForm extends Component<Props, State> {
return (
<form className={className} onSubmit={this.onSubmit}>
<Button kind="link" mix="auth-panel-email-login-form__back-button" {...getHandleClickProps(this.goBack)}>
Back
<FormattedMessage id="emailLoginForm.back" defaultMessage="Back" />
</Button>
<TextareaAutosize
autofocus={true}
name="token"
className="auth-panel-email-login-form__token-input"
ref={this.tokenRef}
placeholder="Token"
placeholder={intl.formatMessage(messages.token)}
value={this.state.tokenValue}
onInput={this.onTokenChange}
spellcheck={false}
Expand All @@ -223,7 +260,7 @@ export class EmailLoginForm extends Component<Props, State> {
title={form2InvalidReason || ''}
disabled={form2InvalidReason !== null}
>
Confirm
<FormattedMessage id="emailLoginForm.confirm" defaultMessage="Confirm" />
</Button>
</form>
);
Expand All @@ -233,7 +270,6 @@ export class EmailLoginForm extends Component<Props, State> {
export type EmailLoginFormRef = EmailLoginForm;

export const EmailLoginFormConnected = forwardRef<EmailLoginForm, OwnProps>((props, ref) => {
const connectedProps = useSelector(mapStateToProps);
const intl = useIntl();
return <EmailLoginForm {...props} {...connectedProps} intl={intl} ref={ref} />;
return <EmailLoginForm {...props} sendEmailVerification={sendEmailVerificationRequest} intl={intl} ref={ref} />;
});
10 changes: 10 additions & 0 deletions frontend/app/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@
"commentsSort.oldest": "Oldest",
"commentsSort.recently-updated": "Recently updated",
"commentsSort.worst": "Worst",
"emailLoginForm.back": "Back",
"emailLoginForm.confirm": "Confirm",
"emailLoginForm.email-address": "Email Address",
"emailLoginForm.empty-token": "Token field must not be empty",
"emailLoginForm.expiredToken": "Token is expired",
"emailLoginForm.invalid-email": "Address should be valid email address",
"emailLoginForm.loading": "Loading...",
"emailLoginForm.send-verification": "Send Verification",
"emailLoginForm.token": "Token",
"emailLoginForm.user-not-found": "No user was found",
"errors.0": "Something went wrong. Please try again a bit later.",
"errors.1": "Comment cannot be found. Please refresh the page and try again.",
"errors.10": "It is too late to edit the comment.",
Expand Down
10 changes: 10 additions & 0 deletions frontend/app/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@
"commentsSort.oldest": "Старые",
"commentsSort.recently-updated": "Недавно обновленные",
"commentsSort.worst": "Худшие",
"emailLoginForm.back": "Back",
"emailLoginForm.confirm": "Confirm",
"emailLoginForm.email-address": "Email Address",
"emailLoginForm.empty-token": "Token field must not be empty",
"emailLoginForm.expiredToken": "Token is expired",
"emailLoginForm.invalid-email": "Address should be valid email address",
"emailLoginForm.loading": "Loading...",
"emailLoginForm.send-verification": "Send Verification",
"emailLoginForm.token": "Token",
"emailLoginForm.user-not-found": "No user was found",
"errors.0": "Something went wrong. Please try again a bit later.",
"errors.1": "Comment cannot be found. Please refresh the page and try again.",
"errors.10": "It is too late to edit the comment.",
Expand Down

0 comments on commit db35489

Please sign in to comment.