import { jest } from '@jest/globals'; import { render, screen } from '@testing-library/react'; import GithubSlugger from 'github-slugger'; import mockRouter from 'next-router-mock'; import { MemoryRouterProvider } from 'next-router-mock/MemoryRouterProvider'; import { u as node } from 'unist-builder'; import { visit } from 'unist-util-visit'; import { findActiveRoute, Navigation } from './Navigation'; import { NavigationNode } from './types'; import { HeadingManager } from '~/common/headingManager'; import { HeadingsContext } from '~/components/page-higher-order/withHeadingManager'; const prepareHeadingManager = () => { const headingManager = new HeadingManager(new GithubSlugger(), { headings: [] }); return headingManager; }; jest.mock('next/router', () => mockRouter); /** A set of navigation nodes to test with */ const nodes: NavigationNode[] = [ node('section', { name: 'Get started' }, [ node('page', { name: 'Introduction', href: '/introduction' }), node('page', { name: 'Create a new app', href: '/introduction/create-new-app' }), node('page', { name: 'Errors and debugging', href: '/introduction/not-that-great-tbh' }), ]), node('section', { name: 'Tutorial' }, [ node('group', { name: 'First steps' }, [ node('page', { name: 'Styling text', href: '/tutorial/first-steps/styling-text' }), node('page', { name: 'Adding an image', href: '/tutorial/first-steps/adding-images' }), node('page', { name: 'Creating a button', href: '/tutorial/creating-button' }), ]), node('group', { name: 'Building apps' }, [ node('page', { name: 'Building for store', href: '/build/eas-build' }), node('page', { name: 'Submitting to store', href: '/build/eas-submit' }), ]), node('group', { name: 'Parallel universe', hidden: true }, [ node('page', { name: 'Create Flutter apps', href: '/parallel-universe/flutter' }), node('page', { name: 'Create websites', href: '/parallel-universe/ionic' }), node('page', { name: 'Create broken apps', href: '/parallel-universe/microsoft-uwp' }), ]), ]), ]; describe(Navigation, () => { it('renders pages', () => { const section = getNode(nodes, { name: 'Get started' }); render( ); // Get started -> expect(screen.getByText('Introduction')).toBeInTheDocument(); expect(screen.getByText('Create a new app')).toBeInTheDocument(); expect(screen.getByText('Errors and debugging')).toBeInTheDocument(); }); it('renders pages inside groups', () => { const section = getNode(nodes, { name: 'Tutorial' }); render( ); // Tutorial -> expect(screen.getByText('Building apps')).toBeInTheDocument(); // Tutorial -> Building apps -> expect(screen.getByText('Building for store')).toBeInTheDocument(); expect(screen.getByText('Submitting to store')).toBeInTheDocument(); }); it('renders pages inside groups inside sections', () => { const headingManager = prepareHeadingManager(); render( // Need context due to withHeadingManager in Collapsible, which enables anchor links ); // Get started -> expect(screen.getByText('Introduction')).toBeInTheDocument(); // Tutorial -> First steps -> expect(screen.getByText('Adding an image')).toBeInTheDocument(); // Tutorial -> Building apps -> expect(screen.getByText('Submitting to store')).toBeInTheDocument(); }); }); describe(findActiveRoute, () => { it('finds active page in list', () => { const section = getNode(nodes, { name: 'Get started' }); expect(findActiveRoute(children(section), '/introduction/create-new-app')).toMatchObject({ page: getNode(section, { name: 'Create a new app' }), group: null, section: null, }); }); it('finds active page and group in list', () => { const section = getNode(nodes, { name: 'Tutorial' }); expect(findActiveRoute(children(section), '/build/eas-submit')).toMatchObject({ page: getNode(section, { name: 'Submitting to store' }), group: getNode(section, { name: 'Building apps' }), section: null, }); }); it('finds active page, group, and section in list', () => { expect(findActiveRoute(nodes, '/tutorial/first-steps/styling-text')).toMatchObject({ page: getNode(nodes, { name: 'Styling text' }), group: getNode(nodes, { name: 'First steps' }), section: getNode(nodes, { name: 'Tutorial' }), }); }); it('skips hidden navigation node', () => { expect(findActiveRoute(nodes, '/parallel-universe/microsoft-uwp')).toMatchObject({ page: null, group: null, section: null, }); }); }); /** Helper function to find the first node that matches the predicate */ function getNode( list: NavigationNode | NavigationNode[] | null, predicate: Partial | ((node: NavigationNode) => boolean) ): NavigationNode | null { let result = null; const tree = Array.isArray(list) ? node('root', list) : list || node('root'); visit(tree, predicate as any, node => { result = node; }); return result; } /** Helper function to pull children from the node, if any */ function children(node: NavigationNode | null) { switch (node?.type) { case 'section': case 'group': return node.children; default: return []; } }