- The real-browser collapse smoke tracked the old chrome: visible HARNESS text (the wordmark svg is aria-hidden now), an 'Expand sidebar' label (renamed 'Open sidebar'), a 300px settle (default is 280), and an immediate focus assert (rail search defers focus past the slide). The case now tracks the brand span, polls the deferred focus, and uses the current labels and width. - Tooltip treated hover and focus as one trigger: leaving with the mouse dropped the bubble of a still-focused anchor (and vice versa). The two triggers are tracked independently; the bubble hides only after both clear. Spec pins both orders. - The ui-sidebar README and the bilingual collapse note still described the retired geometry morph; both now state the slide + crossfade contract, the fixed-width (never-conceding) sidebar, and the rail's whale-mark/tooltip chrome.
124 lines
4.3 KiB
TypeScript
124 lines
4.3 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
|
|
afterEach(cleanup)
|
|
|
|
describe('Tooltip', () => {
|
|
it('shows the bubble to the right on hover and hides it on leave', () => {
|
|
render(
|
|
<Tooltip label="Open sidebar">
|
|
<button type="button">anchor</button>
|
|
</Tooltip>,
|
|
)
|
|
const anchor = screen.getByText('anchor')
|
|
fireEvent.mouseEnter(anchor)
|
|
const bubble = screen.getByRole('tooltip')
|
|
expect(bubble.textContent).toBe('Open sidebar')
|
|
expect(bubble.getAttribute('data-side')).toBe('right')
|
|
// jsdom rects are all-zero: right placement lands at the +10 gutter.
|
|
expect(bubble.style.left).toBe('10px')
|
|
expect(bubble.style.top).toBe('0px')
|
|
fireEvent.mouseLeave(anchor)
|
|
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
})
|
|
|
|
it('supports bottom placement and the focus/blur channel', () => {
|
|
render(
|
|
<Tooltip label="Below" side="bottom">
|
|
<button type="button">anchor</button>
|
|
</Tooltip>,
|
|
)
|
|
const anchor = screen.getByText('anchor')
|
|
fireEvent.focus(anchor)
|
|
const bubble = screen.getByRole('tooltip')
|
|
expect(bubble.getAttribute('data-side')).toBe('bottom')
|
|
expect(bubble.style.left).toBe('0px')
|
|
expect(bubble.style.top).toBe('8px')
|
|
fireEvent.blur(anchor)
|
|
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
})
|
|
|
|
it('chains the anchor\'s own handlers ahead of the tooltip\'s', () => {
|
|
const onMouseEnter = vi.fn()
|
|
const onMouseLeave = vi.fn()
|
|
const onFocus = vi.fn()
|
|
const onBlur = vi.fn()
|
|
render(
|
|
<Tooltip label="Chained">
|
|
<button type="button" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} onFocus={onFocus} onBlur={onBlur}>anchor</button>
|
|
</Tooltip>,
|
|
)
|
|
const anchor = screen.getByText('anchor')
|
|
fireEvent.mouseEnter(anchor)
|
|
fireEvent.mouseLeave(anchor)
|
|
fireEvent.focus(anchor)
|
|
fireEvent.blur(anchor)
|
|
expect(onMouseEnter).toHaveBeenCalledOnce()
|
|
expect(onMouseLeave).toHaveBeenCalledOnce()
|
|
expect(onFocus).toHaveBeenCalledOnce()
|
|
expect(onBlur).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('suppresses the bubble while disabled without remounting the anchor', () => {
|
|
const { rerender } = render(
|
|
<Tooltip label="Rail" disabled>
|
|
<button type="button">anchor</button>
|
|
</Tooltip>,
|
|
)
|
|
const anchor = screen.getByText('anchor')
|
|
fireEvent.mouseEnter(anchor)
|
|
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
rerender(
|
|
<Tooltip label="Rail">
|
|
<button type="button">anchor</button>
|
|
</Tooltip>,
|
|
)
|
|
// Same DOM node: toggling disabled never remounted the anchor.
|
|
expect(screen.getByText('anchor')).toBe(anchor)
|
|
fireEvent.mouseEnter(anchor)
|
|
expect(screen.getByRole('tooltip')).toBeTruthy()
|
|
})
|
|
|
|
it('keeps the bubble while either hover or focus is still active', () => {
|
|
render(
|
|
<Tooltip label="Sticky">
|
|
<button type="button">anchor</button>
|
|
</Tooltip>,
|
|
)
|
|
const anchor = screen.getByText('anchor')
|
|
// Focused AND hovered: leaving with the mouse must not drop the bubble.
|
|
fireEvent.focus(anchor)
|
|
fireEvent.mouseEnter(anchor)
|
|
fireEvent.mouseLeave(anchor)
|
|
expect(screen.getByRole('tooltip')).toBeTruthy()
|
|
fireEvent.blur(anchor)
|
|
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
// Symmetric: blurring while still hovered keeps it, mouseleave ends it.
|
|
fireEvent.mouseEnter(anchor)
|
|
fireEvent.focus(anchor)
|
|
fireEvent.blur(anchor)
|
|
expect(screen.getByRole('tooltip')).toBeTruthy()
|
|
fireEvent.mouseLeave(anchor)
|
|
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
})
|
|
|
|
it('drops an already-visible bubble when disabled flips mid-hover', () => {
|
|
const { rerender } = render(
|
|
<Tooltip label="Rail">
|
|
<button type="button">anchor</button>
|
|
</Tooltip>,
|
|
)
|
|
fireEvent.mouseEnter(screen.getByText('anchor'))
|
|
expect(screen.getByRole('tooltip')).toBeTruthy()
|
|
// e.g. clicking a rail control expands the sidebar: no mouseleave fires.
|
|
rerender(
|
|
<Tooltip label="Rail" disabled>
|
|
<button type="button">anchor</button>
|
|
</Tooltip>,
|
|
)
|
|
expect(screen.queryByRole('tooltip')).toBeNull()
|
|
})
|
|
})
|