// @vitest-environment jsdom import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import { Button, ConnectionBanner, Input, Menu, Modal, Pill } from '@deepseek-ai/dsh-client-ui-primitives' afterEach(cleanup) describe('Button', () => { it('renders children, icon, and forwards clicks', () => { const onClick = vi.fn() render() const button = screen.getByRole('button', { name: 'Go' }) expect(screen.getByTestId('ic')).toBeDefined() fireEvent.click(button) expect(onClick).toHaveBeenCalledTimes(1) }) it('disabled blocks interaction', () => { const onClick = vi.fn() render() fireEvent.click(screen.getByRole('button')) expect(onClick).not.toHaveBeenCalled() }) it('outline variant renders a bordered cancel-style button', () => { render() expect(screen.getByRole('button', { name: 'Cancel' })).toBeDefined() }) }) describe('Pill', () => { it('is a span when static, a button when clickable', () => { const { rerender } = render(tab) expect(screen.queryByRole('button')).toBeNull() rerender( {}}>tab) expect(screen.getByRole('button', { name: 'tab' })).toBeDefined() }) it('active and className land on both static and interactive forms', () => { const { container, rerender } = render(tab) const asSpan = container.firstElementChild as HTMLElement expect(asSpan.classList.contains('x')).toBe(true) rerender( {}}>tab) const asButton = screen.getByRole('button') expect(asButton.classList.contains('x')).toBe(true) }) }) describe('Input', () => { it('forwards value/onChange and renders the leading icon', () => { const onChange = vi.fn() render(} value="q" onChange={onChange} placeholder="search" />) const input = screen.getByPlaceholderText('search') expect(input.value).toBe('q') fireEvent.change(input, { target: { value: 'qq' } }) expect(onChange).toHaveBeenCalled() expect(screen.getByTestId('ic')).toBeDefined() }) }) describe('Menu', () => { const items = [ { id: 'a', label: 'Alpha' }, { id: 'b', label: 'Beta', disabled: true }, ] it('shows items only while open; select fires onSelect', () => { const onSelect = vi.fn() const { rerender } = render( trigger} items={items} onSelect={onSelect} onClose={() => {}} />) expect(screen.queryByRole('menu')).toBeNull() rerender( trigger} items={items} selectedId="a" onSelect={onSelect} onClose={() => {}} />) fireEvent.click(screen.getByRole('menuitem', { name: 'Alpha' })) expect(onSelect).toHaveBeenCalledWith('a') }) it('disabled item does not select; Escape and outside pointerdown close', () => { const onSelect = vi.fn() const onClose = vi.fn() render( trigger} items={items} onSelect={onSelect} onClose={onClose} />) fireEvent.click(screen.getByRole('menuitem', { name: 'Beta' })) expect(onSelect).not.toHaveBeenCalled() fireEvent.keyDown(document, { key: 'Escape' }) expect(onClose).toHaveBeenCalledTimes(1) fireEvent.pointerDown(document.body) expect(onClose).toHaveBeenCalledTimes(2) }) it('inside pointerdown does not close', () => { const onClose = vi.fn() render( trigger} items={items} onSelect={() => {}} onClose={onClose} />) fireEvent.pointerDown(screen.getByRole('menuitem', { name: 'Alpha' })) expect(onClose).not.toHaveBeenCalled() }) it('selected item shows the trailing check; align=end, side=top, and className apply', () => { const { container } = render( trigger} items={items} selectedId="a" onSelect={() => {}} onClose={() => {}} />) expect((container.firstElementChild as HTMLElement).classList.contains('x')).toBe(true) const menu = screen.getByRole('menu') expect(menu.className).toMatch(/sideTop|alignEnd/) const selected = screen.getByRole('menuitem', { name: 'Alpha' }) expect(selected.querySelector('svg')).not.toBeNull() const other = screen.getByRole('menuitem', { name: 'Beta' }) expect(other.querySelector('svg')).toBeNull() fireEvent.keyDown(document, { key: 'a' }) }) it('renders a leading icon and a separator between groups', () => { render( trigger} items={[ { id: 'a', label: 'Alpha', icon: }, { type: 'separator', id: 's1' }, { id: 'c', label: 'Create' }, ]} onSelect={() => {}} onClose={() => {}} />) expect(screen.getByTestId('ic')).toBeDefined() expect(screen.getByRole('separator')).toBeDefined() }) it('opens a submenu on hover and selects a nested item', () => { const onSelect = vi.fn() render( trigger} items={[ { id: 'plain', label: 'Plain' }, { id: 'new', label: 'New Workspace', submenu: [ { id: 'ok', label: 'Create ok', icon: }, ], }, ]} onSelect={onSelect} onClose={() => {}} />) const plain = screen.getByRole('menuitem', { name: 'Plain' }) fireEvent.mouseEnter(plain.parentElement as HTMLElement) fireEvent.focus(plain) const parent = screen.getByRole('menuitem', { name: 'New Workspace' }) const wrap = parent.parentElement as HTMLElement fireEvent.click(parent) expect(onSelect).not.toHaveBeenCalled() fireEvent.focus(parent) fireEvent.mouseEnter(wrap) expect(screen.getByTestId('sub-ic')).toBeDefined() fireEvent.click(screen.getByRole('menuitem', { name: 'Create ok' })) expect(onSelect).toHaveBeenCalledWith('ok') fireEvent.mouseLeave(wrap) expect(screen.queryByRole('menuitem', { name: 'Create ok' })).toBeNull() }) }) describe('Modal', () => { it('is absent while closed; Escape and mask click call onClose', () => { const onClose = vi.fn() const { rerender } = render( body) expect(screen.queryByRole('dialog')).toBeNull() rerender( Create}> ) expect(screen.getByRole('dialog', { name: 'Create new workspace' })).toBeDefined() expect(screen.getByText('Name it.')).toBeDefined() fireEvent.keyDown(document, { key: 'a' }) expect(onClose).not.toHaveBeenCalled() fireEvent.keyDown(document, { key: 'Escape' }) expect(onClose).toHaveBeenCalledTimes(1) // Mask is the presentation sibling behind the dialog. const mask = document.querySelector('[aria-hidden="true"]') as HTMLElement fireEvent.click(mask) expect(onClose).toHaveBeenCalledTimes(2) }) }) describe('ConnectionBanner', () => { it('renders only while reconnecting', () => { const { container, rerender } = render() expect(container.firstChild).toBeNull() rerender() expect(container.textContent).toContain('重连') }) })