1import { jest } from '@jest/globals';
2import { render, screen } from '@testing-library/react';
3import mockRouter from 'next-router-mock';
4import { MemoryRouterProvider } from 'next-router-mock/MemoryRouterProvider';
5import { u as node } from 'unist-builder';
6import { visit } from 'unist-util-visit';
7
8import { findActiveRoute, Navigation } from './Navigation';
9import { NavigationNode } from './types';
10
11jest.mock('next/router', () => mockRouter);
12
13/** A set of navigation nodes to test with */
14const nodes: NavigationNode[] = [
15  node('section', { name: 'Get started' }, [
16    node('page', { name: 'Introduction', href: '/introduction' }),
17    node('page', { name: 'Create a new app', href: '/introduction/create-new-app' }),
18    node('page', { name: 'Errors and debugging', href: '/introduction/not-that-great-tbh' }),
19  ]),
20  node('section', { name: 'Tutorial' }, [
21    node('group', { name: 'First steps' }, [
22      node('page', { name: 'Styling text', href: '/tutorial/first-steps/styling-text' }),
23      node('page', { name: 'Adding an image', href: '/tutorial/first-steps/adding-images' }),
24      node('page', { name: 'Creating a button', href: '/tutorial/creating-button' }),
25    ]),
26    node('group', { name: 'Building apps' }, [
27      node('page', { name: 'Building for store', href: '/build/eas-build' }),
28      node('page', { name: 'Submitting to store', href: '/build/eas-submit' }),
29    ]),
30    node('group', { name: 'Parallel universe', hidden: true }, [
31      node('page', { name: 'Create Flutter apps', href: '/parallel-universe/flutter' }),
32      node('page', { name: 'Create websites', href: '/parallel-universe/ionic' }),
33      node('page', { name: 'Create broken apps', href: '/parallel-universe/microsoft-uwp' }),
34    ]),
35  ]),
36];
37
38describe(Navigation, () => {
39  it('renders pages', () => {
40    const section = getNode(nodes, { name: 'Get started' });
41    render(
42      <MemoryRouterProvider>
43        <Navigation routes={children(section)} />
44      </MemoryRouterProvider>
45    );
46    // Get started ->
47    expect(screen.getByText('Introduction')).toBeInTheDocument();
48    expect(screen.getByText('Create a new app')).toBeInTheDocument();
49    expect(screen.getByText('Errors and debugging')).toBeInTheDocument();
50  });
51
52  it('renders pages inside groups', () => {
53    const section = getNode(nodes, { name: 'Tutorial' });
54    render(
55      <MemoryRouterProvider>
56        <Navigation routes={children(section)} />
57      </MemoryRouterProvider>
58    );
59    // Tutorial ->
60    expect(screen.getByText('Building apps')).toBeInTheDocument();
61    // Tutorial -> Building apps ->
62    expect(screen.getByText('Building for store')).toBeInTheDocument();
63    expect(screen.getByText('Submitting to store')).toBeInTheDocument();
64  });
65
66  it('renders pages inside groups inside sections', () => {
67    render(
68      <MemoryRouterProvider>
69        <Navigation routes={nodes} />
70      </MemoryRouterProvider>
71    );
72    // Get started ->
73    expect(screen.getByText('Introduction')).toBeInTheDocument();
74    // Tutorial -> First steps ->
75    expect(screen.getByText('Adding an image')).toBeInTheDocument();
76    // Tutorial -> Building apps ->
77    expect(screen.getByText('Submitting to store')).toBeInTheDocument();
78  });
79});
80
81describe(findActiveRoute, () => {
82  it('finds active page in list', () => {
83    const section = getNode(nodes, { name: 'Get started' });
84    expect(findActiveRoute(children(section), '/introduction/create-new-app')).toMatchObject({
85      page: getNode(section, { name: 'Create a new app' }),
86      group: null,
87      section: null,
88    });
89  });
90
91  it('finds active page and group in list', () => {
92    const section = getNode(nodes, { name: 'Tutorial' });
93    expect(findActiveRoute(children(section), '/build/eas-submit')).toMatchObject({
94      page: getNode(section, { name: 'Submitting to store' }),
95      group: getNode(section, { name: 'Building apps' }),
96      section: null,
97    });
98  });
99
100  it('finds active page, group, and section in list', () => {
101    expect(findActiveRoute(nodes, '/tutorial/first-steps/styling-text')).toMatchObject({
102      page: getNode(nodes, { name: 'Styling text' }),
103      group: getNode(nodes, { name: 'First steps' }),
104      section: getNode(nodes, { name: 'Tutorial' }),
105    });
106  });
107
108  it('skips hidden navigation node', () => {
109    expect(findActiveRoute(nodes, '/parallel-universe/microsoft-uwp')).toMatchObject({
110      page: null,
111      group: null,
112      section: null,
113    });
114  });
115});
116
117/** Helper function to find the first node that matches the predicate */
118function getNode(
119  list: NavigationNode | NavigationNode[] | null,
120  predicate: Partial<NavigationNode> | ((node: NavigationNode) => boolean)
121): NavigationNode | null {
122  let result = null;
123  const tree = Array.isArray(list) ? node('root', list) : list || node('root');
124  visit(tree, predicate as any, node => {
125    result = node;
126  });
127  return result;
128}
129
130/** Helper function to pull children from the node, if any */
131function children(node: NavigationNode | null) {
132  switch (node?.type) {
133    case 'section':
134    case 'group':
135      return node.children;
136    default:
137      return [];
138  }
139}
140