// @vitest-environment jsdom /** * What the CommandPalette page claims, pinned. * * Every key on the page's Keys table has a test here. They were first walked * in Chrome on the stories (UIG-29, 16 September 2026), because a key that * works in jsdom can still fail in a browser; what is here keeps them from * quietly changing afterwards. Where Base UI did something the key list does * not want — Home and End moving the highlight, a late row taking it — the * test is the measured case. */ import { useState } from 'react' import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { CommandPalette, CommandPaletteAnswer, CommandPaletteForm, CommandPaletteProposal, CommandPaletteQuote, CommandPaletteSearch, CommandPaletteWorking, type CommandPaletteChip, type CommandPaletteGroup, type CommandPaletteRow, } from './CommandPalette' import { Field } from './Field' import { Select } from './Select' import { TextInput } from './TextInput' afterEach(cleanup) const row = (id: string, extra: Partial = {}): CommandPaletteRow => ({ id, label: id, onSelect: () => {}, ...extra }) function Search({ groups, chip, initialQuery = '', onOpenChange = () => {}, modKey, pending, notes, empty, }: { groups: CommandPaletteGroup[] chip?: CommandPaletteChip initialQuery?: string onOpenChange?: (open: boolean) => void modKey?: string pending?: string notes?: string[] empty?: string }) { const [query, setQuery] = useState(initialQuery) return ( ) } const field = () => screen.getByRole('combobox', { name: 'Search' }) as HTMLInputElement const lit = () => document.querySelector('[role="option"][data-highlighted] .truncate')?.textContent ?? null const footer = () => document.querySelector('.border-t.h-9')?.textContent ?? '' /** The field, once the dialog has put focus in it. */ async function focusedField() { await waitFor(() => expect(document.activeElement).toBe(field())) return field() } describe('CommandPalette — the window', () => { it('is a dialog named by its label, and starts in the field', async () => { render() expect(screen.getByRole('dialog', { name: 'Palette' })).toBeTruthy() await focusedField() }) it('closes on Escape, with text in the field too', async () => { const onOpenChange = vi.fn() const user = userEvent.setup() render() await focusedField() await user.keyboard('abc{Escape}') await waitFor(() => expect(onOpenChange).toHaveBeenCalledWith(false)) }) }) describe('CommandPaletteSearch — rows', () => { it('draws a heading per group, and no group that has no rows', () => { render() const groups = screen.getAllByRole('group') expect(groups.map((g) => g.firstElementChild?.textContent)).toEqual(['First', 'Second']) }) it('draws a row’s aside after its label, on the label’s line, above the second line (SUM-7)', () => { render() const option = screen.getByRole('option') expect(within(option).getByText('What we show on Friday').previousElementSibling?.textContent).toBe('Ship launchSHI-12 · Ship 1') }) it('puts 2px between its rows and under each heading, as a menu does', () => { render() const [first] = screen.getAllByRole('option') expect(first.parentElement!.className.split(' ')).toContain('gap-0.5') }) it('lights the first row, and Enter does what it does', async () => { const onSelect = vi.fn() const user = userEvent.setup() render() await focusedField() await waitFor(() => expect(lit()).toContain('One')) await user.keyboard('{Enter}') expect(onSelect).toHaveBeenCalledTimes(1) }) it('stops the arrows at both ends', async () => { const user = userEvent.setup() render() await focusedField() await user.keyboard('{ArrowUp}') expect(lit()).toContain('One') await user.keyboard('{ArrowDown}{ArrowDown}{ArrowDown}') expect(lit()).toContain('Two') }) it('goes in with Tab on a row that leads somewhere, and does nothing with Tab anywhere else', async () => { const onGoIn = vi.fn() const user = userEvent.setup() render() const input = await focusedField() await user.keyboard('{Tab}') expect(onGoIn).not.toHaveBeenCalled() expect(document.activeElement).toBe(input) await user.keyboard('{ArrowDown}{Tab}') expect(onGoIn).toHaveBeenCalledTimes(1) }) it('goes in with → only at the end of the text', async () => { const onGoIn = vi.fn() const user = userEvent.setup() render() const input = await focusedField() input.setSelectionRange(1, 1) await user.keyboard('{ArrowRight}') expect(onGoIn).not.toHaveBeenCalled() input.setSelectionRange(2, 2) await user.keyboard('{ArrowRight}') expect(onGoIn).toHaveBeenCalledTimes(1) }) it('forgets a row that can be forgotten with Ctrl+Backspace, and only that', async () => { const onForget = vi.fn() const user = userEvent.setup() render() await focusedField() await user.keyboard('{Control>}{Backspace}{/Control}') expect(onForget).not.toHaveBeenCalled() await user.keyboard('{ArrowDown}{Control>}{Backspace}{/Control}') expect(onForget).toHaveBeenCalledTimes(1) }) it('goes back with Backspace at the start of the field, inside a level only', async () => { const onBack = vi.fn() const user = userEvent.setup() const { unmount } = render() await focusedField() await user.keyboard('{Backspace}') unmount() render() const input = await focusedField() input.setSelectionRange(2, 2) await user.keyboard('{Backspace}') expect(onBack).not.toHaveBeenCalled() expect(input.value).toBe('a') input.setSelectionRange(0, 0) await user.keyboard('{Backspace}') expect(onBack).toHaveBeenCalledTimes(1) }) it("goes back from the chip's ✕, and leaves focus in the field", async () => { const onBack = vi.fn() const user = userEvent.setup() render() const input = await focusedField() await user.click(screen.getByRole('button', { name: 'Leave Place' })) expect(onBack).toHaveBeenCalledTimes(1) await waitFor(() => expect(document.activeElement).toBe(input)) }) it('keeps the lit row where it is on Home and End', async () => { const user = userEvent.setup() render() await focusedField() await user.keyboard('{ArrowDown}') expect(lit()).toContain('Two') await user.keyboard('{Home}') expect(lit()).toContain('Two') await user.keyboard('{End}') expect(lit()).toContain('Two') }) it('keeps the lit row lit when rows arrive above it (F7)', async () => { const user = userEvent.setup() const early: CommandPaletteGroup = { label: 'Early', rows: [row('One'), row('Two')] } const late: CommandPaletteGroup = { label: 'Late', rows: [row('Late one'), row('Late two')] } const { rerender } = render() await focusedField() await user.keyboard('{ArrowDown}') expect(lit()).toContain('Two') rerender() await waitFor(() => expect(screen.getAllByRole('option')).toHaveLength(4)) await waitFor(() => expect(lit()).toBe('Two')) }) it('lets rows arriving above take the first place while the highlight has not been moved', async () => { const early: CommandPaletteGroup = { label: 'Early', rows: [row('One'), row('Two')] } const late: CommandPaletteGroup = { label: 'Late', rows: [row('Late one')] } const { rerender } = render() await focusedField() await waitFor(() => expect(lit()).toBe('One')) rerender() await waitFor(() => expect(screen.getAllByRole('option')).toHaveLength(3)) await waitFor(() => expect(lit()).toBe('Late one')) }) it('names in the footer only the keys that work on the lit row', async () => { const user = userEvent.setup() render( {} }} groups={[{ label: 'Group', rows: [row('Plain'), row('Place', { onGoIn: () => {} }), row('Recent', { onForget: () => {} })] }]} />, ) await focusedField() expect(footer()).toContain('Enter') expect(footer()).not.toContain('forget') expect(footer()).toContain('Backspace') // back, while the field is empty await user.keyboard('{ArrowDown}') expect(footer()).toContain('Tab') await user.keyboard('{ArrowDown}') expect(footer()).toContain('Cmd+Backspace') await user.keyboard('x') expect(footer()).not.toContain('back') }) it('names no arrows with one row, and only Esc with none', async () => { const { unmount } = render() expect(footer()).not.toContain('move') unmount() render() expect(footer()).toContain('Esc') expect(footer()).not.toContain('open') expect(await screen.findByText('Nothing here yet.')).toBeTruthy() }) it('draws notes as lines under the rows, which the arrows do not stop on', async () => { const user = userEvent.setup() render() await focusedField() expect(await screen.findByText('2 more you cannot open.')).toBeTruthy() expect(screen.getAllByRole('option')).toHaveLength(2) await user.keyboard('{ArrowDown}{ArrowDown}{ArrowDown}') expect(lit()).toBe('Two') }) it('says rows are on their way', async () => { render() expect(await screen.findByText('Searching…')).toBeTruthy() }) }) function Form({ onSubmit = () => {}, onBack = () => {}, submitWaits, working, error, oneList = false, }: { onSubmit?: () => void onBack?: () => void submitWaits?: string working?: { button: string; line: string } error?: string oneList?: boolean }) { const [title, setTitle] = useState('') const [kind, setKind] = useState('one') const [missing, setMissing] = useState() return ( {}} label="Palette"> { if (!oneList && !title) setMissing('Give it a title.') else onSubmit() }} submitWaits={submitWaits} working={working} error={error} > {!oneList && ( setTitle(e.target.value)} /> )}