1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-26 04:20:53 +01:00

Refactor: Extract SRP input from create vault component (#13720)

This is a pure refactor that extracts the SRP input from the
`CreateNewVault` component. This is intended to make future changes to
the SRP input easier, and to reduce duplication between the old and new
onboarding flows.

Extensive unit tests have been added for the new SRP input component.

A new test library was added (`@testing-library/user-event`) for
simulating user events with components rendered using the
`@testing-library` library.

A new helper method has been added (`renderWithLocalization`) for
rendering components using `@testing-library` with just our
localization contexts added as a wrapper. The localization contexts
were already added by the `renderWithProviders` helper function, but
there is no need for a Redux provider in these unit tests.
This commit is contained in:
Mark Stacey 2022-02-23 17:00:26 -03:30 committed by GitHub
parent 219f8e6299
commit f49e5076f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 766 additions and 108 deletions

View File

@ -262,6 +262,7 @@
"@testing-library/jest-dom": "^5.11.10", "@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^10.4.8", "@testing-library/react": "^10.4.8",
"@testing-library/react-hooks": "^3.2.1", "@testing-library/react-hooks": "^3.2.1",
"@testing-library/user-event": "^13.5.0",
"@types/react": "^16.9.53", "@types/react": "^16.9.53",
"addons-linter": "1.14.0", "addons-linter": "1.14.0",
"babelify": "^10.0.0", "babelify": "^10.0.0",

View File

@ -79,3 +79,11 @@ if (!window.crypto.getRandomValues) {
// eslint-disable-next-line node/global-require // eslint-disable-next-line node/global-require
window.crypto.getRandomValues = require('polyfill-crypto.getrandomvalues'); window.crypto.getRandomValues = require('polyfill-crypto.getrandomvalues');
} }
// Used to test `clearClipboard` function
if (!window.navigator.clipboard) {
window.navigator.clipboard = {};
}
if (!window.navigator.clipboard.writeText) {
window.navigator.clipboard.writeText = () => undefined;
}

View File

@ -95,3 +95,17 @@ export function renderWithProvider(component, store) {
return render(component, { wrapper: Wrapper }); return render(component, { wrapper: Wrapper });
} }
export function renderWithLocalization(component) {
const Wrapper = ({ children }) => (
<I18nProvider currentLocale="en" current={en} en={en}>
<LegacyI18nProvider>{children}</LegacyI18nProvider>
</I18nProvider>
);
Wrapper.propTypes = {
children: PropTypes.node,
};
return render(component, { wrapper: Wrapper });
}

View File

@ -54,6 +54,7 @@
@import 'selected-account/index'; @import 'selected-account/index';
@import 'signature-request/index'; @import 'signature-request/index';
@import 'signature-request-original/index'; @import 'signature-request-original/index';
@import 'srp-input/srp-input';
@import 'tab-bar/index'; @import 'tab-bar/index';
@import 'token-cell/token-cell'; @import 'token-cell/token-cell';
@import 'token-list-display/token-list-display'; @import 'token-list-display/token-list-display';

View File

