0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

refactor(ui): refactor ui terms of use flow (#1259)

* refactor(ui): refactor ui terms of use flow

refactor ui terms of use flow

* test(terms): add ut

add ut
This commit is contained in:
simeng-li 2022-06-28 09:15:44 +08:00 committed by GitHub
parent 6148bb0add
commit f765ccf8cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 241 additions and 108 deletions

View file

@ -135,7 +135,7 @@ export const mockSignInExperience: SignInExperience = {
},
termsOfUse: {
enabled: true,
contentUrl: 'http://terms.of.use',
contentUrl: 'http://terms.of.use/',
},
languageInfo: {
autoDetect: true,

View file

@ -1,6 +1,7 @@
import { render } from '@testing-library/react';
import React from 'react';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import TermsOfUseModal from '.';
describe('TermsOfUseModal', () => {
@ -8,13 +9,8 @@ describe('TermsOfUseModal', () => {
const onCancel = jest.fn();
it('render properly', () => {
const { queryByText } = render(
<TermsOfUseModal
isOpen
termsUrl="https://www.google.com"
onConfirm={onConfirm}
onClose={onCancel}
/>
const { queryByText } = renderWithPageContext(
<TermsOfUseModal isOpen onConfirm={onConfirm} onClose={onCancel} />
);
expect(queryByText('description.agree_with_terms_modal')).not.toBeNull();

View file

@ -1,9 +1,15 @@
import React, { ReactNode } from 'react';
import React, { ReactNode, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { create } from 'react-modal-promise';
import reactStringReplace from 'react-string-replace';
import { WebModal as ConfirmModal } from '@/components/ConfirmModal';
import { WebModal, MobileModal } from '@/components/ConfirmModal';
import TextLink from '@/components/TextLink';
import { PageContext } from '@/hooks/use-page-context';
import usePlatform from '@/hooks/use-platform';
import { TermsOfUseModalMessage } from '@/types';
import { modalPromisify } from '../termsOfUseModalPromisify';
/**
* For web use only confirm modal, does not contain Terms iframe
@ -12,25 +18,43 @@ import TextLink from '@/components/TextLink';
type Props = {
isOpen?: boolean;
onConfirm: () => void;
onClose: () => void;
termsUrl: string;
onClose: (message?: TermsOfUseModalMessage) => void;
};
const TermsOfUseConfirmModal = ({ isOpen = false, termsUrl, onConfirm, onClose }: Props) => {
const TermsOfUseConfirmModal = ({ isOpen = false, onConfirm, onClose }: Props) => {
const { t } = useTranslation();
const { isMobile } = usePlatform();
const { setTermsAgreement, experienceSettings } = useContext(PageContext);
const { termsOfUse } = experienceSettings ?? {};
const ConfirmModal = isMobile ? MobileModal : WebModal;
const terms = t('description.terms_of_use');
const content = t('description.agree_with_terms_modal', { terms });
const linkProps = isMobile
? {
onClick: () => {
onClose(TermsOfUseModalMessage.SHOW_DETAIL_MODAL);
},
}
: {
href: termsOfUse?.contentUrl,
target: '_blank',
};
const modalContent: ReactNode = reactStringReplace(content, terms, () => (
<TextLink key={terms} text="description.terms_of_use" href={termsUrl} target="_blank" />
<TextLink key={terms} text="description.terms_of_use" {...linkProps} />
));
return (
<ConfirmModal
isOpen={isOpen}
confirmText="action.agree"
onConfirm={onConfirm}
onConfirm={() => {
setTermsAgreement(true);
onConfirm();
}}
onClose={onClose}
>
{modalContent}
@ -39,3 +63,5 @@ const TermsOfUseConfirmModal = ({ isOpen = false, termsUrl, onConfirm, onClose }
};
export default TermsOfUseConfirmModal;
export const termsOfUseConfirmModalPromise = create(modalPromisify(TermsOfUseConfirmModal));

View file

@ -1,6 +1,10 @@
import { render, screen } from '@testing-library/react';
import { screen, fireEvent } from '@testing-library/react';
import React from 'react';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
import { mockSignInExperienceSettings } from '@/__mocks__/logto';
import TermsOfUseIframeModal from '.';
describe('TermsOfUseModal', () => {
@ -8,13 +12,10 @@ describe('TermsOfUseModal', () => {
const onCancel = jest.fn();
it('render properly', () => {
const { queryByText } = render(
<TermsOfUseIframeModal
isOpen
termsUrl="https://www.google.com/"
onConfirm={onConfirm}
onClose={onCancel}
/>
const { queryByText, getByText } = renderWithPageContext(
<SettingsProvider>
<TermsOfUseIframeModal isOpen onConfirm={onConfirm} onClose={onCancel} />
</SettingsProvider>
);
expect(queryByText('action.agree')).not.toBeNull();
@ -24,7 +25,13 @@ describe('TermsOfUseModal', () => {
expect(iframe).not.toBeNull();
if (iframe) {
expect(iframe).toHaveProperty('src', 'https://www.google.com/');
expect(iframe).toHaveProperty('src', mockSignInExperienceSettings.termsOfUse.contentUrl);
}
const confirmButton = getByText('action.agree');
fireEvent.click(confirmButton);
expect(onConfirm).toBeCalled();
});
});

View file

@ -1,6 +1,10 @@
import React from 'react';
import React, { useContext } from 'react';
import { create } from 'react-modal-promise';
import { IframeModal } from '@/components/ConfirmModal';
import { PageContext } from '@/hooks/use-page-context';
import { modalPromisify } from '../termsOfUseModalPromisify';
/**
* For mobile use only, includes embedded Terms iframe
@ -9,19 +13,26 @@ type Props = {
isOpen?: boolean;
onConfirm: () => void;
onClose: () => void;
termsUrl: string;
};
const TermsOfUseIframeModal = ({ isOpen = false, termsUrl, onConfirm, onClose }: Props) => {
const TermsOfUseIframeModal = ({ isOpen = false, onConfirm, onClose }: Props) => {
const { setTermsAgreement, experienceSettings } = useContext(PageContext);
const { termsOfUse } = experienceSettings ?? {};
return (
<IframeModal
isOpen={isOpen}
confirmText="action.agree"
url={termsUrl}
onConfirm={onConfirm}
url={termsOfUse?.contentUrl ?? ''}
onConfirm={() => {
setTermsAgreement(true);
onConfirm();
}}
onClose={onClose}
/>
);
};
export default TermsOfUseIframeModal;
export const termsOfUseIframeModalPromise = create(modalPromisify(TermsOfUseIframeModal));

View file

@ -1,36 +0,0 @@
import { fireEvent } from '@testing-library/react';
import React from 'react';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
import TermsOfUsePromiseModal from '.';
describe('TermsOfUsePromiseModal', () => {
it('render properly', () => {
const onResolve = jest.fn();
const onReject = jest.fn();
const { getByText } = renderWithPageContext(
<SettingsProvider>
<TermsOfUsePromiseModal
isOpen
open
instanceId="1"
close={jest.fn()}
onReject={onReject}
onResolve={onResolve}
/>
</SettingsProvider>
);
const confirmButton = getByText('action.agree');
const cancelButton = getByText('action.cancel');
fireEvent.click(confirmButton);
expect(onResolve).toBeCalledWith(true);
fireEvent.click(cancelButton);
expect(onReject).toBeCalledWith(false);
});
});

View file

@ -1,34 +0,0 @@
import React, { useContext } from 'react';
import { create, InstanceProps } from 'react-modal-promise';
import { PageContext } from '@/hooks/use-page-context';
import usePlatform from '@/hooks/use-platform';
import TermsOfUseConfirmModal from '../TermsOfUseConfirmModal';
import TermsOfUseIframeModal from '../TermsOfUseIframeModal';
const TermsOfUsePromiseModal = ({ isOpen, onResolve, onReject }: InstanceProps<boolean>) => {
const { setTermsAgreement, experienceSettings } = useContext(PageContext);
const { termsOfUse } = experienceSettings ?? {};
const { isMobile } = usePlatform();
const ConfirmModal = isMobile ? TermsOfUseIframeModal : TermsOfUseConfirmModal;
return (
<ConfirmModal
isOpen={isOpen}
termsUrl={termsOfUse?.contentUrl ?? ''}
onConfirm={() => {
setTermsAgreement(true);
onResolve(true);
}}
onClose={() => {
onReject(false);
}}
/>
);
};
export default TermsOfUsePromiseModal;
export const termsOfUseModalPromise = create(TermsOfUsePromiseModal);

View file

@ -10,7 +10,8 @@ type Props = {
};
const TermsOfUse = ({ className }: Props) => {
const { termsAgreement, setTermsAgreement, termsSettings, termsOfUserModalHandler } = useTerms();
const { termsAgreement, setTermsAgreement, termsSettings, termsOfUseIframeModalHandler } =
useTerms();
const { isMobile } = usePlatform();
if (!termsSettings?.enabled || !termsSettings.contentUrl) {
@ -27,7 +28,7 @@ const TermsOfUse = ({ className }: Props) => {
onChange={(checked) => {
setTermsAgreement(checked);
}}
onTermsClick={isMobile ? termsOfUserModalHandler : undefined}
onTermsClick={isMobile ? termsOfUseIframeModalHandler : undefined}
/>
<ModalContainer />
</>

View file

@ -0,0 +1,38 @@
import { fireEvent } from '@testing-library/react';
import React from 'react';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { modalPromisify } from '.';
import TermsOfUseConfirmModal from '../TermsOfUseConfirmModal';
describe('modalPromisify', () => {
const onResolve = jest.fn();
const onReject = jest.fn();
it('render properly', () => {
const PromisifyModal = modalPromisify(TermsOfUseConfirmModal);
const { getByText } = renderWithPageContext(
<PromisifyModal isOpen instanceId="foo" onResolve={onResolve} onReject={onReject} />
);
const confirmButton = getByText('action.agree');
fireEvent.click(confirmButton);
expect(onResolve).toBeCalled();
});
it('reject with message', () => {
const PromisifyModal = modalPromisify(TermsOfUseConfirmModal);
const { getByText } = renderWithPageContext(
<PromisifyModal isOpen instanceId="foo" onResolve={onResolve} onReject={onReject} />
);
const cancelButton = getByText('action.cancel');
fireEvent.click(cancelButton);
expect(onReject).toBeCalled();
});
});

View file

@ -0,0 +1,30 @@
import React from 'react';
import { InstanceProps } from 'react-modal-promise';
import { TermsOfUseModalMessage } from '@/types';
type Props = {
isOpen?: boolean;
onConfirm: () => void;
onClose: (message?: TermsOfUseModalMessage) => void;
};
export const modalPromisify =
(ConfirmModal: (props: Props) => JSX.Element) =>
({
isOpen,
onResolve,
onReject,
}: Omit<InstanceProps<boolean | TermsOfUseModalMessage>, 'open' | 'close'>) => {
return (
<ConfirmModal
isOpen={isOpen}
onConfirm={() => {
onResolve(true);
}}
onClose={(message?: TermsOfUseModalMessage) => {
onReject(message ?? false);
}}
/>
);
};

View file

@ -4,12 +4,29 @@ import React from 'react';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
import { signInBasic } from '@/apis/sign-in';
import { termsOfUseConfirmModalPromise } from '@/containers/TermsOfUse/TermsOfUseConfirmModal';
import { termsOfUseIframeModalPromise } from '@/containers/TermsOfUse/TermsOfUseIframeModal';
import { TermsOfUseModalMessage } from '@/types';
import UsernameSignin from '.';
jest.mock('@/apis/sign-in', () => ({ signInBasic: jest.fn(async () => Promise.resolve()) }));
jest.mock('@/containers/TermsOfUse/TermsOfUseConfirmModal', () => ({
termsOfUseConfirmModalPromise: jest.fn().mockResolvedValue(true),
}));
jest.mock('@/containers/TermsOfUse/TermsOfUseIframeModal', () => ({
termsOfUseIframeModalPromise: jest.fn().mockResolvedValue(true),
}));
const termsOfUseConfirmModalPromiseMock = termsOfUseConfirmModalPromise as jest.Mock;
const termsOfUseIframeModalPromiseMock = termsOfUseIframeModalPromise as jest.Mock;
describe('<UsernameSignin>', () => {
afterEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
});
test('render', () => {
const { queryByText, container } = renderWithPageContext(<UsernameSignin />);
expect(container.querySelector('input[name="username"]')).not.toBeNull();
@ -52,6 +69,60 @@ describe('<UsernameSignin>', () => {
expect(queryByText('required')).toBeNull();
});
test('should show terms confirm modal', async () => {
const { getByText, container } = renderWithPageContext(
<SettingsProvider>
<UsernameSignin />
</SettingsProvider>
);
const submitButton = getByText('action.sign_in');
const usernameInput = container.querySelector('input[name="username"]');
const passwordInput = container.querySelector('input[name="password"]');
if (usernameInput) {
fireEvent.change(usernameInput, { target: { value: 'username' } });
}
if (passwordInput) {
fireEvent.change(passwordInput, { target: { value: 'password' } });
}
await waitFor(() => {
fireEvent.click(submitButton);
});
expect(signInBasic).toBeCalledWith('username', 'password', undefined);
});
test('should show terms detail modal', async () => {
termsOfUseConfirmModalPromiseMock.mockRejectedValue(TermsOfUseModalMessage.SHOW_DETAIL_MODAL);
const { getByText, container } = renderWithPageContext(
<SettingsProvider>
<UsernameSignin />
</SettingsProvider>
);
const submitButton = getByText('action.sign_in');
const usernameInput = container.querySelector('input[name="username"]');
const passwordInput = container.querySelector('input[name="password"]');
if (usernameInput) {
fireEvent.change(usernameInput, { target: { value: 'username' } });
}
if (passwordInput) {
fireEvent.change(passwordInput, { target: { value: 'password' } });
}
await waitFor(() => {
fireEvent.click(submitButton);
});
expect(termsOfUseIframeModalPromiseMock).toBeCalledWith();
});
test('submit form', async () => {
const { getByText, container } = renderWithPageContext(
<SettingsProvider>

View file

@ -1,6 +1,8 @@
import { useContext, useCallback } from 'react';
import { termsOfUseModalPromise } from '@/containers/TermsOfUse/TermsOfUsePromiseModal';
import { termsOfUseConfirmModalPromise } from '@/containers/TermsOfUse/TermsOfUseConfirmModal';
import { termsOfUseIframeModalPromise } from '@/containers/TermsOfUse/TermsOfUseIframeModal';
import { TermsOfUseModalMessage } from '@/types';
import { PageContext } from './use-page-context';
@ -9,9 +11,9 @@ const useTerms = () => {
const { termsOfUse } = experienceSettings ?? {};
const termsOfUserModalHandler = useCallback(async () => {
const termsOfUseIframeModalHandler = useCallback(async () => {
try {
await termsOfUseModalPromise();
await termsOfUseIframeModalPromise();
return true;
} catch {
@ -19,20 +21,37 @@ const useTerms = () => {
}
}, []);
const termsOfUseConfirmModalHandler = useCallback(async () => {
try {
await termsOfUseConfirmModalPromise();
return true;
} catch (error: unknown) {
if (error === TermsOfUseModalMessage.SHOW_DETAIL_MODAL) {
const result = await termsOfUseIframeModalHandler();
return result;
}
return false;
}
}, [termsOfUseIframeModalHandler]);
const termsValidation = useCallback(async () => {
if (termsAgreement || !termsOfUse?.enabled || !termsOfUse.contentUrl) {
return true;
}
return termsOfUserModalHandler();
}, [termsAgreement, termsOfUse, termsOfUserModalHandler]);
return termsOfUseConfirmModalHandler();
}, [termsAgreement, termsOfUse, termsOfUseConfirmModalHandler]);
return {
termsSettings: termsOfUse,
termsAgreement,
termsValidation,
setTermsAgreement,
termsOfUserModalHandler,
termsOfUseConfirmModalHandler,
termsOfUseIframeModalHandler,
};
};

View file

@ -30,3 +30,7 @@ export type SignInExperienceSettings = Omit<
primarySignInMethod: SignInMethod;
secondarySignInMethods: SignInMethod[];
};
export enum TermsOfUseModalMessage {
SHOW_DETAIL_MODAL = 'SHOW_DETAIL_MODAL',
}