From 3037d650e3b5c26e0f3fa8eca6d059d99cf12219 Mon Sep 17 00:00:00 2001 From: Riccardo Forina Date: Thu, 10 Oct 2019 10:57:49 +0200 Subject: [PATCH] Some work on the example website, acting as documentation --- example/app.tsx | 87 +++++++---- example/components/DashboardLayout.tsx | 37 ----- example/components/DashboardParams.tsx | 11 -- example/components/DashboardSimple.tsx | 7 - example/components/ExampleLink.tsx | 8 + .../{Support.tsx => GettingStarted.tsx} | 2 +- example/components/Installation.tsx | 55 +++++++ example/components/Keyword.tsx | 18 +++ example/components/Overview.tsx | 59 ++++++++ example/components/StarWarsPeople.tsx | 142 ++++++++++++++++++ example/components/Usage.tsx | 41 +++++ example/components/index.ts | 11 +- example/index.html | 1 + example/package.json | 3 +- example/pages/AsyncDataList.tsx | 35 +++++ example/pages/DashboardPage.tsx | 28 ---- example/pages/DashboardParamsPage.tsx | 27 ---- example/pages/DashboardSimplePage.tsx | 25 --- example/pages/InstallationPage.tsx | 11 ++ example/pages/OverviewPage.tsx | 11 ++ .../pages/{SupportPage.tsx => UsagePage.tsx} | 8 +- example/use-patternfly.png | Bin 0 -> 13049 bytes example/yarn.lock | 5 + src/LazyRoute.tsx | 52 +------ src/Loading.tsx | 18 +++ src/index.ts | 1 + test/LazyRoute.test.tsx | 19 +-- 27 files changed, 494 insertions(+), 228 deletions(-) delete mode 100644 example/components/DashboardLayout.tsx delete mode 100644 example/components/DashboardParams.tsx delete mode 100644 example/components/DashboardSimple.tsx create mode 100644 example/components/ExampleLink.tsx rename example/components/{Support.tsx => GettingStarted.tsx} (93%) create mode 100644 example/components/Installation.tsx create mode 100644 example/components/Keyword.tsx create mode 100644 example/components/Overview.tsx create mode 100644 example/components/StarWarsPeople.tsx create mode 100644 example/components/Usage.tsx create mode 100644 example/pages/AsyncDataList.tsx delete mode 100644 example/pages/DashboardPage.tsx delete mode 100644 example/pages/DashboardParamsPage.tsx delete mode 100644 example/pages/DashboardSimplePage.tsx create mode 100644 example/pages/InstallationPage.tsx create mode 100644 example/pages/OverviewPage.tsx rename example/pages/{SupportPage.tsx => UsagePage.tsx} (54%) create mode 100644 example/use-patternfly.png create mode 100644 src/Loading.tsx diff --git a/example/app.tsx b/example/app.tsx index ce5e71a..d20c2d5 100644 --- a/example/app.tsx +++ b/example/app.tsx @@ -1,58 +1,91 @@ import 'react-app-polyfill/ie11'; import '@patternfly/react-core/dist/styles/base.css'; +import { Brand } from '@patternfly/react-core'; import * as React from 'react'; import { Redirect, useHistory } from 'react-router-dom'; import { LastLocationProvider } from 'react-router-last-location'; import { AppLayout, LazyRoute, SwitchWith404 } from 'use-patternfly'; import './app.css'; +import logo from './use-patternfly.png'; export const App = () => { const history = useHistory(); - return ( - } logoProps={{ onClick: () => history.push('/') }} navVariant={'vertical'} navItems={[ { - title: 'Dashboard', - to: '/dashboard', + title: 'Overview', + to: '/', + exact: true + }, + { + title: 'Getting Started', + to: '/getting-started', items: [ - { to: '/dashboard/simple', title: 'Simple' }, - {}, - { to: '/dashboard/params', title: 'Params: empty', exact: true }, - { to: '/dashboard/params/hello', title: 'Params: hello', exact: true }, + { to: '/getting-started/installation', title: 'Installation' }, + { to: '/getting-started/usage', title: 'Usage' }, + ] + }, + { + title: 'API', + to: '/api', + items: [ + { to: '/api/AppLayout', title: 'AppLayout' }, + { to: '/api/AppNavExpandable', title: 'AppNavExpandable' }, + { to: '/api/AppNavGroup', title: 'AppNavGroup' }, + { to: '/api/AppNavItem', title: 'AppNavItem' }, + { to: '/api/LazyRoute', title: 'LazyRoute' }, + { to: '/api/Loading', title: 'Loading' }, + { to: '/api/NotFound', title: 'NotFound' }, + { to: '/api/SwitchWith404', title: 'SwitchWith404' }, + { to: '/api/useA11yRoute', title: 'useA11yRoute' }, + { to: '/api/useBreadcrumb', title: 'useBreadcrumb' }, + { to: '/api/useDocumentTitle', title: 'useDocumentTitle' }, + ], + }, { + title: 'Examples', + to: '/examples', + items: [ + { to: '/examples/async-data-list', title: 'Async Data List' }, ], }, - { to: '/support', title: 'Support' }, - {}, - { to: '/broken', title: 'Broken link' }, ]} navGroupsStyle={'expandable'} > - import(/* webpackChunkName: 'dashboard-page' */ './pages/DashboardPage')} - > - {({ component: DashboardPage }) => - - } - + path='/' + exact={true} + getComponent={() => import('./pages/OverviewPage')} + /> + + import('./pages/InstallationPage')} + /> + import('./pages/UsagePage')} + /> + import(/* webpackChunkName: 'support-page' */ './pages/SupportPage')} - > - {({ component: SupportPage }) => - - } - + path='/examples/async-data-list' + getComponent={() => import('./pages/AsyncDataList')} + /> diff --git a/example/components/DashboardLayout.tsx b/example/components/DashboardLayout.tsx deleted file mode 100644 index 4a1ab83..0000000 --- a/example/components/DashboardLayout.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import * as React from 'react'; -import { Link } from 'react-router-dom'; -import { PageSection, Title, EmptyState, EmptyStateVariant, Button, EmptyStateSecondaryActions, Text, TextContent } from '@patternfly/react-core'; - -export const DashboardLayout: React.FunctionComponent = ({ children, ...props }) => { - return ( - <> - - - - Dashboard - - This page shows how to implement nested routes. - - - - - - - - - - - - - - - - - - - - {children} - - - ); -} diff --git a/example/components/DashboardParams.tsx b/example/components/DashboardParams.tsx deleted file mode 100644 index 40bcc6f..0000000 --- a/example/components/DashboardParams.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from 'react'; - -export interface IDashboardParamsProps extends React.HTMLAttributes { - sample: string; -} - -export const DashboardParams: React.FunctionComponent = ({ sample }) => { - return ( -
Parameter "sample": {sample}
- ) -}; diff --git a/example/components/DashboardSimple.tsx b/example/components/DashboardSimple.tsx deleted file mode 100644 index d6f5229..0000000 --- a/example/components/DashboardSimple.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import * as React from 'react'; - -export const DashboardSimple: React.FunctionComponent> = (props) => { - return ( -
Simple
- ) -}; diff --git a/example/components/ExampleLink.tsx b/example/components/ExampleLink.tsx new file mode 100644 index 0000000..1bdfed2 --- /dev/null +++ b/example/components/ExampleLink.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; + +export interface IExampleLinkProps extends React.HTMLAttributes { + filename: string; +} + +export const ExampleLink: React.FunctionComponent = ({ filename, ...props }) => + \ No newline at end of file diff --git a/example/components/Support.tsx b/example/components/GettingStarted.tsx similarity index 93% rename from example/components/Support.tsx rename to example/components/GettingStarted.tsx index b66ff9d..d207926 100644 --- a/example/components/Support.tsx +++ b/example/components/GettingStarted.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { PageSection, Title, EmptyState, EmptyStateVariant, EmptyStateIcon, EmptyStateBody, Button, EmptyStateSecondaryActions } from '@patternfly/react-core'; import { CubesIcon } from '@patternfly/react-icons'; -export const Support: React.FunctionComponent = ({ children, ...props }) => { +export const GettingStarted: React.FunctionComponent = ({ children, ...props }) => { return ( diff --git a/example/components/Installation.tsx b/example/components/Installation.tsx new file mode 100644 index 0000000..a04c538 --- /dev/null +++ b/example/components/Installation.tsx @@ -0,0 +1,55 @@ +import { + PageSection, + TextContent, + Title, + Text, + FlexItem, + Flex, + Button, + TextList, + TextListItem, +} from '@patternfly/react-core'; +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import { Keyword } from './Keyword'; + + +export const Installation: React.FunctionComponent = ({ children, ...props }) => { + return ( + + + Installation + You can install use-patternfly from npm: + + npm install use-patternfly --save + + Or if you're using Yarn: + + yarn add use-patternfly + + + This package requires the following packages as peer dependencies: + + + @patternfly/react-core + @patternfly/react-styles + @patternfly/react-icons + react-router + react-router-dom + + + Please make sure to install them as well. You'll need react@16.8.0 + or later since the package uses hooks. + + + Some of the provided components and hooks could require additional dependencies. + + + + + + + + + ); +} diff --git a/example/components/Keyword.tsx b/example/components/Keyword.tsx new file mode 100644 index 0000000..add5210 --- /dev/null +++ b/example/components/Keyword.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { css, StyleSheet } from '@patternfly/react-styles'; + +const styles = StyleSheet.create({ + keyword: { + display: 'inline-block', + backgroundColor: `var(--pf-global--BackgroundColor--light-200)`, + fontFamily: 'monospace', + padding: '3px', + borderRadius: '3px' + } +}); + +export const Keyword: React.FunctionComponent = props => + ; diff --git a/example/components/Overview.tsx b/example/components/Overview.tsx new file mode 100644 index 0000000..7cc7415 --- /dev/null +++ b/example/components/Overview.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { PageSection, TextContent, Title, Text, FlexItem, Flex, Button } from '@patternfly/react-core'; +import { Link } from 'react-router-dom'; +import logo from '../use-patternfly.png'; +import { Keyword } from './Keyword'; + +export const Overview: React.FunctionComponent = ({ children, ...props }) => { + return ( + <> + +
+ {'use-patternfly +
+ +
+ {'Build +   + + {'Coverage + + + + + Overview + + use-patternfly is an opinionated set of hooks and components useful when building a React app with PatternFly. + + + This project aims to encourage code reuse between all projects using PatternFly and to showcase how to implement features like fetching from an API and showing the data in a DataList or Table component. + + + Motivation + + Since I (@riccardoforina) started + building apps using PatternFly, I found myself writing solutions to the same problem + many times. But how many ways can there be to set the document title or wrap an app in a PatternFly chrome? I felt the need + to finalize these solutions in something reusable and well-tested, to finally + be able to focus on the app needs instead of re-learning how to wire PatternFly's + components once again. + + + Dependencies + The only real dependency-other than PatternFly itself-is react-router, + although it's listed as a peer dependency to avoid clashing with whatever + version you are running on your project. Other utilities might have extra + dependencies, but if you don't plan on using that specific utility you + don't need to install them. + + + + + + + + + + + ); +} diff --git a/example/components/StarWarsPeople.tsx b/example/components/StarWarsPeople.tsx new file mode 100644 index 0000000..2a06e6e --- /dev/null +++ b/example/components/StarWarsPeople.tsx @@ -0,0 +1,142 @@ +import * as React from 'react'; +import { + DataList, + DataListCell, + DataListItem, + DataListItemCells, + DataListItemRow, + PageSection, Pagination, Text, + TextContent, TextList, TextListItem, TextListItemVariants, TextListVariants, + Title, +} from '@patternfly/react-core'; +import { InfoIcon } from '@patternfly/react-icons'; +import { Loading } from 'use-patternfly'; +import { ExampleLink } from './ExampleLink'; + +export interface IStarWarsPerson { + name: string; + height: string; + mass: string; + hair_color: string; + skin_color: string; + eye_color: string; + birth_year: string; + gender: string; + homeworld: string; + films: string[]; + species: string[]; + vehicles: string[]; + starships: string[]; + created: Date; + edited: Date; + url: string; +} + +export interface IStarWarsPeopleProps { + people: IStarWarsPerson[]; + perPage: number; + page: number; + total: number; + loading: boolean; + onPageChange: (page: number) => void +} + +export const StarWarsPeople: React.FunctionComponent = ({ + people, + page, + perPage, + total, + loading, + onPageChange, + ...props +}) => { + const onPerPageSelect = () => false; + const paginationProps = { + itemCount: total, + page: page, + perPage: perPage, + onSetPage: (_, page) => onPageChange(page), + onPerPageSelect: onPerPageSelect, + perPageOptions: [{ title: `Sorry, SWApi doesn't support this`, value: perPage }], + }; + return ( + <> + + + Async Data List + + A common pattern is to call an API, wait for its results and show + the data in a data list. In this example we use the + SWApi as a sample backend service + to show the list of characters in the Star Wars universe. + + Source for this example. + + + + + + {loading ? : people.map(person => { + const itemId = `sw-person-${person.name}`; + return ( + + + + + , + + + {person.name} + Gender: {person.gender} Mass: {person.mass} + + , + + + + Films + + {person.films.length} + + + + , + + + + Vehicles + + {person.vehicles.length} + + + + , + + + + Starships + + {person.starships.length} + + + + + ]} + /> + + + ); + })} + + + + + ); +} diff --git a/example/components/Usage.tsx b/example/components/Usage.tsx new file mode 100644 index 0000000..14a5abc --- /dev/null +++ b/example/components/Usage.tsx @@ -0,0 +1,41 @@ +import { + PageSection, + TextContent, + Title, + Text, + FlexItem, + Flex, + Button, +} from '@patternfly/react-core'; +import * as React from 'react'; +import { Link } from 'react-router-dom'; + +export const Usage: React.FunctionComponent = ({ children, ...props }) => { + return ( + + + Usage + Import the component or hook that you need using the object destructuring syntax: + + + + + + Another source of usage examples is the code behind this website. + + + + + + + + + ); +} diff --git a/example/components/index.ts b/example/components/index.ts index 784600a..dba9ebe 100644 --- a/example/components/index.ts +++ b/example/components/index.ts @@ -1,4 +1,7 @@ -export * from './Support'; -export * from './DashboardLayout'; -export * from './DashboardParams'; -export * from './DashboardSimple'; +export * from './ExampleLink'; +export * from './GettingStarted'; +export * from './Installation'; +export * from './Usage'; +export * from './Overview'; +export * from './StarWarsPeople'; +export * from './Keyword'; diff --git a/example/index.html b/example/index.html index 547e2e0..c58179f 100644 --- a/example/index.html +++ b/example/index.html @@ -10,5 +10,6 @@
+ diff --git a/example/package.json b/example/package.json index b106a9d..ccfc09c 100644 --- a/example/package.json +++ b/example/package.json @@ -8,7 +8,8 @@ "build": "parcel build index.html" }, "dependencies": { - "react-app-polyfill": "^1.0.4" + "react-app-polyfill": "^1.0.4", + "react-async": "^9.0.0" }, "alias": { "use-patternfly": "../.", diff --git a/example/pages/AsyncDataList.tsx b/example/pages/AsyncDataList.tsx new file mode 100644 index 0000000..7fe4bb7 --- /dev/null +++ b/example/pages/AsyncDataList.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { useFetch } from 'react-async'; +import { useA11yRouteContainer, useDocumentTitle } from 'use-patternfly'; +import { IStarWarsPerson, StarWarsPeople } from '../components'; + +interface IStarWarsPeopleResponse { + count: number; + next: string; + previous?: string; + results: IStarWarsPerson[] +} + +export default function AsyncDataList() { + useDocumentTitle('Async Data List'); + const a11yContainerProps = useA11yRouteContainer(); + const [page, setPage] = React.useState(1); + const { data, isPending } = useFetch( + `https://swapi.co/api/people/?page=${page}`, + { headers: { Accept: 'application/json' } } + ); + const handlePageChange = React.useCallback((newPage: number) => { + setPage(newPage < 0 ? 0 : newPage); + }, [setPage]); + return ( + + ); +} diff --git a/example/pages/DashboardPage.tsx b/example/pages/DashboardPage.tsx deleted file mode 100644 index 8612fce..0000000 --- a/example/pages/DashboardPage.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; -import { useRouteMatch, Route } from 'react-router'; -import { SwitchWith404, useA11yRouteContainer, useDocumentTitle } from 'use-patternfly'; -import { DashboardLayout } from '../components'; -import DashboardSimplePage from './DashboardSimplePage'; -import DashboardParamsPage from './DashboardParamsPage'; - -export default function DashboardPage() { - const { path } = useRouteMatch()!; - const a11yContainerProps = useA11yRouteContainer(); - useDocumentTitle('Dashboard'); - return ( - - - - - - - - - - - ); -} diff --git a/example/pages/DashboardParamsPage.tsx b/example/pages/DashboardParamsPage.tsx deleted file mode 100644 index de0ef8d..0000000 --- a/example/pages/DashboardParamsPage.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import { useParams } from 'react-router'; -import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core'; -import { Link } from 'react-router-dom'; -import { useA11yRouteContainer, useBreadcrumb, useDocumentTitle } from 'use-patternfly'; -import { DashboardParams } from '../components'; - -export const DashboardPageBreadcrumbs = ( - - Dashboard - - Params - - -); - -export default function DashboardPage() { - const a11yContainerProps = useA11yRouteContainer(); - const { sample } = useParams<{ sample?: string }>(); - useDocumentTitle('Dashboard - Params'); - useBreadcrumb( - DashboardPageBreadcrumbs - ); - return ( - - ); -} diff --git a/example/pages/DashboardSimplePage.tsx b/example/pages/DashboardSimplePage.tsx deleted file mode 100644 index d3a5a85..0000000 --- a/example/pages/DashboardSimplePage.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from 'react'; -import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core'; -import { Link } from 'react-router-dom'; -import { useA11yRouteContainer, useBreadcrumb, useDocumentTitle } from 'use-patternfly'; -import { DashboardSimple } from '../components'; - -const DashboardPageBreadcrumbs = ( - - Dashboard - - Simple - - -); - -export default function DashboardPage() { - const a11yContainerProps = useA11yRouteContainer(); - useDocumentTitle('Dashboard - Simple'); - useBreadcrumb( - DashboardPageBreadcrumbs - ); - return ( - - ); -} diff --git a/example/pages/InstallationPage.tsx b/example/pages/InstallationPage.tsx new file mode 100644 index 0000000..315f3e8 --- /dev/null +++ b/example/pages/InstallationPage.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { useA11yRouteContainer, useDocumentTitle } from 'use-patternfly'; +import { Installation } from '../components'; + +export default function InstallationPage() { + const a11yContainerProps = useA11yRouteContainer(); + useDocumentTitle('Installation'); + return ( + + ); +} diff --git a/example/pages/OverviewPage.tsx b/example/pages/OverviewPage.tsx new file mode 100644 index 0000000..6dc0827 --- /dev/null +++ b/example/pages/OverviewPage.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { useA11yRouteContainer, useDocumentTitle } from 'use-patternfly'; +import { Overview } from '../components'; + +export default function OverviewPage() { + const a11yContainerProps = useA11yRouteContainer(); + useDocumentTitle('Overview'); + return ( + + ); +} diff --git a/example/pages/SupportPage.tsx b/example/pages/UsagePage.tsx similarity index 54% rename from example/pages/SupportPage.tsx rename to example/pages/UsagePage.tsx index d3ddf2f..9864620 100644 --- a/example/pages/SupportPage.tsx +++ b/example/pages/UsagePage.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { useA11yRouteContainer, useDocumentTitle } from 'use-patternfly'; -import { Support } from '../components'; +import { Usage } from '../components'; -export default function SupportPage() { +export default function UsagePage() { const a11yContainerProps = useA11yRouteContainer(); - useDocumentTitle('Support'); + useDocumentTitle('Usage'); return ( - + ); } diff --git a/example/use-patternfly.png b/example/use-patternfly.png new file mode 100644 index 0000000000000000000000000000000000000000..af81290f08019fcec62800466b938082b194e62a GIT binary patch literal 13049 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 literal 0 HcmV?d00001 diff --git a/example/yarn.lock b/example/yarn.lock index 057b7d0..f1ceeeb 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -4187,6 +4187,11 @@ react-app-polyfill@^1.0.4: regenerator-runtime "0.13.3" whatwg-fetch "3.0.0" +react-async@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/react-async/-/react-async-9.0.0.tgz#60e8282278fc642429229a33ce1dbd427d6470e3" + integrity sha512-J1o8clttyaQ12+G2GiRTgsvkD+QZJS9A9GTh3nDWX6gvoskmUwMPXFDntOcUvP032xUAf6F75M2sT3FW095HeA== + readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.3, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" diff --git a/src/LazyRoute.tsx b/src/LazyRoute.tsx index d453e31..f7adafa 100644 --- a/src/LazyRoute.tsx +++ b/src/LazyRoute.tsx @@ -1,54 +1,18 @@ import * as React from 'react'; import { RouteProps, Route } from 'react-router-dom'; -import { - PageSection, - EmptyState, - EmptyStateIcon, - Title, -} from '@patternfly/react-core'; -import { Spinner } from '@patternfly/react-core/dist/js/experimental'; +import { Loading } from './Loading'; -export interface IDynamicImportChildrenProps { - component: React.ComponentType; +export interface IDynamicImportProps extends RouteProps { + getComponent: () => Promise<{ default: React.ComponentType }>; } -export interface IDynamicImportProps extends RouteProps { - getComponent: () => Promise<{ default: React.ComponentType }>; - children: (props: IDynamicImportChildrenProps) => React.ReactElement; -} - -export function LazyRoute({ - getComponent, - children, - ...props -}: IDynamicImportProps) { - const [module, setModule] = React.useState<{ - default: React.ComponentType; - } | null>(null); - const isStale = React.useRef(false); - React.useEffect(() => { - (async () => { - const newModule = await getComponent(); - if (!isStale.current) { - setModule(newModule); - } - return () => { - isStale.current = true; - }; - })(); - }, [getComponent, setModule, isStale]); +export function LazyRoute({ getComponent, ...props }: IDynamicImportProps) { + const LazyComponent = React.lazy(getComponent); return ( - {module && module.default ? ( - children({ component: module.default }) - ) : ( - - - - Loading - - - )} + }> + + ); } diff --git a/src/Loading.tsx b/src/Loading.tsx new file mode 100644 index 0000000..54cbbe5 --- /dev/null +++ b/src/Loading.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { + EmptyState, + EmptyStateIcon, + EmptyStateVariant, + PageSection, + Title, +} from '@patternfly/react-core'; +import { Spinner } from '@patternfly/react-core/dist/js/experimental'; + +export const Loading: React.FunctionComponent = () => ( + + + + Loading + + +); diff --git a/src/index.ts b/src/index.ts index 266e6c7..459ff80 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ export * from './AppNavExpandable'; export * from './AppNavGroup'; export * from './AppNavItem'; export * from './LazyRoute'; +export * from './Loading'; export * from './NotFound'; export * from './SwitchWith404'; export * from './useA11yRoute'; diff --git a/test/LazyRoute.test.tsx b/test/LazyRoute.test.tsx index be944c5..812daa6 100644 --- a/test/LazyRoute.test.tsx +++ b/test/LazyRoute.test.tsx @@ -4,24 +4,19 @@ import { LazyRoute } from '@src'; describe('LazyRoute tests', () => { test('should render a spinner while waiting for a component module to be loaded', async () => { - const spyContent = jest.fn(); - const getComponent = jest.fn(); - const { getByText } = render( - - ); + expect.assertions(1); + const getComponent = () => { + expect(true).toBe(true); + return new Promise<{ default: React.ComponentType }>(() => {}); + }; + const { getByText } = render(); await waitForElement(() => getByText('Loading')); - expect(getComponent).toHaveBeenCalledTimes(1); - expect(spyContent).toHaveBeenCalledTimes(0); }); test('should render the async component', async () => { const getComponent = () => Promise.resolve({ default: () =>
content
}); - const { getByText } = render( - - {({ component: Component }) => } - - ); + const { getByText } = render(); await waitForElement(() => getByText('content')); }); });