From df19eba175a993e0da5575ee9180464f1409a318 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Tue, 5 Sep 2023 16:16:59 +0800 Subject: [PATCH] refactor(ui): fix tests and phrases --- packages/phrases-ui/src/locales/de/list.ts | 6 +- packages/phrases-ui/src/locales/en/list.ts | 6 +- packages/phrases-ui/src/locales/es/list.ts | 6 +- packages/phrases-ui/src/locales/fr/list.ts | 6 +- packages/phrases-ui/src/locales/it/list.ts | 6 +- packages/phrases-ui/src/locales/ja/list.ts | 6 +- packages/phrases-ui/src/locales/ko/list.ts | 6 +- packages/phrases-ui/src/locales/pl-pl/list.ts | 6 +- packages/phrases-ui/src/locales/pt-br/list.ts | 6 +- packages/phrases-ui/src/locales/pt-pt/list.ts | 6 +- packages/phrases-ui/src/locales/ru/list.ts | 6 +- packages/phrases-ui/src/locales/tr-tr/list.ts | 6 +- packages/phrases-ui/src/locales/zh-cn/list.ts | 6 +- packages/phrases-ui/src/locales/zh-hk/list.ts | 6 +- packages/phrases-ui/src/locales/zh-tw/list.ts | 6 +- .../src/containers/SetPassword/Lite.test.tsx | 38 +---------- .../ui/src/containers/SetPassword/Lite.tsx | 1 - .../SetPassword/SetPassword.test.tsx | 68 ------------------- .../ui/src/hooks/use-list-translation.test.ts | 50 ++++++++++++++ packages/ui/src/hooks/use-list-translation.ts | 16 +++-- packages/ui/src/hooks/use-password-action.ts | 6 +- .../pages/Continue/SetPassword/index.test.tsx | 35 ++++++++++ .../src/pages/Continue/SetPassword/index.tsx | 2 +- .../src/pages/RegisterPassword/index.test.tsx | 38 ++++++++++- .../ui/src/pages/ResetPassword/index.test.tsx | 23 +++++++ 25 files changed, 204 insertions(+), 163 deletions(-) diff --git a/packages/phrases-ui/src/locales/de/list.ts b/packages/phrases-ui/src/locales/de/list.ts index f595571dd..4b93b3304 100644 --- a/packages/phrases-ui/src/locales/de/list.ts +++ b/packages/phrases-ui/src/locales/de/list.ts @@ -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); diff --git a/packages/phrases-ui/src/locales/en/list.ts b/packages/phrases-ui/src/locales/en/list.ts index 880b2a789..2d758ab5d 100644 --- a/packages/phrases-ui/src/locales/en/list.ts +++ b/packages/phrases-ui/src/locales/en/list.ts @@ -1,7 +1,7 @@ const list = { - or: ' or ', - and: ' and ', - separator: ', ', + or: 'or', + and: 'and', + separator: ',', }; export default Object.freeze(list); diff --git a/packages/phrases-ui/src/locales/es/list.ts b/packages/phrases-ui/src/locales/es/list.ts index f595571dd..4b93b3304 100644 --- a/packages/phrases-ui/src/locales/es/list.ts +++ b/packages/phrases-ui/src/locales/es/list.ts @@ -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); diff --git a/packages/phrases-ui/src/locales/fr/list.ts b/packages/phrases-ui/src/locales/fr/list.ts index f595571dd..4b93b3304 100644 --- a/packages/phrases-ui/src/locales/fr/list.ts +++ b/packages/phrases-ui/src/locales/fr/list.ts @@ -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); diff --git a/packages/phrases-ui/src/locales/it/list.ts b/packages/phrases-ui/src/locales/it/list.ts index f595571dd..4b93b3304 100644 --- a/packages/phrases-ui/src/locales/it/list.ts +++ b/packages/phrases-ui/src/locales/it/list.ts @@ -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); diff --git a/packages/phrases-ui/src/locales/ja/list.ts b/packages/phrases-ui/src/locales/ja/list.ts index f595571dd..4b93b3304 100644 --- a/packages/phrases-ui/src/locales/ja/list.ts +++ b/packages/phrases-ui/src/locales/ja/list.ts @@ -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); diff --git a/packages/phrases-ui/src/locales/ko/list.ts b/packages/phrases-ui/src/locales/ko/list.ts index f595571dd..4b93b3304 100644 --- a/packages/phrases-ui/src/locales/ko/list.ts +++ b/packages/phrases-ui/src/locales/ko/list.ts @@ -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); diff --git a/packages/phrases-ui/src/locales/pl-pl/list.ts b/packages/phrases-ui/src/locales/pl-pl/list.ts index f595571dd..4b93b3304 100644 --- a/packages/phrases-ui/src/locales/pl-pl/list.ts +++ b/packages/phrases-ui/src/locales/pl-pl/list.ts @@ -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); diff --git a/packages/phrases-ui/src/locales/pt-br/list.ts b/packages/phrases-ui/src/locales/pt-br/list.ts index f595571dd..4b93b3304 100644 --- a/packages/phrases-ui/src/locales/pt-br/list.ts +++ b/packages/phrases-ui/src/locales/pt-br/list.ts @@ -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); diff --git a/packages/phrases-ui/src/locales/pt-pt/list.ts b/packages/phrases-ui/src/locales/pt-pt/list.ts index f595571dd..4b93b3304 100644 --- a/packages/phrases-ui/src/locales/pt-pt/list.ts +++ b/packages/phrases-ui/src/locales/pt-pt/list.ts @@ -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); diff --git a/packages/phrases-ui/src/locales/ru/list.ts b/packages/phrases-ui/src/locales/ru/list.ts index f595571dd..4b93b3304 100644 --- a/packages/phrases-ui/src/locales/ru/list.ts +++ b/packages/phrases-ui/src/locales/ru/list.ts @@ -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); diff --git a/packages/phrases-ui/src/locales/tr-tr/list.ts b/packages/phrases-ui/src/locales/tr-tr/list.ts index f595571dd..4b93b3304 100644 --- a/packages/phrases-ui/src/locales/tr-tr/list.ts +++ b/packages/phrases-ui/src/locales/tr-tr/list.ts @@ -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); diff --git a/packages/phrases-ui/src/locales/zh-cn/list.ts b/packages/phrases-ui/src/locales/zh-cn/list.ts index f595571dd..4b93b3304 100644 --- a/packages/phrases-ui/src/locales/zh-cn/list.ts +++ b/packages/phrases-ui/src/locales/zh-cn/list.ts @@ -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); diff --git a/packages/phrases-ui/src/locales/zh-hk/list.ts b/packages/phrases-ui/src/locales/zh-hk/list.ts index f595571dd..4b93b3304 100644 --- a/packages/phrases-ui/src/locales/zh-hk/list.ts +++ b/packages/phrases-ui/src/locales/zh-hk/list.ts @@ -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); diff --git a/packages/phrases-ui/src/locales/zh-tw/list.ts b/packages/phrases-ui/src/locales/zh-tw/list.ts index f595571dd..4b93b3304 100644 --- a/packages/phrases-ui/src/locales/zh-tw/list.ts +++ b/packages/phrases-ui/src/locales/zh-tw/list.ts @@ -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); diff --git a/packages/ui/src/containers/SetPassword/Lite.test.tsx b/packages/ui/src/containers/SetPassword/Lite.test.tsx index cfa0715b2..2daa21fc1 100644 --- a/packages/ui/src/containers/SetPassword/Lite.test.tsx +++ b/packages/ui/src/containers/SetPassword/Lite.test.tsx @@ -37,44 +37,8 @@ describe('', () => { expect(submit).not.toBeCalled(); }); - test('password less than 8 chars should throw', async () => { - const { queryByText, getByText, container } = render(); - 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(); + const { getByText, container } = render(); const submitButton = getByText('action.save_password'); const passwordInput = container.querySelector('input[name="newPassword"]'); diff --git a/packages/ui/src/containers/SetPassword/Lite.tsx b/packages/ui/src/containers/SetPassword/Lite.tsx index b7dfba50e..f99072047 100644 --- a/packages/ui/src/containers/SetPassword/Lite.tsx +++ b/packages/ui/src/containers/SetPassword/Lite.tsx @@ -36,7 +36,6 @@ const Lite = ({ className, autoFocus, onSubmit, errorMessage, clearErrorMessage useEffect(() => { if (!isValid) { - console.log('!isValid'); clearErrorMessage?.(); } }, [clearErrorMessage, isValid]); diff --git a/packages/ui/src/containers/SetPassword/SetPassword.test.tsx b/packages/ui/src/containers/SetPassword/SetPassword.test.tsx index d47855752..4e962c182 100644 --- a/packages/ui/src/containers/SetPassword/SetPassword.test.tsx +++ b/packages/ui/src/containers/SetPassword/SetPassword.test.tsx @@ -40,74 +40,6 @@ describe('', () => { expect(submit).not.toBeCalled(); }); - test('password less than 8 chars should throw', async () => { - const { queryByText, getByText, container } = render(); - 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(); - 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(); const submitButton = getByText('action.save_password'); diff --git a/packages/ui/src/hooks/use-list-translation.test.ts b/packages/ui/src/hooks/use-list-translation.test.ts index f3a2a0c09..b7a98ea5e 100644 --- a/packages/ui/src/hooks/use-list-translation.test.ts +++ b/packages/ui/src/hooks/use-list-translation.test.ts @@ -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(); }); diff --git a/packages/ui/src/hooks/use-list-translation.ts b/packages/ui/src/hooks/use-list-translation.ts index d9b8bcbeb..2b1410afd 100644 --- a/packages/ui/src/hooks/use-list-translation.ts +++ b/packages/ui/src/hooks/use-list-translation.ts @@ -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(''); }, diff --git a/packages/ui/src/hooks/use-password-action.ts b/packages/ui/src/hooks/use-password-action.ts index 52c98b7df..9432308a6 100644 --- a/packages/ui/src/hooks/use-password-action.ts +++ b/packages/ui/src/hooks/use-password-action.ts @@ -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 = (...args: any[]) => Promise; +export type PasswordAction = (password: string) => Promise; -export type SuccessHandler = F extends PasswordAction +// 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 extends (...args: any[]) => Promise ? (result?: Response) => void : never; diff --git a/packages/ui/src/pages/Continue/SetPassword/index.test.tsx b/packages/ui/src/pages/Continue/SetPassword/index.test.tsx index 607e66e06..2b3ba470c 100644 --- a/packages/ui/src/pages/Continue/SetPassword/index.test.tsx +++ b/packages/ui/src/pages/Continue/SetPassword/index.test.tsx @@ -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( + + + + ); + 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( { } }, []); const { action } = usePasswordAction({ - api: addProfile, + api: async (password) => addProfile({ password }), setErrorMessage, errorHandlers, successHandler, diff --git a/packages/ui/src/pages/RegisterPassword/index.test.tsx b/packages/ui/src/pages/RegisterPassword/index.test.tsx index f2c70c8ca..81aef9f3c 100644 --- a/packages/ui/src/pages/RegisterPassword/index.test.tsx +++ b/packages/ui/src/pages/RegisterPassword/index.test.tsx @@ -80,7 +80,43 @@ describe('', () => { 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( + + + + ); + + 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( { expect(queryByText('action.save_password')).not.toBeNull(); }); + test('should show error message when password cannot pass fast check', async () => { + const { queryByText, getByText, container } = renderWithPageContext(); + 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(); const submitButton = getByText('action.save_password');