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