chore(account): write tests for CreateAccessToken [EE-2561] (#6578)

pull/6651/head
Chaim Lev-Ari 2022-03-13 09:14:41 +02:00 committed by GitHub
parent b7d18ef50f
commit f1ea2b5c02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 178 additions and 27 deletions

View File

@ -0,0 +1,39 @@
import * as i18nextMocks from './i18next';
describe('mockT', () => {
it('should return correctly with no arguments', async () => {
const testText = `The company's new IT initiative, code named Phoenix Project, is critical to the
future of Parts Unlimited, but the project is massively over budget and very late. The CEO wants
Bill to report directly to him and fix the mess in ninety days or else Bill's entire department
will be outsourced.`;
const translatedText = i18nextMocks.mockT(testText);
expect(translatedText).toBe(testText);
});
test.each`
testText | args | expectedText
${'{{fileName}} is invalid.'} | ${{ fileName: 'example_5.csv' }} | ${'example_5.csv is invalid.'}
${'{{fileName}} {is}.'} | ${{ fileName: ' ' }} | ${' {is}.'}
${'{{number}} of {{total}}'} | ${{ number: 0, total: 999 }} | ${'0 of 999'}
${'There was an error:\n{{error}}'} | ${{ error: 'Failed' }} | ${'There was an error:\nFailed'}
${'Click:{{li}}{{li2}}{{li_3}}'} | ${{ li: '', li2: 'https://', li_3: '!@#$%' }} | ${'Click:https://!@#$%'}
${'{{happy}}😏y✔{{sad}}{{laugh}}'} | ${{ happy: '😃', sad: '😢', laugh: '🤣' }} | ${'😃😏y✔😢🤣'}
`(
'should return correctly while handling arguments in different scenarios',
({ testText, args, expectedText }) => {
const translatedText = i18nextMocks.mockT(testText, args);
expect(translatedText).toBe(expectedText);
}
);
});
describe('language', () => {
it('should return language', async () => {
const { language } = i18nextMocks.default;
expect(language).toBe('en');
});
});

36
app/__mocks__/i18next.ts Normal file
View File

@ -0,0 +1,36 @@
function replaceBetween(
startIndex: number,
endIndex: number,
original: string,
insertion: string
) {
const result =
original.substring(0, startIndex) +
insertion +
original.substring(endIndex);
return result;
}
export function mockT(i18nKey: string, args?: Record<string, string>) {
let key = i18nKey;
while (key.includes('{{') && args) {
const startIndex = key.indexOf('{{');
const endIndex = key.indexOf('}}');
const currentArg = key.substring(startIndex + 2, endIndex);
const value = args[currentArg];
key = replaceBetween(startIndex, endIndex + 2, key, value);
}
return key;
}
const i18next: Record<string, unknown> = jest.createMockFromModule('i18next');
i18next.t = mockT;
i18next.language = 'en';
i18next.changeLanguage = () => new Promise(() => {});
i18next.use = () => i18next;
export default i18next;

View File

@ -0,0 +1,16 @@
import { PropsWithChildren } from 'react';
import { mockT } from './i18next';
export function useTranslation() {
return {
t: mockT,
i18n: {
changeLanguage: () => new Promise(() => {}),
},
};
}
export function Trans({ children }: PropsWithChildren<unknown>) {
return <>{children}</>;
}

View File

@ -30,12 +30,12 @@ export function ReactExample({ text }: ReactExampleProps) {
<div className={styles.redBg}>{text}</div>
<div>
<a href={href} onClick={onClick}>
{t('reactExample.registries.useSref', 'Registries useSref')}
{t('Registries useSref')}
</a>
</div>
<div>
<Link to={route}>
<Trans i18nKey="reactExample.registries.link">
<Trans>
Registries <strong>Link</strong>
</Trans>
</Link>

View File

@ -0,0 +1,54 @@
import userEvent from '@testing-library/user-event';
import { render } from '@/react-tools/test-utils';
import { CreateAccessToken } from './CreateAccessToken';
test('the button is disabled when description is missing and enabled when description is filled', async () => {
const queries = renderComponent();
const button = queries.getByRole('button', { name: 'Add access token' });
expect(button).toBeDisabled();
const descriptionField = queries.getByLabelText('Description');
userEvent.type(descriptionField, 'description');
expect(button).toBeEnabled();
userEvent.clear(descriptionField);
expect(button).toBeDisabled();
});
test('once the button is clicked, the access token is generated and displayed', async () => {
const token = 'a very long access token that should be displayed';
const onSubmit = jest.fn(() => Promise.resolve({ rawAPIKey: token }));
const queries = renderComponent(onSubmit);
const descriptionField = queries.getByLabelText('Description');
userEvent.type(descriptionField, 'description');
const button = queries.getByRole('button', { name: 'Add access token' });
userEvent.click(button);
expect(onSubmit).toHaveBeenCalledWith('description');
expect(onSubmit).toHaveBeenCalledTimes(1);
await expect(queries.findByText('New access token')).resolves.toBeVisible();
expect(queries.getByText(token)).toHaveTextContent(token);
});
function renderComponent(onSubmit = jest.fn()) {
const queries = render(
<CreateAccessToken onSubmit={onSubmit} onError={jest.fn()} />
);
expect(queries.getByLabelText('Description')).toBeVisible();
return queries;
}

View File

@ -18,9 +18,6 @@ interface AccessTokenResponse {
}
export interface Props {
// userId for whom the access token is generated for
userId: number;
// onSubmit dispatches a successful matomo analytics event
onSubmit: (description: string) => Promise<AccessTokenResponse>;
@ -32,7 +29,8 @@ export function CreateAccessToken({
onSubmit,
onError,
}: PropsWithChildren<Props>) {
const { t } = useTranslation();
const translationNS = 'account.accessTokens.create';
const { t } = useTranslation(translationNS);
const router = useRouter();
const [description, setDescription] = useState('');
@ -42,12 +40,7 @@ export function CreateAccessToken({
useEffect(() => {
if (description.length === 0) {
setErrorText(
t(
'users.access-tokens.create.form.description-field.error.required',
'this field is required'
)
);
setErrorText(t('this field is required'));
} else setErrorText('');
}, [description, t]);
@ -73,10 +66,7 @@ export function CreateAccessToken({
<div>
<FormControl
inputId="input"
label={t(
'users.access-tokens.create.form.description-field.label',
'Description'
)}
label={t('Description')}
errors={errorText}
>
<Input
@ -90,36 +80,30 @@ export function CreateAccessToken({
onClick={() => generateAccessToken()}
className={styles.addButton}
>
{t('users.access-tokens.create.add-button', 'Add access token')}
{t('Add access token')}
</Button>
</div>
{accessToken && (
<>
<FormSectionTitle>
<Trans i18nKey="users.access-tokens.create.new-access-token.title">
New access token
</Trans>
<Trans ns={translationNS}>New access token</Trans>
</FormSectionTitle>
<TextTip>
<Trans i18nKey="users.access-tokens.create.new-access-token.explanation">
<Trans ns={translationNS}>
Please copy the new access token. You won&#39;t be able to view
the token again.
</Trans>
</TextTip>
<Code>{accessToken}</Code>
<CopyButton copyText={accessToken} className={styles.copyButton}>
<Trans i18nKey="users.access-tokens.create.new-access-token.copy-button">
Copy access token
</Trans>
<Trans ns={translationNS}>Copy access token</Trans>
</CopyButton>
<hr />
<Button
type="button"
onClick={() => router.stateService.go('portainer.account')}
>
<Trans i18nKey="users.access-tokens.create.done-button">
Done
</Trans>
<Trans ns={translationNS}>Done</Trans>
</Button>
</>
)}

21
app/setup-tests/i18n.ts Normal file
View File

@ -0,0 +1,21 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import translation from '../../translations/en/translation.json';
i18n.use(initReactI18next).init({
lng: 'en',
fallbackLng: 'en',
// have a common namespace used around the full app
ns: ['translationsNS'],
defaultNS: 'translationsNS',
interpolation: {
escapeValue: false,
},
resources: { en: { translationsNS: translation } },
});
export default i18n;

View File

@ -24,6 +24,7 @@
"strictNullChecks": true,
"noUnusedLocals": true,
"removeComments": true,
"resolveJsonModule": true,
// "sourceMap": true,
"lib": ["dom", "dom.iterable", "esnext"],
"paths": {