mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
refactor(ui): update global page context (#599)
* refactor(ui): update global page context add usePageContext hook * fix(ui): cr fix cr fix
This commit is contained in:
parent
2fa20363be
commit
a04f472fe0
23 changed files with 163 additions and 156 deletions
|
@ -1,8 +1,8 @@
|
|||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Route, Routes, BrowserRouter, Navigate } from 'react-router-dom';
|
||||
|
||||
import AppContent from './components/AppContent';
|
||||
import PageContext from './hooks/page-context';
|
||||
import usePageContext from './hooks/use-page-context';
|
||||
import initI18n from './i18n/init';
|
||||
import Callback from './pages/Callback';
|
||||
import Consent from './pages/Consent';
|
||||
|
@ -10,20 +10,13 @@ import Passcode from './pages/Passcode';
|
|||
import Register from './pages/Register';
|
||||
import SecondarySignIn from './pages/SecondarySignIn';
|
||||
import SignIn from './pages/SignIn';
|
||||
import { SignInExperienceSettings } from './types';
|
||||
import getSignInExperienceSettings from './utils/sign-in-experience';
|
||||
|
||||
import './scss/normalized.scss';
|
||||
|
||||
const App = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [toast, setToast] = useState('');
|
||||
const [experienceSettings, setExperienceSettings] = useState<SignInExperienceSettings>();
|
||||
|
||||
const context = useMemo(
|
||||
() => ({ toast, loading, experienceSettings, setLoading, setToast, setExperienceSettings }),
|
||||
[experienceSettings, loading, toast]
|
||||
);
|
||||
const { context, Provider } = usePageContext();
|
||||
const { experienceSettings, setLoading, setExperienceSettings } = context;
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
@ -37,14 +30,14 @@ const App = () => {
|
|||
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
}, [setExperienceSettings, setLoading]);
|
||||
|
||||
if (!experienceSettings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContext.Provider value={context}>
|
||||
<Provider value={context}>
|
||||
<AppContent>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
|
@ -61,7 +54,7 @@ const App = () => {
|
|||
</Routes>
|
||||
</BrowserRouter>
|
||||
</AppContent>
|
||||
</PageContext.Provider>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
|
||||
import PageContext from '@/hooks/page-context';
|
||||
import { SignInExperienceSettings } from '@/types';
|
||||
|
||||
const ContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [toast, setToast] = useState('');
|
||||
const [experienceSettings, setExperienceSettings] = useState<SignInExperienceSettings>();
|
||||
|
||||
const context = useMemo(
|
||||
() => ({ loading, setLoading, toast, setToast, experienceSettings, setExperienceSettings }),
|
||||
[experienceSettings, loading, toast]
|
||||
);
|
||||
|
||||
return <PageContext.Provider value={context}>{children}</PageContext.Provider>;
|
||||
};
|
||||
|
||||
export default ContextProvider;
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
import usePageContext from '@/hooks/use-page-context';
|
||||
|
||||
const ContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const { context, Provider } = usePageContext();
|
||||
|
||||
return <Provider value={context}>{children}</Provider>;
|
||||
};
|
||||
|
||||
export default ContextProvider;
|
14
packages/ui/src/__mocks__/RenderWithPageContext/index.tsx
Normal file
14
packages/ui/src/__mocks__/RenderWithPageContext/index.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { render, Queries, queries, RenderOptions } from '@testing-library/react';
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
import ContextProvider from './ContextProvider';
|
||||
|
||||
const renderWithPageContext = <
|
||||
Q extends Queries = typeof queries,
|
||||
Container extends Element | DocumentFragment = HTMLElement
|
||||
>(
|
||||
ui: ReactElement,
|
||||
options: RenderOptions<Q, Container> = {}
|
||||
) => render<Q, Container>(<ContextProvider>{ui}</ContextProvider>, options);
|
||||
|
||||
export default renderWithPageContext;
|
|
@ -42,7 +42,7 @@ $font-family-small: 'PingFang SC', 'SF Pro Text', 'Siyuan Heiti', 'Roboto';
|
|||
--color-layer: linear-gradient(0deg, rgba(202, 190, 255, 14%), rgba(202, 190, 255, 14%)), linear-gradient(0deg, rgba(196, 199, 199, 2%), rgba(196, 199, 199, 2%)), #191c1d;
|
||||
--color-error: #dd3730;
|
||||
--color-toast: rgba(247, 248, 248, 80%);
|
||||
--color-overlay: rgba(247, 248, 248, 80%);
|
||||
--color-overlay: rgba(247, 248, 248, 16%);
|
||||
--color-base: #191c1d;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, { ReactNode, useCallback, useContext } from 'react';
|
|||
|
||||
import LoadingLayer from '@/components/LoadingLayer';
|
||||
import Toast from '@/components/Toast';
|
||||
import PageContext from '@/hooks/page-context';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import { register } from '@/apis/register';
|
||||
|
||||
import CreateAccount from '.';
|
||||
|
||||
jest.mock('@/apis/register', () => ({ register: jest.fn(async () => Promise.resolve()) }));
|
||||
jest.mock('@/hooks/page-context', () =>
|
||||
React.createContext({
|
||||
loading: false,
|
||||
setLoading: jest.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
describe('<CreateAccount/>', () => {
|
||||
test('default render', () => {
|
||||
const { queryByText, container } = render(<CreateAccount />);
|
||||
const { queryByText, container } = renderWithPageContext(<CreateAccount />);
|
||||
expect(container.querySelector('input[name="username"]')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="password"]')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="confirm_password"]')).not.toBeNull();
|
||||
|
@ -24,7 +19,7 @@ describe('<CreateAccount/>', () => {
|
|||
});
|
||||
|
||||
test('username and password are required', () => {
|
||||
const { queryAllByText, getByText } = render(<CreateAccount />);
|
||||
const { queryAllByText, getByText } = renderWithPageContext(<CreateAccount />);
|
||||
const submitButton = getByText('action.create');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
|
@ -34,7 +29,7 @@ describe('<CreateAccount/>', () => {
|
|||
});
|
||||
|
||||
test('username with initial numeric char should throw', () => {
|
||||
const { queryByText, getByText, container } = render(<CreateAccount />);
|
||||
const { queryByText, getByText, container } = renderWithPageContext(<CreateAccount />);
|
||||
const submitButton = getByText('action.create');
|
||||
|
||||
const usernameInput = container.querySelector('input[name="username"]');
|
||||
|
@ -58,7 +53,7 @@ describe('<CreateAccount/>', () => {
|
|||
});
|
||||
|
||||
test('username with special character should throw', () => {
|
||||
const { queryByText, getByText, container } = render(<CreateAccount />);
|
||||
const { queryByText, getByText, container } = renderWithPageContext(<CreateAccount />);
|
||||
const submitButton = getByText('action.create');
|
||||
const usernameInput = container.querySelector('input[name="username"]');
|
||||
|
||||
|
@ -81,7 +76,7 @@ describe('<CreateAccount/>', () => {
|
|||
});
|
||||
|
||||
test('password less than 6 chars should throw', () => {
|
||||
const { queryByText, getByText, container } = render(<CreateAccount />);
|
||||
const { queryByText, getByText, container } = renderWithPageContext(<CreateAccount />);
|
||||
const submitButton = getByText('action.create');
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
|
||||
|
@ -104,7 +99,7 @@ describe('<CreateAccount/>', () => {
|
|||
});
|
||||
|
||||
test('password mismatch with confirmPassword should throw', () => {
|
||||
const { queryByText, getByText, container } = render(<CreateAccount />);
|
||||
const { queryByText, getByText, container } = renderWithPageContext(<CreateAccount />);
|
||||
const submitButton = getByText('action.create');
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirm_password"]');
|
||||
|
@ -137,7 +132,7 @@ describe('<CreateAccount/>', () => {
|
|||
});
|
||||
|
||||
test('terms of use not checked should throw', () => {
|
||||
const { queryByText, getByText, container } = render(<CreateAccount />);
|
||||
const { queryByText, getByText, container } = renderWithPageContext(<CreateAccount />);
|
||||
const submitButton = getByText('action.create');
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirm_password"]');
|
||||
|
@ -169,7 +164,7 @@ describe('<CreateAccount/>', () => {
|
|||
});
|
||||
|
||||
test('submit form properly', async () => {
|
||||
const { getByText, container } = render(<CreateAccount />);
|
||||
const { getByText, container } = renderWithPageContext(<CreateAccount />);
|
||||
const submitButton = getByText('action.create');
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirm_password"]');
|
||||
|
|
|
@ -15,8 +15,8 @@ import { ErrorType } from '@/components/ErrorMessage';
|
|||
import Input from '@/components/Input';
|
||||
import PasswordInput from '@/components/Input/PasswordInput';
|
||||
import TermsOfUse from '@/components/TermsOfUse';
|
||||
import PageContext from '@/hooks/page-context';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.field {
|
||||
.inputField {
|
||||
margin-bottom: _.unit(8);
|
||||
|
||||
&.withError {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { render, act, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { act, fireEvent, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
|
||||
import PasscodeValidation from '.';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
@ -13,13 +15,6 @@ jest.mock('@/apis/utils', () => ({
|
|||
getVerifyPasscodeApi: () => verifyPasscodeApi,
|
||||
}));
|
||||
|
||||
jest.mock('@/hooks/page-context', () =>
|
||||
React.createContext({
|
||||
loading: false,
|
||||
setLoading: jest.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
describe('<PasscodeValidation />', () => {
|
||||
const email = 'foo@logto.io';
|
||||
|
||||
|
@ -28,7 +23,7 @@ describe('<PasscodeValidation />', () => {
|
|||
});
|
||||
|
||||
it('render counter', () => {
|
||||
const { queryByText } = render(
|
||||
const { queryByText } = renderWithPageContext(
|
||||
<PasscodeValidation type="sign-in" method="email" target={email} />
|
||||
);
|
||||
|
||||
|
@ -42,7 +37,7 @@ describe('<PasscodeValidation />', () => {
|
|||
});
|
||||
|
||||
it('fire resend event', async () => {
|
||||
const { getByText } = render(
|
||||
const { getByText } = renderWithPageContext(
|
||||
<PasscodeValidation type="sign-in" method="email" target={email} />
|
||||
);
|
||||
act(() => {
|
||||
|
@ -58,7 +53,7 @@ describe('<PasscodeValidation />', () => {
|
|||
});
|
||||
|
||||
it('fire validate passcode event', async () => {
|
||||
const { container } = render(
|
||||
const { container } = renderWithPageContext(
|
||||
<PasscodeValidation type="sign-in" method="email" target={email} />
|
||||
);
|
||||
const inputs = container.querySelectorAll('input');
|
||||
|
|
|
@ -8,8 +8,8 @@ import { getSendPasscodeApi, getVerifyPasscodeApi } from '@/apis/utils';
|
|||
import { ErrorType } from '@/components/ErrorMessage';
|
||||
import Passcode, { defaultLength } from '@/components/Passcode';
|
||||
import TextLink from '@/components/TextLink';
|
||||
import PageContext from '@/hooks/page-context';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
import { UserFlow } from '@/types';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -102,7 +102,7 @@ const PasscodeValidation = ({ type, method, className, target }: Props) => {
|
|||
<form className={classNames(styles.form, className)}>
|
||||
<Passcode
|
||||
name="passcode"
|
||||
className={classNames(styles.field, error && styles.withError)}
|
||||
className={classNames(styles.inputField, error && styles.withError)}
|
||||
value={code}
|
||||
error={error}
|
||||
onChange={setCode}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import { sendRegisterEmailPasscode } from '@/apis/register';
|
||||
import { sendSignInEmailPasscode } from '@/apis/sign-in';
|
||||
|
||||
|
@ -13,16 +14,10 @@ jest.mock('@/apis/sign-in', () => ({
|
|||
jest.mock('@/apis/register', () => ({
|
||||
sendRegisterEmailPasscode: jest.fn(async () => Promise.resolve()),
|
||||
}));
|
||||
jest.mock('@/hooks/page-context', () =>
|
||||
React.createContext({
|
||||
loading: false,
|
||||
setLoading: jest.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
describe('<EmailPasswordless/>', () => {
|
||||
test('render', () => {
|
||||
const { queryByText, container } = render(
|
||||
const { queryByText, container } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<EmailPasswordless type="sign-in" />
|
||||
</MemoryRouter>
|
||||
|
@ -33,7 +28,7 @@ describe('<EmailPasswordless/>', () => {
|
|||
});
|
||||
|
||||
test('required email with error message', () => {
|
||||
const { queryByText, container, getByText } = render(
|
||||
const { queryByText, container, getByText } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<EmailPasswordless type="sign-in" />
|
||||
</MemoryRouter>
|
||||
|
@ -56,7 +51,7 @@ describe('<EmailPasswordless/>', () => {
|
|||
});
|
||||
|
||||
test('required terms of agreement with error message', () => {
|
||||
const { queryByText, container, getByText } = render(
|
||||
const { queryByText, container, getByText } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<EmailPasswordless type="sign-in" />
|
||||
</MemoryRouter>
|
||||
|
@ -73,7 +68,7 @@ describe('<EmailPasswordless/>', () => {
|
|||
});
|
||||
|
||||
test('signin method properly', async () => {
|
||||
const { container, getByText } = render(
|
||||
const { container, getByText } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<EmailPasswordless type="sign-in" />
|
||||
</MemoryRouter>
|
||||
|
@ -96,7 +91,7 @@ describe('<EmailPasswordless/>', () => {
|
|||
});
|
||||
|
||||
test('register method properly', async () => {
|
||||
const { container, getByText } = render(
|
||||
const { container, getByText } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<EmailPasswordless type="register" />
|
||||
</MemoryRouter>
|
||||
|
|
|
@ -14,8 +14,8 @@ import Button from '@/components/Button';
|
|||
import { ErrorType } from '@/components/ErrorMessage';
|
||||
import Input from '@/components/Input';
|
||||
import TermsOfUse from '@/components/TermsOfUse';
|
||||
import PageContext from '@/hooks/page-context';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
import { UserFlow } from '@/types';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import { sendRegisterSmsPasscode } from '@/apis/register';
|
||||
import { sendSignInSmsPasscode } from '@/apis/sign-in';
|
||||
import { defaultCountryCallingCode } from '@/hooks/use-phone-number';
|
||||
|
@ -14,18 +15,12 @@ jest.mock('@/apis/sign-in', () => ({
|
|||
jest.mock('@/apis/register', () => ({
|
||||
sendRegisterSmsPasscode: jest.fn(async () => Promise.resolve()),
|
||||
}));
|
||||
jest.mock('@/hooks/page-context', () =>
|
||||
React.createContext({
|
||||
loading: false,
|
||||
setLoading: jest.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
describe('<PhonePasswordless/>', () => {
|
||||
const phoneNumber = '18888888888';
|
||||
|
||||
test('render', () => {
|
||||
const { queryByText, container } = render(
|
||||
const { queryByText, container } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<PhonePasswordless type="sign-in" />
|
||||
</MemoryRouter>
|
||||
|
@ -36,7 +31,7 @@ describe('<PhonePasswordless/>', () => {
|
|||
});
|
||||
|
||||
test('required phone with error message', () => {
|
||||
const { queryByText, container, getByText } = render(
|
||||
const { queryByText, container, getByText } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<PhonePasswordless type="sign-in" />
|
||||
</MemoryRouter>
|
||||
|
@ -59,7 +54,7 @@ describe('<PhonePasswordless/>', () => {
|
|||
});
|
||||
|
||||
test('required terms of agreement with error message', () => {
|
||||
const { queryByText, container, getByText } = render(
|
||||
const { queryByText, container, getByText } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<PhonePasswordless type="sign-in" />
|
||||
</MemoryRouter>
|
||||
|
@ -76,7 +71,7 @@ describe('<PhonePasswordless/>', () => {
|
|||
});
|
||||
|
||||
test('signin method properly', async () => {
|
||||
const { container, getByText } = render(
|
||||
const { container, getByText } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<PhonePasswordless type="sign-in" />
|
||||
</MemoryRouter>
|
||||
|
@ -99,7 +94,7 @@ describe('<PhonePasswordless/>', () => {
|
|||
});
|
||||
|
||||
test('register method properly', async () => {
|
||||
const { container, getByText } = render(
|
||||
const { container, getByText } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<PhonePasswordless type="register" />
|
||||
</MemoryRouter>
|
||||
|
|
|
@ -14,8 +14,8 @@ import Button from '@/components/Button';
|
|||
import { ErrorType } from '@/components/ErrorMessage';
|
||||
import PhoneInput from '@/components/Input/PhoneInput';
|
||||
import TermsOfUse from '@/components/TermsOfUse';
|
||||
import PageContext from '@/hooks/page-context';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
import usePhoneNumber, { countryList } from '@/hooks/use-phone-number';
|
||||
import { UserFlow } from '@/types';
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { render, fireEvent, waitFor } from '@testing-library/react';
|
|||
import React from 'react';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import ContextProvider from '@/__mocks__/ContextProvider';
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import { socialConnectors } from '@/__mocks__/logto';
|
||||
import * as socialSignInApi from '@/apis/social';
|
||||
import { generateState, storeState } from '@/hooks/use-social';
|
||||
|
@ -57,12 +57,10 @@ describe('SecondarySocialSignIn', () => {
|
|||
it('invoke web social signIn', async () => {
|
||||
const connectors = socialConnectors.slice(0, 1);
|
||||
|
||||
const { container } = render(
|
||||
<ContextProvider>
|
||||
<MemoryRouter>
|
||||
<SecondarySocialSignIn connectors={connectors} />
|
||||
</MemoryRouter>
|
||||
</ContextProvider>
|
||||
const { container } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<SecondarySocialSignIn connectors={connectors} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
const socialButton = container.querySelector('button');
|
||||
|
||||
|
@ -82,12 +80,10 @@ describe('SecondarySocialSignIn', () => {
|
|||
/* eslint-enable @silverhand/fp/no-mutation */
|
||||
|
||||
const connectors = socialConnectors.slice(0, 1);
|
||||
const { container } = render(
|
||||
<ContextProvider>
|
||||
<MemoryRouter>
|
||||
<SecondarySocialSignIn connectors={connectors} />
|
||||
</MemoryRouter>
|
||||
</ContextProvider>
|
||||
const { container } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<SecondarySocialSignIn connectors={connectors} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
const socialButton = container.querySelector('button');
|
||||
|
||||
|
@ -118,17 +114,15 @@ describe('SecondarySocialSignIn', () => {
|
|||
});
|
||||
/* eslint-enable @silverhand/fp/no-mutating-methods */
|
||||
|
||||
render(
|
||||
<ContextProvider>
|
||||
<MemoryRouter initialEntries={['/sign-in/callback/github']}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/sign-in/callback/:connector"
|
||||
element={<SecondarySocialSignIn connectors={connectors} />}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</ContextProvider>
|
||||
renderWithPageContext(
|
||||
<MemoryRouter initialEntries={['/sign-in/callback/github']}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/sign-in/callback/:connector"
|
||||
element={<SecondarySocialSignIn connectors={connectors} />}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import { signInBasic } from '@/apis/sign-in';
|
||||
|
||||
import UsernameSignin from '.';
|
||||
|
||||
jest.mock('@/apis/sign-in', () => ({ signInBasic: jest.fn(async () => Promise.resolve()) }));
|
||||
jest.mock('@/hooks/page-context', () =>
|
||||
React.createContext({
|
||||
loading: false,
|
||||
setLoading: jest.fn(),
|
||||
})
|
||||
);
|
||||
|
||||
describe('<UsernameSignin>', () => {
|
||||
test('render', () => {
|
||||
const { queryByText, container } = render(<UsernameSignin />);
|
||||
const { queryByText, container } = renderWithPageContext(<UsernameSignin />);
|
||||
expect(container.querySelector('input[name="username"]')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="password"]')).not.toBeNull();
|
||||
expect(queryByText('action.sign_in')).not.toBeNull();
|
||||
|
@ -23,7 +18,9 @@ describe('<UsernameSignin>', () => {
|
|||
});
|
||||
|
||||
test('required inputs with error message', () => {
|
||||
const { queryByText, queryAllByText, getByText, container } = render(<UsernameSignin />);
|
||||
const { queryByText, queryAllByText, getByText, container } = renderWithPageContext(
|
||||
<UsernameSignin />
|
||||
);
|
||||
const submitButton = getByText('action.sign_in');
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
@ -53,7 +50,7 @@ describe('<UsernameSignin>', () => {
|
|||
});
|
||||
|
||||
test('submit form', async () => {
|
||||
const { getByText, container } = render(<UsernameSignin />);
|
||||
const { getByText, container } = renderWithPageContext(<UsernameSignin />);
|
||||
const submitButton = getByText('action.sign_in');
|
||||
|
||||
const usernameInput = container.querySelector('input[name="username"]');
|
||||
|
|
|
@ -15,8 +15,8 @@ import { ErrorType } from '@/components/ErrorMessage';
|
|||
import Input from '@/components/Input';
|
||||
import PasswordInput from '@/components/Input/PasswordInput';
|
||||
import TermsOfUse from '@/components/TermsOfUse';
|
||||
import PageContext from '@/hooks/page-context';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { SignInExperienceSettings } from '@/types';
|
||||
|
||||
type Context = {
|
||||
toast: string;
|
||||
loading: boolean;
|
||||
setToast: (message: string) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
experienceSettings: SignInExperienceSettings | undefined;
|
||||
setExperienceSettings: (settings: SignInExperienceSettings) => void;
|
||||
};
|
||||
|
||||
const NOOP = () => {
|
||||
throw new Error('Context provider not found');
|
||||
};
|
||||
|
||||
const PageContext = React.createContext<Context>({
|
||||
toast: '',
|
||||
loading: false,
|
||||
setToast: NOOP,
|
||||
setLoading: NOOP,
|
||||
experienceSettings: undefined,
|
||||
setExperienceSettings: NOOP,
|
||||
});
|
||||
|
||||
export default PageContext;
|
|
@ -2,7 +2,7 @@ import { RequestErrorBody } from '@logto/schemas';
|
|||
import { HTTPError } from 'ky';
|
||||
import { useState, useCallback, useContext } from 'react';
|
||||
|
||||
import PageContext from '@/hooks/page-context';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
|
||||
type UseApi<T extends any[], U> = {
|
||||
result?: U;
|
||||
|
|
64
packages/ui/src/hooks/use-page-context.ts
Normal file
64
packages/ui/src/hooks/use-page-context.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { useState, useMemo, createContext } from 'react';
|
||||
|
||||
import { SignInExperienceSettings } from '@/types';
|
||||
|
||||
type Context = {
|
||||
toast: string;
|
||||
loading: boolean;
|
||||
termsAgreement: boolean;
|
||||
showTermsModal: boolean;
|
||||
experienceSettings: SignInExperienceSettings | undefined;
|
||||
setToast: (message: string) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setTermsAgreement: (termsAgreement: boolean) => void;
|
||||
setShowTermsModal: (showTermsModal: boolean) => void;
|
||||
setExperienceSettings: (settings: SignInExperienceSettings) => void;
|
||||
};
|
||||
|
||||
const noop = () => {
|
||||
throw new Error('Context provider not found');
|
||||
};
|
||||
|
||||
export const PageContext = createContext<Context>({
|
||||
toast: '',
|
||||
loading: false,
|
||||
termsAgreement: false,
|
||||
showTermsModal: false,
|
||||
experienceSettings: undefined,
|
||||
setToast: noop,
|
||||
setLoading: noop,
|
||||
setTermsAgreement: noop,
|
||||
setShowTermsModal: noop,
|
||||
setExperienceSettings: noop,
|
||||
});
|
||||
|
||||
const usePageContext = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [toast, setToast] = useState('');
|
||||
const [experienceSettings, setExperienceSettings] = useState<SignInExperienceSettings>();
|
||||
const [termsAgreement, setTermsAgreement] = useState(false);
|
||||
const [showTermsModal, setShowTermsModal] = useState(false);
|
||||
|
||||
const context = useMemo(
|
||||
() => ({
|
||||
toast,
|
||||
loading,
|
||||
termsAgreement,
|
||||
showTermsModal,
|
||||
experienceSettings,
|
||||
setLoading,
|
||||
setToast,
|
||||
setTermsAgreement,
|
||||
setShowTermsModal,
|
||||
setExperienceSettings,
|
||||
}),
|
||||
[experienceSettings, loading, showTermsModal, termsAgreement, toast]
|
||||
);
|
||||
|
||||
return {
|
||||
context,
|
||||
Provider: PageContext.Provider,
|
||||
};
|
||||
};
|
||||
|
||||
export default usePageContext;
|
|
@ -4,8 +4,8 @@ import { useParams } from 'react-router-dom';
|
|||
import { invokeSocialSignIn, signInWithSocial } from '@/apis/social';
|
||||
import { generateRandomString, parseQueryParameters } from '@/utils';
|
||||
|
||||
import PageContext from './page-context';
|
||||
import useApi from './use-api';
|
||||
import { PageContext } from './use-page-context';
|
||||
|
||||
/**
|
||||
* Social Connector State Utility Methods
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useState, useEffect, useContext } from 'react';
|
||||
|
||||
import PageContext from './page-context';
|
||||
import { PageContext } from './use-page-context';
|
||||
|
||||
export type Theme = 'dark' | 'light';
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue