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