@ -1,17 +1,12 @@
import { ethers } from 'ethers';
import React, { useCallback, useContext, useState } from 'react'; import React, { useCallback, useContext, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import { MetaMetricsContext } from '../../../contexts/metametrics'; import { MetaMetricsContext } from '../../../contexts/metametrics';
import TextField from '../../ui/text-field'; import TextField from '../../ui/text-field';
import Button from '../../ui/button'; import Button from '../../ui/button';
import { clearClipboard } from '../../../helpers/utils/util';
import CheckBox from '../../ui/check-box'; import CheckBox from '../../ui/check-box';
import Typography from '../../ui/typography'; import Typography from '../../ui/typography';
import { COLORS } from '../../../helpers/constants/design-system'; import SrpInput from '../srp-input';
import { parseSecretRecoveryPhrase } from './parse-secret-recovery-phrase';
const { isValidMnemonic } = ethers.utils;
export default function CreateNewVault({ export default function CreateNewVault({
disabled = false, disabled = false,
@ -24,33 +19,11 @@ export default function CreateNewVault({
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [passwordError, setPasswordError] = useState(''); const [passwordError, setPasswordError] = useState('');
const [seedPhrase, setSeedPhrase] = useState(''); const [seedPhrase, setSeedPhrase] = useState('');
const [seedPhraseError, setSeedPhraseError] = useState('');
const [showSeedPhrase, setShowSeedPhrase] = useState(false);
const [termsChecked, setTermsChecked] = useState(false); const [termsChecked, setTermsChecked] = useState(false);
const t = useI18nContext(); const t = useI18nContext();
const metricsEvent = useContext(MetaMetricsContext); const metricsEvent = useContext(MetaMetricsContext);
const onSeedPhraseChange = useCallback(
(rawSeedPhrase) => {
let newSeedPhraseError = '';
if (rawSeedPhrase) {
const parsedSeedPhrase = parseSecretRecoveryPhrase(rawSeedPhrase);
const wordCount = parsedSeedPhrase.split(/\s/u).length;
if (wordCount % 3 !== 0 || wordCount > 24 || wordCount < 12) {
newSeedPhraseError = t('seedPhraseReq');
} else if (!isValidMnemonic(parsedSeedPhrase)) {
newSeedPhraseError = t('invalidSeedPhrase');
}
}
setSeedPhrase(rawSeedPhrase);
setSeedPhraseError(newSeedPhraseError);
},
[setSeedPhrase, setSeedPhraseError, t],
);
const onPasswordChange = useCallback( const onPasswordChange = useCallback(
(newPassword) => { (newPassword) => {
let newConfirmPasswordError = ''; let newConfirmPasswordError = '';
@ -93,8 +66,7 @@ export default function CreateNewVault({
seedPhrase && seedPhrase &&
(!includeTerms || termsChecked) && (!includeTerms || termsChecked) &&
!passwordError && !passwordError &&
!confirmPasswordError && !confirmPasswordError;
!seedPhraseError;
const onImport = useCallback( const onImport = useCallback(
async (event) => { async (event) => {
@ -104,7 +76,7 @@ export default function CreateNewVault({
return; return;
} }
await onSubmit(password, parseSecretRecoveryPhrase(seedPhrase)); await onSubmit(password, seedPhrase);
}, },
[isValid, onSubmit, password, seedPhrase], [isValid, onSubmit, password, seedPhrase],
); );
@ -121,10 +93,6 @@ export default function CreateNewVault({
setTermsChecked((currentTermsChecked) => !currentTermsChecked); setTermsChecked((currentTermsChecked) => !currentTermsChecked);
}, [metricsEvent]); }, [metricsEvent]);
const toggleShowSeedPhrase = useCallback(() => {
setShowSeedPhrase((currentShowSeedPhrase) => !currentShowSeedPhrase);
}, []);
const termsOfUse = t('acceptTermsOfUse', [ const termsOfUse = t('acceptTermsOfUse', [
<a <a
className="create-new-vault__terms-link" className="create-new-vault__terms-link"
@ -140,56 +108,7 @@ export default function CreateNewVault({
return ( return (
<form className="create-new-vault__form" onSubmit={onImport}> <form className="create-new-vault__form" onSubmit={onImport}>
<div className="create-new-vault__srp-section"> <div className="create-new-vault__srp-section">
<label <SrpInput onChange={setSeedPhrase} />
htmlFor="create-new-vault__srp"
className="create-new-vault__srp-label"
>
<Typography>{t('secretRecoveryPhrase')}</Typography>
</label>
{showSeedPhrase ? (
<textarea
id="create-new-vault__srp"
className="create-new-vault__srp-shown"
onChange={(e) => onSeedPhraseChange(e.target.value)}
onPaste={clearClipboard}
value={seedPhrase}
placeholder={t('seedPhrasePlaceholder')}
autoComplete="off"
/>
) : (
<TextField
id="create-new-vault__srp"
type="password"
onChange={(e) => onSeedPhraseChange(e.target.value)}
value={seedPhrase}
placeholder={t('seedPhrasePlaceholderPaste')}
autoComplete="off"
onPaste={clearClipboard}
/>
)}
{seedPhraseError ? (
<Typography
color={COLORS.ERROR1}
tag="span"
className="create-new-vault__srp-error"
>
{seedPhraseError}
</Typography>
) : null}
<div className="create-new-vault__show-srp">
<CheckBox
id="create-new-vault__show-srp-checkbox"
checked={showSeedPhrase}
onClick={toggleShowSeedPhrase}
title={t('showSeedPhrase')}
/>
<label
className="create-new-vault__show-srp-label"
htmlFor="create-new-vault__show-srp-checkbox"
>
<Typography tag="span">{t('showSeedPhrase')}</Typography>
</label>
</div>
</div> </div>
<TextField <TextField
id="password" id="password"

View File

@ -5,34 +5,11 @@
width: 360px; width: 360px;
} }
&__srp-shown {
@include Paragraph;
padding: 8px 16px;
}
&__srp-section { &__srp-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
&__srp-label {
margin-bottom: 8px;
}
&__srp-error {
margin-top: 4px;
}
&__show-srp {
margin-top: 16px;
margin-bottom: 16px;
}
&__show-srp-label {
margin-left: 8px;
}
&__terms { &__terms {
margin-top: 16px; margin-top: 16px;
margin-bottom: 16px; margin-bottom: 16px;

View File

@ -0,0 +1 @@
export { default } from './srp-input';

View File

@ -0,0 +1,112 @@
import { ethers } from 'ethers';
import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import { useI18nContext } from '../../../hooks/useI18nContext';
import TextField from '../../ui/text-field';
import { clearClipboard } from '../../../helpers/utils/util';
import CheckBox from '../../ui/check-box';
import Typography from '../../ui/typography';
import { COLORS } from '../../../helpers/constants/design-system';
import { parseSecretRecoveryPhrase } from './parse-secret-recovery-phrase';
const { isValidMnemonic } = ethers.utils;
export default function SrpInput({ onChange }) {
const [srpError, setSrpError] = useState('');
const [draftSrp, setDraftSrp] = useState('');
const [showSrp, setShowSrp] = useState(false);
const t = useI18nContext();
const onSrpChange = useCallback(
(event) => {
const rawSrp = event.target.value;
let newSrpError = '';
const parsedSeedPhrase = parseSecretRecoveryPhrase(rawSrp);
if (rawSrp) {
const wordCount = parsedSeedPhrase.split(/\s/u).length;
if (wordCount % 3 !== 0 || wordCount > 24 || wordCount < 12) {
newSrpError = t('seedPhraseReq');
} else if (!isValidMnemonic(parsedSeedPhrase)) {
newSrpError = t('invalidSeedPhrase');
}
}
setDraftSrp(rawSrp);
setSrpError(newSrpError);
onChange(newSrpError ? '' : parsedSeedPhrase);
},
[setDraftSrp, setSrpError, t, onChange],
);
const toggleShowSrp = useCallback(() => {
setShowSrp((currentShowSrp) => !currentShowSrp);
}, []);
return (
<>
<label htmlFor="import-srp__srp" className="import-srp__srp-label">
<Typography>{t('secretRecoveryPhrase')}</Typography>
</label>
{showSrp ? (
<textarea
id="import-srp__srp"
className="import-srp__srp-shown"
onChange={onSrpChange}
onPaste={clearClipboard}
value={draftSrp}
placeholder={t('seedPhrasePlaceholder')}
autoComplete="off"
/>
) : (
<TextField
id="import-srp__srp"
type="password"
onChange={onSrpChange}
value={draftSrp}
placeholder={t('seedPhrasePlaceholderPaste')}
autoComplete="off"
onPaste={clearClipboard}
/>
)}
{srpError ? (
<Typography
color={COLORS.ERROR1}
tag="span"
className="import-srp__srp-error"
>
{srpError}
</Typography>
) : null}
<div className="import-srp__show-srp">
<CheckBox
id="import-srp__show-srp-checkbox"
checked={showSrp}
onClick={toggleShowSrp}
title={t('showSeedPhrase')}
/>
<label
className="import-srp__show-srp-label"
htmlFor="import-srp__show-srp-checkbox"
>
<Typography tag="span">{t('showSeedPhrase')}</Typography>
</label>
</div>
</>
);
}
SrpInput.propTypes = {
/**
* Event handler for SRP changes.
*
* This is only called with a valid, well-formated (i.e. exactly one space
* between each word) SRP or with an empty string.
*
* This is called each time the draft SRP is updated. If the draft SRP is
* valid, this is called with a well-formatted version of that draft SRP.
* Otherwise, this is called with an empty string.
*/
onChange: PropTypes.func.isRequired,
};

View File

@ -0,0 +1,24 @@
.import-srp {
&__srp-label {
margin-bottom: 8px;
}
&__srp-shown {
@include Paragraph;
padding: 8px 16px;
}
&__srp-error {
margin-top: 4px;
}
&__show-srp {
margin-top: 16px;
margin-bottom: 16px;
}
&__show-srp-label {
margin-left: 8px;
}
}

View File

@ -0,0 +1,23 @@
import React from 'react';
import SrpInput from '.';
export default {
title: 'Components/App/SrpInput',
id: __filename,
argTypes: {
onChange: { action: 'changed' },
},
component: SrpInput,
};
const Template = (args) => {
return (
<div style={{ width: '600px' }}>
<SrpInput {...args} />
</div>
);
};
export const DefaultStory = Template.bind({});
DefaultStory.storyName = 'Default';

View File

@ -0,0 +1,571 @@
import React from 'react';
import { waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import enLocale from '../../../../app/_locales/en/messages.json';
import { renderWithLocalization } from '../../../../test/lib/render-helpers';
import SrpInput from '.';
const tooFewWords = new Array(11).fill('test').join(' ');
const tooManyWords = new Array(25).fill('test').join(' ');
const invalidWordCount = new Array(13).fill('test').join(' ');
const invalidChecksum = new Array(12).fill('test').join(' ');
const invalidWordCorrectChecksum = `aardvark ${new Array(10)
.fill('test')
.join(' ')} wolf`;
const correct = `${new Array(11).fill('test').join(' ')} ball`;
const invalidInputs = [
' ',
'foo',
'🙂',
tooFewWords,
tooManyWords,
invalidWordCount,
invalidChecksum,
invalidWordCorrectChecksum,
];
const poorlyFormattedInputs = [
` ${correct}`,
`\t${correct}`,
`\n${correct}`,
`${correct} `,
`${correct}\t`,
`${correct}\n`,
`${new Array(11).fill('test').join(' ')} ball`,
`${new Array(11).fill('test').join('\t')}\tball`,
];
describe('srp-input', () => {
describe('onChange event', () => {
it('should not fire event on render', async () => {
const onChange = jest.fn();
const { getByLabelText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
await waitFor(() => getByLabelText(enLocale.showSeedPhrase.message));
expect(onChange).not.toHaveBeenCalled();
});
describe('invalid typed inputs', () => {
for (const invalidInput of invalidInputs) {
it(`should fire event with empty string upon invalid input: '${invalidInput}'`, async () => {
const onChange = jest.fn();
const { getByLabelText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
userEvent.keyboard(invalidInput);
expect(onChange).toHaveBeenLastCalledWith('');
});
}
});
describe('invalid pasted inputs', () => {
for (const invalidInput of invalidInputs) {
it(`should fire event with empty string upon invalid pasted input: '${invalidInput}'`, async () => {
const onChange = jest.fn();
const { getByLabelText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
userEvent.paste(
getByLabelText(enLocale.secretRecoveryPhrase.message),
invalidInput,
);
expect(onChange).toHaveBeenLastCalledWith('');
});
}
});
describe('valid typed inputs', () => {
it('should fire event with a valid SRP', () => {
const onChange = jest.fn();
const { getByLabelText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
userEvent.keyboard(correct);
expect(onChange).toHaveBeenLastCalledWith(correct);
});
for (const poorlyFormattedInput of poorlyFormattedInputs) {
it(`should fire with formatted SRP when given poorly formatted valid SRP: '${poorlyFormattedInput}'`, () => {
const onChange = jest.fn();
const { getByLabelText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
userEvent.keyboard(poorlyFormattedInput);
expect(onChange).toHaveBeenLastCalledWith(correct);
});
}
});
describe('valid pasted inputs', () => {
it('should fire event with a valid SRP', () => {
const onChange = jest.fn();
const { getByLabelText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
userEvent.paste(
getByLabelText(enLocale.secretRecoveryPhrase.message),
correct,
);
expect(onChange).toHaveBeenLastCalledWith(correct);
});
for (const poorlyFormattedInput of poorlyFormattedInputs) {
it(`should fire with formatted SRP when given poorly formatted valid SRP: '${poorlyFormattedInput}'`, () => {
const onChange = jest.fn();
const { getByLabelText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
userEvent.paste(
getByLabelText(enLocale.secretRecoveryPhrase.message),
poorlyFormattedInput,
);
expect(onChange).toHaveBeenLastCalledWith(correct);
});
}
});
});
describe('error message', () => {
it('should not show error for empty input', async () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
await waitFor(() => getByLabelText(enLocale.showSeedPhrase.message));
expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
});
describe('typed', () => {
it('should show word requirement error if SRP has too few words', () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
userEvent.keyboard(tooFewWords);
expect(queryByText(enLocale.seedPhraseReq.message)).not.toBeNull();
expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
});
it('should show word requirement error if SRP has too many words', () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
userEvent.keyboard(tooManyWords);
expect(queryByText(enLocale.seedPhraseReq.message)).not.toBeNull();
expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
});
it('should show word requirement error if SRP has an unsupported word count above 12 but below 24', () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
userEvent.keyboard(invalidWordCount);
expect(queryByText(enLocale.seedPhraseReq.message)).not.toBeNull();
expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
});
it('should show invalid SRP error if SRP is correct length but has an invalid checksum', () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
userEvent.keyboard(invalidChecksum);
expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
expect(queryByText(enLocale.invalidSeedPhrase.message)).not.toBeNull();
});
it('should show invalid SRP error if SRP is correct length and has correct checksum but has an invalid word', () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
userEvent.keyboard(invalidWordCorrectChecksum);
expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
expect(queryByText(enLocale.invalidSeedPhrase.message)).not.toBeNull();
});
it('should not show error for valid SRP', () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
userEvent.keyboard(correct);
expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
});
for (const poorlyFormattedInput of poorlyFormattedInputs) {
it(`should not show error for poorly formatted valid SRP: '${poorlyFormattedInput}'`, () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
userEvent.keyboard(poorlyFormattedInput);
expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
});
}
});
describe('pasted', () => {
it('should show word requirement error if SRP has too few words', () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
userEvent.paste(
getByLabelText(enLocale.secretRecoveryPhrase.message),
tooFewWords,
);
expect(queryByText(enLocale.seedPhraseReq.message)).not.toBeNull();
expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
});
it('should show word requirement error if SRP has too many words', () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
userEvent.paste(
getByLabelText(enLocale.secretRecoveryPhrase.message),
tooManyWords,
);
expect(queryByText(enLocale.seedPhraseReq.message)).not.toBeNull();
expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
});
it('should show word requirement error if SRP has an unsupported word count above 12 but below 24', () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
userEvent.paste(
getByLabelText(enLocale.secretRecoveryPhrase.message),
invalidWordCount,
);
expect(queryByText(enLocale.seedPhraseReq.message)).not.toBeNull();
expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
});
it('should show invalid SRP error if SRP is correct length but has an invalid checksum', () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
userEvent.paste(
getByLabelText(enLocale.secretRecoveryPhrase.message),
invalidChecksum,
);
expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
expect(queryByText(enLocale.invalidSeedPhrase.message)).not.toBeNull();
});
it('should show invalid SRP error if SRP is correct length and has correct checksum but has an invalid word', () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
userEvent.paste(
getByLabelText(enLocale.secretRecoveryPhrase.message),
invalidWordCorrectChecksum,
);
expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
expect(queryByText(enLocale.invalidSeedPhrase.message)).not.toBeNull();
});
it('should not show error for valid SRP', () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
userEvent.paste(
getByLabelText(enLocale.secretRecoveryPhrase.message),
correct,
);
expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
});
for (const poorlyFormattedInput of poorlyFormattedInputs) {
it(`should not show error for poorly formatted valid SRP: '${poorlyFormattedInput}'`, () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
userEvent.paste(
getByLabelText(enLocale.secretRecoveryPhrase.message),
poorlyFormattedInput,
);
expect(queryByText(enLocale.seedPhraseReq.message)).toBeNull();
expect(queryByText(enLocale.invalidSeedPhrase.message)).toBeNull();
});
}
});
});
describe('Show/hide SRP', () => {
it('should default to not showing SRP', async () => {
const onChange = jest.fn();
const { getByLabelText, getByRole } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
expect(
getByLabelText(enLocale.secretRecoveryPhrase.message),
).toHaveAttribute('type', 'password');
expect(getByRole('checkbox')).not.toBeChecked();
});
describe('default hidden', () => {
it('should prevent reading typed SRP', async () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
userEvent.keyboard(correct);
expect(queryByText(correct)).toBeNull();
});
it('should prevent reading pasted SRP', async () => {
const onChange = jest.fn();
const { getByLabelText, queryByText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
userEvent.paste(
getByLabelText(enLocale.secretRecoveryPhrase.message),
correct,
);
expect(queryByText(correct)).toBeNull();
});
});
describe('shown then hidden', () => {
it('should prevent reading typed SRP', async () => {
const onChange = jest.fn();
const {
getByLabelText,
getByRole,
queryByText,
} = renderWithLocalization(<SrpInput onChange={onChange} />);
userEvent.click(getByRole('checkbox'));
getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
userEvent.keyboard(correct);
userEvent.click(getByRole('checkbox'));
expect(queryByText(correct)).toBeNull();
});
it('should prevent reading pasted SRP', async () => {
const onChange = jest.fn();
const {
getByLabelText,
getByRole,
queryByText,
} = renderWithLocalization(<SrpInput onChange={onChange} />);
userEvent.click(getByRole('checkbox'));
userEvent.paste(
getByLabelText(enLocale.secretRecoveryPhrase.message),
correct,
);
userEvent.click(getByRole('checkbox'));
expect(queryByText(correct)).toBeNull();
});
});
describe('shown after input', () => {
it('should show typed SRP', () => {
const onChange = jest.fn();
const {
getByLabelText,
getByRole,
queryByText,
} = renderWithLocalization(<SrpInput onChange={onChange} />);
userEvent.paste(
getByLabelText(enLocale.secretRecoveryPhrase.message),
correct,
);
userEvent.click(getByRole('checkbox'));
expect(queryByText(correct)).not.toBeNull();
});
it('should show pasted SRP', () => {
const onChange = jest.fn();
const {
getByLabelText,
getByRole,
queryByText,
} = renderWithLocalization(<SrpInput onChange={onChange} />);
getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
userEvent.keyboard(correct);
userEvent.click(getByRole('checkbox'));
expect(queryByText(correct)).not.toBeNull();
});
});
describe('shown before input', () => {
it('should show typed SRP', () => {
const onChange = jest.fn();
const {
getByLabelText,
getByRole,
queryByText,
} = renderWithLocalization(<SrpInput onChange={onChange} />);
userEvent.click(getByRole('checkbox'));
userEvent.paste(
getByLabelText(enLocale.secretRecoveryPhrase.message),
correct,
);
expect(queryByText(correct)).not.toBeNull();
});
it('should show pasted SRP', () => {
const onChange = jest.fn();
const {
getByLabelText,
getByRole,
queryByText,
} = renderWithLocalization(<SrpInput onChange={onChange} />);
userEvent.click(getByRole('checkbox'));
getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
userEvent.keyboard(correct);
expect(queryByText(correct)).not.toBeNull();
});
});
});
describe('clear after paste', () => {
it('should not clear clipboard after typing hidden SRP', () => {
const onChange = jest.fn();
const writeTextSpy = jest.spyOn(window.navigator.clipboard, 'writeText');
const { getByLabelText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
userEvent.keyboard(correct);
expect(writeTextSpy).not.toHaveBeenCalled();
});
it('should not clear clipboard after typing shown SRP', () => {
const onChange = jest.fn();
const writeTextSpy = jest.spyOn(window.navigator.clipboard, 'writeText');
const { getByLabelText, getByRole } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
userEvent.click(getByRole('checkbox'));
getByLabelText(enLocale.secretRecoveryPhrase.message).focus();
userEvent.keyboard(correct);
expect(writeTextSpy).not.toHaveBeenCalled();
});
it('should clear the clipboard after pasting hidden SRP', () => {
const onChange = jest.fn();
const writeTextSpy = jest.spyOn(window.navigator.clipboard, 'writeText');
const { getByLabelText } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
userEvent.paste(
getByLabelText(enLocale.secretRecoveryPhrase.message),
correct,
);
expect(writeTextSpy).toHaveBeenCalledWith('');
});
it('should clear the clipboard after pasting shown SRP', () => {
const onChange = jest.fn();
const writeTextSpy = jest.spyOn(window.navigator.clipboard, 'writeText');
const { getByLabelText, getByRole } = renderWithLocalization(
<SrpInput onChange={onChange} />,
);
userEvent.click(getByRole('checkbox'));
userEvent.paste(
getByLabelText(enLocale.secretRecoveryPhrase.message),
correct,
);
expect(writeTextSpy).toHaveBeenCalledWith('');
});
});
});

View File

@ -4091,6 +4091,13 @@
"@babel/runtime" "^7.10.3" "@babel/runtime" "^7.10.3"
"@testing-library/dom" "^7.17.1" "@testing-library/dom" "^7.17.1"
"@testing-library/user-event@^13.5.0":
version "13.5.0"
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295"
integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==
dependencies:
"@babel/runtime" "^7.12.5"
"@tootallnate/once@1": "@tootallnate/once@1":
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"