From f993b7b6fb5c6d28d61f0757ca158e421f432f1c Mon Sep 17 00:00:00 2001 From: Pasecinic Nichita Date: Sun, 14 Jan 2024 16:03:25 +0200 Subject: [PATCH] test: routes.spec.tsx --- .github/workflows/main.yml | 5 +- src/lib/utils/create-nested-routes.tsx | 2 +- src/lib/utils/index.ts | 2 +- src/test/__mocks__/page-a.tsx | 5 + src/test/__mocks__/utils.ts | 12 + src/test/routes.spec.tsx | 511 +++++++++++++++++++------ 6 files changed, 425 insertions(+), 112 deletions(-) create mode 100644 src/test/__mocks__/page-a.tsx create mode 100644 src/test/__mocks__/utils.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e3d82fa..53c2f94 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,4 +15,7 @@ jobs: node-version: '20.x' registry-url: 'https://registry.npmjs.org' - run: npm i - - run: npm run lint && npm run build && npm run publint \ No newline at end of file + - run: npm run lint + - run: npm run test + - run: npm run build + - run: npm run publint \ No newline at end of file diff --git a/src/lib/utils/create-nested-routes.tsx b/src/lib/utils/create-nested-routes.tsx index 5cfbdac..4b7374a 100644 --- a/src/lib/utils/create-nested-routes.tsx +++ b/src/lib/utils/create-nested-routes.tsx @@ -1,7 +1,7 @@ import type { RouteProps } from '../types'; import type { FC } from 'react'; import { Route } from 'react-router-dom'; -import { isDefined } from '../utils'; +import { isDefined } from './index'; export const createNestedRoutes = (RouteType: FC, routes?: RouteProps[]) => { if (!isDefined(routes)) return null; diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 9d49d27..ff83f74 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -1,4 +1,4 @@ export * from './get-routes-config'; export * from './assert-is-defined'; -export * from './create-nested-routes' +export * from './create-nested-routes.tsx' export * from './is-defined'; \ No newline at end of file diff --git a/src/test/__mocks__/page-a.tsx b/src/test/__mocks__/page-a.tsx new file mode 100644 index 0000000..619f5e1 --- /dev/null +++ b/src/test/__mocks__/page-a.tsx @@ -0,0 +1,5 @@ +const pageA = () => { + return
; +}; + +export default pageA; diff --git a/src/test/__mocks__/utils.ts b/src/test/__mocks__/utils.ts new file mode 100644 index 0000000..e47d96c --- /dev/null +++ b/src/test/__mocks__/utils.ts @@ -0,0 +1,12 @@ +import { lazy } from 'react'; + +export const getLazyLoadedPageA = (timeout: number) => + lazy(() => { + return new Promise((resolve) => { + setTimeout(() => { + import('./page-a').then((module) => { + resolve({ default: module.default as never }); + }); + }, timeout); + }); + }); diff --git a/src/test/routes.spec.tsx b/src/test/routes.spec.tsx index c9d26d4..f997d8b 100644 --- a/src/test/routes.spec.tsx +++ b/src/test/routes.spec.tsx @@ -1,140 +1,433 @@ -import { AuthReactRouter, RoutesRoot } from '../'; -import { render, screen } from './utils.tsx'; - -describe('[AuthReactRouter] routes configurations', async () => { - it('should render common page', async () => { - render( - [common] /, - }], - }} - > +import { AuthReactRouter, type RoutesConfig, RoutesRoot } from '../'; +import { render } from './utils.tsx'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { Link, Outlet } from 'react-router-dom'; +import { expect } from 'vitest'; +import { getLazyLoadedPageA } from './__mocks__/utils.ts'; + +describe('[AuthReactRouter] various configurations', () => { + it('[routes common] should render correctly', () => { + const routes: RoutesConfig = { + common: [ + { + path: '/a', + element: ( + <> +
+ + + ), + }, + { + path: '/b', + element: ( + <> +
+ + + ), + }, + ], + }; + const { getByRole, getByTestId } = render( + - , { + , + { wrapperProps: { - initialEntries: ['/'], + initialEntries: ['/a'], }, - }); - expect(screen.getByText('[common] /')).toBeInTheDocument(); + }, + ); + + expect(getByTestId('a')).toBeInTheDocument(); + fireEvent.click(getByRole('link', { name: 'link_to_b' })); + expect(getByTestId('b')).toBeInTheDocument(); + fireEvent.click(getByRole('link', { name: 'link_to_a' })); + expect(getByTestId('a')).toBeInTheDocument(); }); - it('should redirect to private route', async () => { - render( - { + const routes: RoutesConfig = { + common: [ + { + path: '/', + element: ( + <> +
+ + + ), + routes: [ { - path: '/public', - element: <>[public] /, + index: true, + element: ( + <> +
+ + + ), }, - ], - private: [ { - path: '/', - element: <>[private] /, - }], - }} - > + path: 'a', + element: ( + <> +
+ + + + ), + routes: [ + { + path: ':id', + element: ( + <> +
+ + ), + }, + ], + }, + ], + }, + ], + }; + const { getByRole, getByTestId } = render( + + + , + { + wrapperProps: { + initialEntries: ['/'], + }, + }, + ); + + expect(getByTestId('root')).toBeInTheDocument(); + expect(getByTestId('index')).toBeInTheDocument(); + fireEvent.click(getByRole('link', { name: 'link_to_a' })); + expect(getByTestId('a_root')).toBeInTheDocument(); + fireEvent.click(getByRole('link', { name: 'link_to_a/dynamic_param' })); + expect(getByTestId('dynamic_param_page')).toBeInTheDocument(); + }); + + it('[routes common redirect] should redirect to first common route when accessing routes with wrong authorized state', async () => { + const authorizedRoutes: RoutesConfig = { + common: [ + { + path: '/a', + element:
, + }, + ], + public: [ + { + path: '/b', + element:
, + }, + ], + private: [ + { + path: '/c', + element:
, + }, + ], + }; + + // authorized + const { getByTestId, rerender } = render( + + + , + { + wrapperProps: { + initialEntries: ['/b'], + }, + }, + ); + + expect(getByTestId('a')).toBeInTheDocument(); + + const unauthorizedRoutes: RoutesConfig = { + common: [ + { + path: '/d', + element:
, + }, + ], + public: [ + { + path: '/e', + element:
, + }, + ], + private: [ + { + path: '/a', + element:
, + }, + ], + }; + + // unauthorized + rerender( + + + , + ); + expect(getByTestId('d')).toBeInTheDocument(); + }); + + it('[global fallback] redirect to privateRoute when unauthorized users navigates to private route', () => { + const routes: RoutesConfig = { + fallback: { + privateRoute: '/a', + }, + public: [ + { + path: '/a', + element:
, + }, + ], + private: [ + { + path: '/b', + element:
, + }, + ], + }; + const { getByTestId } = render( + + + , + { + wrapperProps: { + initialEntries: ['/b'], + }, + }, + ); + expect(getByTestId('a')).toBeInTheDocument(); + }); + + it('[global fallback] redirect to publicRoute when authorized users navigates to public route', () => { + const routes: RoutesConfig = { + fallback: { + publicRoute: '/b', + }, + public: [ + { + path: '/a', + element:
, + }, + ], + private: [ + { + path: '/b', + element:
, + }, + ], + }; + const { getByTestId } = render( + - , { + , + { wrapperProps: { - initialEntries: ['/public'], + initialEntries: ['/a'], }, - }); - expect(screen.getByText('[private] /')).toBeInTheDocument(); + }, + ); + expect(getByTestId('b')).toBeInTheDocument(); }); - it('should redirect to public route', async () => { - render( - { + const atLeastOneRoleRequired: RoutesConfig = { + private: [ + { + path: '/a', + element:
, + roles: ['admin', 'manager'], + }, + ], + }; + const { getByTestId, rerender } = render( + + + , + { + wrapperProps: { + initialEntries: ['/a'], + }, + }, + ); + expect(getByTestId('a')).toBeInTheDocument(); + + const allRolesRequired: RoutesConfig = { + private: [ + { + path: '/a', + element:
, + roles: ['admin', 'manager'], + allRolesRequired: true, + }, + ], + }; + + rerender( + + + , + ); + expect(getByTestId('a')).toBeInTheDocument(); + }); + + it('[global fallback] should render InvalidRoles (allRolesRequired: false)', () => { + const routes: RoutesConfig = { + fallback: { + InvalidRoles: () =>
, + }, + private: [ + { + path: '/a', + element:
, + roles: ['admin'], + }, + ], + }; + const { getByTestId } = render( + + + , + { + wrapperProps: { + initialEntries: ['/a'], + }, + }, + ); + expect(getByTestId('invalid_roles')).toBeInTheDocument(); + }); + + it('[global fallback] should render InvalidRoles (allRolesRequired: true)', () => { + const routes: RoutesConfig = { + fallback: { + InvalidRoles: () =>
, + }, + private: [ + { + path: '/a', + element:
, + roles: ['admin', 'manager'], + allRolesRequired: true, + }, + ], + }; + const { getByTestId } = render( + + + , + { + wrapperProps: { + initialEntries: ['/a'], + }, + }, + ); + expect(getByTestId('invalid_roles')).toBeInTheDocument(); + }); + + it('[route fallback] should render InvalidRoles', () => { + const routes: RoutesConfig = { + private: [ + { + path: '/a', + element:
, + roles: ['admin', 'manager'], fallback: { - privateRoute: '/public', + InvalidRoles: () =>
, }, - public: [ - { - path: '/public', - element: <>[public] /, - }, - ], - private: [ - { - path: '/', - element: <>[private] /, - }], - }} - > + allRolesRequired: true, + }, + ], + }; + const { getByTestId } = render( + - , { + , + { wrapperProps: { - initialEntries: ['/'], + initialEntries: ['/a'], }, - }); - expect(screen.getByText('[public] /')).toBeInTheDocument(); + }, + ); + expect(getByTestId('invalid_roles')).toBeInTheDocument(); }); - it('should redirect to common route', async () => { - render( - [common] /, - }, - ], - public: [ - { - path: '/public', - element: <>[public] /, - }, - ], - private: [ - { - path: '/private', - element: <>[private] /, - }], - }} - > + it('[global fallback] should render Suspense', async () => { + const PageA = getLazyLoadedPageA(2000); + const routes: RoutesConfig = { + fallback: { + Suspense:
, + }, + private: [ + { + path: '/a', + element: , + }, + ], + }; + const { getByTestId } = render( + - , { + , + { wrapperProps: { - initialEntries: ['/private'], + initialEntries: ['/a'], }, - }); - expect(screen.getByText('[common] /')).toBeInTheDocument(); + }, + ); + expect(getByTestId('suspense_global')).toBeInTheDocument(); + + await waitFor( + () => { + expect(getByTestId('page_a')).toBeInTheDocument(); + }, + { + timeout: 2500, + }, + ); }); - it('should render InvalidRoles globally configured', async () => { - render( - { + const PageA = getLazyLoadedPageA(2000); + const routes: RoutesConfig = { + private: [ + { + path: '/a', + element: , fallback: { - InvalidRoles: () => <>[InvalidRoles] custom, + Suspense:
, }, - private: [ - { - path: '/private', - element: <>[private] /, - roles: ['admin'], - }], - }} - > + }, + ], + }; + const { getByTestId } = render( + - , { + , + { wrapperProps: { - initialEntries: ['/private'], + initialEntries: ['/a'], }, - }); - expect(screen.getByText('[InvalidRoles] custom')).toBeInTheDocument(); - }); + }, + ); -}); \ No newline at end of file + expect(getByTestId('suspense_route')).toBeInTheDocument(); + + await waitFor( + () => { + expect(getByTestId('page_a')).toBeInTheDocument(); + }, + { + timeout: 2500, + }, + ); + }); +});