// @vitest-environment jsdom /** * What the page claims: every level is drawn from the one list; only the * group holding the current row starts open, and the current row is active; * a click reaches onSelect with its node; loading and empty say so. */ import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { MenuItem } from './Menu' import { NavTree, NavTreeSection, type NavTreeGroup } from './NavTree' afterEach(cleanup) const GROUPS: NavTreeGroup[] = [ { id: 'g1', title: 'Group one', nodes: [{ id: 'a', label: 'Item a', href: '#a' }] }, { id: 'g2', title: 'Group two', nodes: [{ id: 'b', label: 'Item b', href: '#b', children: [{ id: 'c', label: 'Item c', href: '#c', children: [{ id: 'd', label: 'Item d', href: '#d' }] }] }], }, ] const hidden = (text: string) => screen.getByText(text).closest('[hidden]') !== null describe('NavTree', () => { it('draws every level, and opens only the group holding the current row', () => { render() expect(hidden('Item a')).toBe(true) expect(hidden('Item d')).toBe(false) expect(screen.getByRole('link', { name: 'Item d' }).getAttribute('aria-current')).toBe('page') }) it('a click reaches onSelect with the node it was on', async () => { const onSelect = vi.fn((_node, event: { preventDefault: () => void }) => event.preventDefault()) render() await userEvent.click(screen.getByRole('link', { name: 'Item c' })) expect(onSelect.mock.calls[0][0].id).toBe('c') }) it('draws the quiet label when given one', () => { render() expect(screen.getByText('Groups')).not.toBeNull() }) it('says it is empty in the caller’s words', () => { render() expect(screen.getByText('Nothing here yet.')).not.toBeNull() }) it('draws no rows while loading', () => { render() expect(screen.queryAllByRole('link')).toHaveLength(0) }) }) describe('NavTree read as it opens', () => { it('a group being read draws no rows; a group’s message is said in their place', () => { render( , ) expect(screen.queryAllByRole('link')).toHaveLength(0) expect(screen.getByText('Nothing in this yet.')).not.toBeNull() }) it('a row with hasChildren has its arrow, and says when it opens', async () => { const onOpenChange = vi.fn() render( , ) await userEvent.click(screen.getByRole('button', { name: /Show what is under Item x/ })) expect(onOpenChange).toHaveBeenCalledWith(true) }) it('the label’s and a group’s actions are drawn', () => { render( , tooltip: 'New', onClick: () => {} }]} groups={[{ id: 'a', title: 'Group a', nodes: [], actions: [{ icon: , tooltip: 'Open', onClick: () => {} }] }]} emptyMessage="Nothing here yet." />, ) expect(screen.getByRole('button', { name: 'New' })).not.toBeNull() expect(screen.getByRole('button', { name: 'Open' })).not.toBeNull() }) it('a group’s menu is a ⋮ named for the group, and opens its items', async () => { render( }]} />) await userEvent.click(screen.getByRole('button', { name: 'More options for Group a' })) expect(await screen.findByRole('menuitem', { name: 'Rename' })).not.toBeNull() }) it('onIntent: the keyboard reaching a group says so at once', () => { const onIntent = vi.fn() render() screen.getByRole('button', { name: 'Group a' }).focus() expect(onIntent).toHaveBeenCalled() }) }) describe('NavTree drawn by the app', () => { it('draws NavTreeSections given as children, each with its rows, line or skeleton', () => { render( Item a , ) expect(screen.getByRole('button', { name: 'Group a' })).not.toBeNull() expect(screen.getByText('Item a')).not.toBeNull() expect(screen.getByText('Nothing in this yet.')).not.toBeNull() }) it('a pending NavTreeSection says open and busy, its rows shut until it is not (OPT-11)', () => { const { rerender } = render(Item a) const heading = screen.getByRole('button', { name: 'Group a' }) expect(heading.getAttribute('aria-expanded')).toBe('true') expect(heading.getAttribute('aria-busy')).toBe('true') expect(screen.getByText('Item a').closest('[hidden]')).not.toBeNull() rerender(Item a) expect(screen.getByText('Item a').closest('[hidden]')).toBeNull() }) }) describe('NavTree marks its groups', () => { it('each group box carries data-nav-group with its id', () => { const { container } = render() expect(container.querySelector('[data-nav-group="g1"]')).not.toBeNull() }) }) /** * 4px between groups and none above the first, title or no title. The margin * was `mt-1 first:mt-0`, and the quiet title is drawn before the groups, so * the first group under a title was never the first child and took the 4px * (measured in Chrome: 6px under the title, 2px after). */ describe('NavTree spaces its groups', () => { it('only a group after a group takes the 4px, so the first sits at the column’s 2px under the title', () => { const { container } = render() const boxes = [...container.querySelectorAll('[data-nav-group]')] for (const box of boxes) { const classes = box.className.split(' ') expect(classes).toContain('[&+&]:mt-1') expect(classes).not.toContain('mt-1') } // The selector's own terms: the first group follows the title, which is no group. expect(boxes[0].previousElementSibling!.className.split(' ')).not.toContain('[&+&]:mt-1') expect(boxes[1].previousElementSibling).toBe(boxes[0]) }) }) describe('NavTree with no groups and no words', () => { it('draws no line at all', () => { const { container } = render() expect(container.textContent).toBe('') }) })