// @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('')
})
})