mirror of https://github.com/portainer/portainer
chore(account): write tests for CreateAccessToken [EE-2561] (#6578)
parent
b7d18ef50f
commit
f1ea2b5c02
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
|
@ -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}</>;
|
||||||
|
}
|
|
@ -30,12 +30,12 @@ export function ReactExample({ text }: ReactExampleProps) {
|
||||||
<div className={styles.redBg}>{text}</div>
|
<div className={styles.redBg}>{text}</div>
|
||||||
<div>
|
<div>
|
||||||
<a href={href} onClick={onClick}>
|
<a href={href} onClick={onClick}>
|
||||||
{t('reactExample.registries.useSref', 'Registries useSref')}
|
{t('Registries useSref')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Link to={route}>
|
<Link to={route}>
|
||||||
<Trans i18nKey="reactExample.registries.link">
|
<Trans>
|
||||||
Registries <strong>Link</strong>
|
Registries <strong>Link</strong>
|
||||||
</Trans>
|
</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -18,9 +18,6 @@ interface AccessTokenResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
// userId for whom the access token is generated for
|
|
||||||
userId: number;
|
|
||||||
|
|
||||||
// onSubmit dispatches a successful matomo analytics event
|
// onSubmit dispatches a successful matomo analytics event
|
||||||
onSubmit: (description: string) => Promise<AccessTokenResponse>;
|
onSubmit: (description: string) => Promise<AccessTokenResponse>;
|
||||||
|
|
||||||
|
@ -32,7 +29,8 @@ export function CreateAccessToken({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onError,
|
onError,
|
||||||
}: PropsWithChildren<Props>) {
|
}: PropsWithChildren<Props>) {
|
||||||
const { t } = useTranslation();
|
const translationNS = 'account.accessTokens.create';
|
||||||
|
const { t } = useTranslation(translationNS);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
|
@ -42,12 +40,7 @@ export function CreateAccessToken({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (description.length === 0) {
|
if (description.length === 0) {
|
||||||
setErrorText(
|
setErrorText(t('this field is required'));
|
||||||
t(
|
|
||||||
'users.access-tokens.create.form.description-field.error.required',
|
|
||||||
'this field is required'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else setErrorText('');
|
} else setErrorText('');
|
||||||
}, [description, t]);
|
}, [description, t]);
|
||||||
|
|
||||||
|
@ -73,10 +66,7 @@ export function CreateAccessToken({
|
||||||
<div>
|
<div>
|
||||||
<FormControl
|
<FormControl
|
||||||
inputId="input"
|
inputId="input"
|
||||||
label={t(
|
label={t('Description')}
|
||||||
'users.access-tokens.create.form.description-field.label',
|
|
||||||
'Description'
|
|
||||||
)}
|
|
||||||
errors={errorText}
|
errors={errorText}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
@ -90,36 +80,30 @@ export function CreateAccessToken({
|
||||||
onClick={() => generateAccessToken()}
|
onClick={() => generateAccessToken()}
|
||||||
className={styles.addButton}
|
className={styles.addButton}
|
||||||
>
|
>
|
||||||
{t('users.access-tokens.create.add-button', 'Add access token')}
|
{t('Add access token')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{accessToken && (
|
{accessToken && (
|
||||||
<>
|
<>
|
||||||
<FormSectionTitle>
|
<FormSectionTitle>
|
||||||
<Trans i18nKey="users.access-tokens.create.new-access-token.title">
|
<Trans ns={translationNS}>New access token</Trans>
|
||||||
New access token
|
|
||||||
</Trans>
|
|
||||||
</FormSectionTitle>
|
</FormSectionTitle>
|
||||||
<TextTip>
|
<TextTip>
|
||||||
<Trans i18nKey="users.access-tokens.create.new-access-token.explanation">
|
<Trans ns={translationNS}>
|
||||||
Please copy the new access token. You won't be able to view
|
Please copy the new access token. You won't be able to view
|
||||||
the token again.
|
the token again.
|
||||||
</Trans>
|
</Trans>
|
||||||
</TextTip>
|
</TextTip>
|
||||||
<Code>{accessToken}</Code>
|
<Code>{accessToken}</Code>
|
||||||
<CopyButton copyText={accessToken} className={styles.copyButton}>
|
<CopyButton copyText={accessToken} className={styles.copyButton}>
|
||||||
<Trans i18nKey="users.access-tokens.create.new-access-token.copy-button">
|
<Trans ns={translationNS}>Copy access token</Trans>
|
||||||
Copy access token
|
|
||||||
</Trans>
|
|
||||||
</CopyButton>
|
</CopyButton>
|
||||||
<hr />
|
<hr />
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.stateService.go('portainer.account')}
|
onClick={() => router.stateService.go('portainer.account')}
|
||||||
>
|
>
|
||||||
<Trans i18nKey="users.access-tokens.create.done-button">
|
<Trans ns={translationNS}>Done</Trans>
|
||||||
Done
|
|
||||||
</Trans>
|
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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;
|
|
@ -24,6 +24,7 @@
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
// "sourceMap": true,
|
// "sourceMap": true,
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|
Loading…
Reference in New Issue