mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
refactor(ui): fix tests and phrases
This commit is contained in:
parent
7a6f5621c8
commit
df19eba175
25 changed files with 204 additions and 163 deletions
|
@ -1,7 +1,7 @@
|
|||
const list = {
|
||||
or: ' or ', // UNTRANSLATED
|
||||
and: ' and ', // UNTRANSLATED
|
||||
separator: ', ', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
separator: ',', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(list);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const list = {
|
||||
or: ' or ',
|
||||
and: ' and ',
|
||||
separator: ', ',
|
||||
or: 'or',
|
||||
and: 'and',
|
||||
separator: ',',
|
||||
};
|
||||
|
||||
export default Object.freeze(list);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const list = {
|
||||
or: ' or ', // UNTRANSLATED
|
||||
and: ' and ', // UNTRANSLATED
|
||||
separator: ', ', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
separator: ',', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(list);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const list = {
|
||||
or: ' or ', // UNTRANSLATED
|
||||
and: ' and ', // UNTRANSLATED
|
||||
separator: ', ', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
separator: ',', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(list);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const list = {
|
||||
or: ' or ', // UNTRANSLATED
|
||||
and: ' and ', // UNTRANSLATED
|
||||
separator: ', ', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
separator: ',', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(list);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const list = {
|
||||
or: ' or ', // UNTRANSLATED
|
||||
and: ' and ', // UNTRANSLATED
|
||||
separator: ', ', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
separator: ',', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(list);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const list = {
|
||||
or: ' or ', // UNTRANSLATED
|
||||
and: ' and ', // UNTRANSLATED
|
||||
separator: ', ', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
separator: ',', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(list);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const list = {
|
||||
or: ' or ', // UNTRANSLATED
|
||||
and: ' and ', // UNTRANSLATED
|
||||
separator: ', ', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
separator: ',', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(list);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const list = {
|
||||
or: ' or ', // UNTRANSLATED
|
||||
and: ' and ', // UNTRANSLATED
|
||||
separator: ', ', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
separator: ',', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(list);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const list = {
|
||||
or: ' or ', // UNTRANSLATED
|
||||
and: ' and ', // UNTRANSLATED
|
||||
separator: ', ', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
separator: ',', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(list);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const list = {
|
||||
or: ' or ', // UNTRANSLATED
|
||||
and: ' and ', // UNTRANSLATED
|
||||
separator: ', ', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
separator: ',', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(list);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const list = {
|
||||
or: ' or ', // UNTRANSLATED
|
||||
and: ' and ', // UNTRANSLATED
|
||||
separator: ', ', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
separator: ',', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(list);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const list = {
|
||||
or: ' or ', // UNTRANSLATED
|
||||
and: ' and ', // UNTRANSLATED
|
||||
separator: ', ', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
separator: ',', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(list);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const list = {
|
||||
or: ' or ', // UNTRANSLATED
|
||||
and: ' and ', // UNTRANSLATED
|
||||
separator: ', ', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
separator: ',', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(list);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const list = {
|
||||
or: ' or ', // UNTRANSLATED
|
||||
and: ' and ', // UNTRANSLATED
|
||||
separator: ', ', // UNTRANSLATED
|
||||
or: 'or', // UNTRANSLATED
|
||||
and: 'and', // UNTRANSLATED
|
||||
separator: ',', // UNTRANSLATED
|
||||
};
|
||||
|
||||
export default Object.freeze(list);
|
||||
|
|
|
@ -37,44 +37,8 @@ describe('<Lite />', () => {
|
|||
expect(submit).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('password less than 8 chars should throw', async () => {
|
||||
const { queryByText, getByText, container } = render(<Lite onSubmit={submit} />);
|
||||
const submitButton = getByText('action.save_password');
|
||||
const passwordInput = container.querySelector('input[name="newPassword"]');
|
||||
|
||||
if (passwordInput) {
|
||||
fireEvent.change(passwordInput, { target: { value: '1234567' } });
|
||||
}
|
||||
|
||||
act(() => {
|
||||
fireEvent.submit(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByText('error.password_rejected.too_short')).not.toBeNull();
|
||||
expect(queryByText('error.password_rejected.character_types')).not.toBeNull();
|
||||
expect(queryByText('error.password_rejected.sequence')).not.toBeNull();
|
||||
});
|
||||
|
||||
expect(submit).not.toBeCalled();
|
||||
|
||||
act(() => {
|
||||
// Clear error
|
||||
if (passwordInput) {
|
||||
fireEvent.change(passwordInput, { target: { value: '1234asdf' } });
|
||||
fireEvent.blur(passwordInput);
|
||||
}
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByText('error.password_rejected.too_short')).toBeNull();
|
||||
expect(queryByText('error.password_rejected.character_types')).toBeNull();
|
||||
expect(queryByText('error.password_rejected.sequence')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test('should submit properly', async () => {
|
||||
const { queryByText, getByText, container } = render(<Lite onSubmit={submit} />);
|
||||
const { getByText, container } = render(<Lite onSubmit={submit} />);
|
||||
const submitButton = getByText('action.save_password');
|
||||
const passwordInput = container.querySelector('input[name="newPassword"]');
|
||||
|
||||
|
|
|
@ -36,7 +36,6 @@ const Lite = ({ className, autoFocus, onSubmit, errorMessage, clearErrorMessage
|
|||
|
||||
useEffect(() => {
|
||||
if (!isValid) {
|
||||
console.log('!isValid');
|
||||
clearErrorMessage?.();
|
||||
}
|
||||
}, [clearErrorMessage, isValid]);
|
||||
|
|
|
@ -40,74 +40,6 @@ describe('<SetPassword />', () => {
|
|||
expect(submit).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('password less than 8 chars should throw', async () => {
|
||||
const { queryByText, getByText, container } = render(<SetPassword onSubmit={submit} />);
|
||||
const submitButton = getByText('action.save_password');
|
||||
const passwordInput = container.querySelector('input[name="newPassword"]');
|
||||
|
||||
if (passwordInput) {
|
||||
fireEvent.change(passwordInput, { target: { value: '12345' } });
|
||||
}
|
||||
|
||||
act(() => {
|
||||
fireEvent.submit(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByText('error.password_rejected.too_short')).not.toBeNull();
|
||||
expect(queryByText('error.password_rejected.character_types')).not.toBeNull();
|
||||
expect(queryByText('error.password_rejected.sequence')).not.toBeNull();
|
||||
});
|
||||
|
||||
expect(submit).not.toBeCalled();
|
||||
|
||||
act(() => {
|
||||
// Clear error
|
||||
if (passwordInput) {
|
||||
fireEvent.change(passwordInput, { target: { value: '1234asdf' } });
|
||||
fireEvent.blur(passwordInput);
|
||||
}
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByText('error.password_rejected.too_short')).toBeNull();
|
||||
expect(queryByText('error.password_rejected.character_types')).toBeNull();
|
||||
expect(queryByText('error.password_rejected.sequence')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test('password with single type chars should throw', async () => {
|
||||
const { queryByText, getByText, container } = render(<SetPassword onSubmit={submit} />);
|
||||
const submitButton = getByText('action.save_password');
|
||||
const passwordInput = container.querySelector('input[name="newPassword"]');
|
||||
|
||||
if (passwordInput) {
|
||||
fireEvent.change(passwordInput, { target: { value: '12345678' } });
|
||||
}
|
||||
|
||||
act(() => {
|
||||
fireEvent.submit(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByText('error.password_rejected.character_types')).not.toBeNull();
|
||||
expect(queryByText('error.password_rejected.sequence')).not.toBeNull();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
// Clear error
|
||||
if (passwordInput) {
|
||||
fireEvent.change(passwordInput, { target: { value: '1234asdf' } });
|
||||
fireEvent.blur(passwordInput);
|
||||
}
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByText('error.password_rejected.character_types')).toBeNull();
|
||||
expect(queryByText('error.password_rejected.sequence')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test('password mismatch with confirmPassword should throw', async () => {
|
||||
const { queryByText, getByText, container } = render(<SetPassword onSubmit={submit} />);
|
||||
const submitButton = getByText('action.save_password');
|
||||
|
|
|
@ -1,8 +1,39 @@
|
|||
import useListTranslation from './use-list-translation';
|
||||
|
||||
const mockT = jest.fn((key: string) => key);
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useCallback: (function_: () => void) => function_,
|
||||
}));
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: mockT,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useListTranslation (en)', () => {
|
||||
const translateList = useListTranslation();
|
||||
|
||||
beforeAll(() => {
|
||||
mockT.mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'list.or': {
|
||||
return 'or';
|
||||
}
|
||||
case 'list.and': {
|
||||
return 'and';
|
||||
}
|
||||
case 'list.separator': {
|
||||
return ',';
|
||||
}
|
||||
default: {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('returns undefined for an empty list', () => {
|
||||
expect(translateList([])).toBeUndefined();
|
||||
});
|
||||
|
@ -27,6 +58,25 @@ describe('useListTranslation (en)', () => {
|
|||
describe('useListTranslation (zh)', () => {
|
||||
const translateList = useListTranslation();
|
||||
|
||||
beforeAll(() => {
|
||||
mockT.mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'list.or': {
|
||||
return '或';
|
||||
}
|
||||
case 'list.and': {
|
||||
return '和';
|
||||
}
|
||||
case 'list.separator': {
|
||||
return '、';
|
||||
}
|
||||
default: {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('returns undefined for an empty list', () => {
|
||||
expect(translateList([])).toBeUndefined();
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { conditionalArray } from '@silverhand/essentials';
|
||||
import { condString, conditionalArray } from '@silverhand/essentials';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -52,20 +52,22 @@ const useListTranslation = () => {
|
|||
return list[0];
|
||||
}
|
||||
|
||||
const prefix = list.slice(0, -1).join(t('list.separator'));
|
||||
const suffix = list.at(-1)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion -- `list` is not empty
|
||||
const jointT = t(`list.${joint}`);
|
||||
const prefix = list
|
||||
.slice(0, -1)
|
||||
.join(t('list.separator') + condString(!isCjk(jointT) && ' '));
|
||||
const suffix = list.at(-1)!; // eslint-disable-line @typescript-eslint/no-non-null-assertion -- `list` is not empty
|
||||
|
||||
if (!isCjk(jointT)) {
|
||||
if (!isCjk(jointT) && list.length > 2) {
|
||||
// Oxford comma
|
||||
return `${prefix}${t(`list.separator`)}${jointT}${suffix}`;
|
||||
return `${prefix}${t(`list.separator`)} ${jointT} ${suffix}`;
|
||||
}
|
||||
|
||||
return conditionalArray(
|
||||
prefix,
|
||||
isCjk(prefix.at(-1)) && ' ',
|
||||
!isCjk(prefix.at(-1)) && ' ',
|
||||
jointT,
|
||||
isCjk(suffix[0]) && ' ',
|
||||
!isCjk(suffix[0]) && ' ',
|
||||
suffix
|
||||
).join('');
|
||||
},
|
||||
|
|
|
@ -7,10 +7,10 @@ import useErrorHandler, { type ErrorHandlers } from './use-error-handler';
|
|||
import usePasswordErrorMessage from './use-password-error-message';
|
||||
import { usePasswordPolicy } from './use-sie';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- we don't know the type of the api, use `any` to avoid type error
|
||||
export type PasswordAction<Response> = (...args: any[]) => Promise<Response>;
|
||||
export type PasswordAction<Response> = (password: string) => Promise<Response>;
|
||||
|
||||
export type SuccessHandler<F> = F extends PasswordAction<infer Response>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- we don't care about the args type, but `any` is needed for type inference
|
||||
export type SuccessHandler<F> = F extends (...args: any[]) => Promise<infer Response>
|
||||
? (result?: Response) => void
|
||||
: never;
|
||||
|
||||
|
|
|
@ -49,6 +49,41 @@ describe('SetPassword', () => {
|
|||
expect(queryByText('action.save_password')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should show error message when password cannot pass fast check', async () => {
|
||||
const { queryByText, getByText, container } = renderWithPageContext(
|
||||
<SettingsProvider
|
||||
settings={{
|
||||
...mockSignInExperienceSettings,
|
||||
forgotPassword: {
|
||||
email: false,
|
||||
phone: false,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SetPassword />
|
||||
</SettingsProvider>
|
||||
);
|
||||
const submitButton = getByText('action.save_password');
|
||||
const passwordInput = container.querySelector('input[name="newPassword"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirmPassword"]');
|
||||
|
||||
act(() => {
|
||||
if (passwordInput) {
|
||||
fireEvent.change(passwordInput, { target: { value: '1234' } });
|
||||
}
|
||||
|
||||
if (confirmPasswordInput) {
|
||||
fireEvent.change(confirmPasswordInput, { target: { value: '1234' } });
|
||||
}
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByText('error.password_rejected.too_short')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit properly', async () => {
|
||||
const { getByText, container } = renderWithPageContext(
|
||||
<SettingsProvider
|
||||
|
|
|
@ -36,7 +36,7 @@ const SetPassword = () => {
|
|||
}
|
||||
}, []);
|
||||
const { action } = usePasswordAction({
|
||||
api: addProfile,
|
||||
api: async (password) => addProfile({ password }),
|
||||
setErrorMessage,
|
||||
errorHandlers,
|
||||
successHandler,
|
||||
|
|
|
@ -80,7 +80,43 @@ describe('<RegisterPassword />', () => {
|
|||
expect(queryByText('description.not_found')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('submit properly', async () => {
|
||||
it('should show error message when password cannot pass fast check', async () => {
|
||||
const { queryByText, getByText, container } = renderWithPageContext(
|
||||
<SettingsProvider
|
||||
settings={{
|
||||
...mockSignInExperienceSettings,
|
||||
forgotPassword: {
|
||||
email: false,
|
||||
phone: false,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RegisterPassword />
|
||||
</SettingsProvider>
|
||||
);
|
||||
|
||||
const submitButton = getByText('action.save_password');
|
||||
const passwordInput = container.querySelector('input[name="newPassword"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirmPassword"]');
|
||||
|
||||
act(() => {
|
||||
if (passwordInput) {
|
||||
fireEvent.change(passwordInput, { target: { value: '1234' } });
|
||||
}
|
||||
|
||||
if (confirmPasswordInput) {
|
||||
fireEvent.change(confirmPasswordInput, { target: { value: '1234' } });
|
||||
}
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByText('error.password_rejected.too_short')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit properly', async () => {
|
||||
const { getByText, container } = renderWithPageContext(
|
||||
<SettingsProvider
|
||||
settings={{
|
||||
|
|
|
@ -31,6 +31,29 @@ describe('ForgotPassword', () => {
|
|||
expect(queryByText('action.save_password')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('should show error message when password cannot pass fast check', async () => {
|
||||
const { queryByText, getByText, container } = renderWithPageContext(<ResetPassword />);
|
||||
const submitButton = getByText('action.save_password');
|
||||
const passwordInput = container.querySelector('input[name="newPassword"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirmPassword"]');
|
||||
|
||||
act(() => {
|
||||
if (passwordInput) {
|
||||
fireEvent.change(passwordInput, { target: { value: '1234' } });
|
||||
}
|
||||
|
||||
if (confirmPasswordInput) {
|
||||
fireEvent.change(confirmPasswordInput, { target: { value: '1234' } });
|
||||
}
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByText('error.password_rejected.too_short')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test('should submit properly', async () => {
|
||||
const { getByText, container } = renderWithPageContext(<ResetPassword />);
|
||||
const submitButton = getByText('action.save_password');
|
||||
|
|
Loading…
Add table
Reference in a new issue