tooltip.spec.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. // @vitest-environment jsdom
  2. import { cleanup, fireEvent, render, screen } from '@testing-library/react'
  3. import { afterEach, describe, expect, it, vi } from 'vitest'
  4. import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
  5. afterEach(cleanup)
  6. describe('Tooltip', () => {
  7. it('shows the bubble to the right on hover and hides it on leave', () => {
  8. render(
  9. <Tooltip label="Open sidebar">
  10. <button type="button">anchor</button>
  11. </Tooltip>,
  12. )
  13. const anchor = screen.getByText('anchor')
  14. fireEvent.mouseEnter(anchor)
  15. const bubble = screen.getByRole('tooltip')
  16. expect(bubble.textContent).toBe('Open sidebar')
  17. expect(bubble.getAttribute('data-side')).toBe('right')
  18. // jsdom rects are all-zero: right placement lands at the +10 gutter.
  19. expect(bubble.style.left).toBe('10px')
  20. expect(bubble.style.top).toBe('0px')
  21. fireEvent.mouseLeave(anchor)
  22. expect(screen.queryByRole('tooltip')).toBeNull()
  23. })
  24. it('supports bottom placement and the focus/blur channel', () => {
  25. render(
  26. <Tooltip label="Below" side="bottom">
  27. <button type="button">anchor</button>
  28. </Tooltip>,
  29. )
  30. const anchor = screen.getByText('anchor')
  31. fireEvent.focus(anchor)
  32. const bubble = screen.getByRole('tooltip')
  33. expect(bubble.getAttribute('data-side')).toBe('bottom')
  34. expect(bubble.style.left).toBe('0px')
  35. expect(bubble.style.top).toBe('8px')
  36. fireEvent.blur(anchor)
  37. expect(screen.queryByRole('tooltip')).toBeNull()
  38. })
  39. it('chains the anchor\'s own handlers ahead of the tooltip\'s', () => {
  40. const onMouseEnter = vi.fn()
  41. const onMouseLeave = vi.fn()
  42. const onFocus = vi.fn()
  43. const onBlur = vi.fn()
  44. render(
  45. <Tooltip label="Chained">
  46. <button type="button" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} onFocus={onFocus} onBlur={onBlur}>anchor</button>
  47. </Tooltip>,
  48. )
  49. const anchor = screen.getByText('anchor')
  50. fireEvent.mouseEnter(anchor)
  51. fireEvent.mouseLeave(anchor)
  52. fireEvent.focus(anchor)
  53. fireEvent.blur(anchor)
  54. expect(onMouseEnter).toHaveBeenCalledOnce()
  55. expect(onMouseLeave).toHaveBeenCalledOnce()
  56. expect(onFocus).toHaveBeenCalledOnce()
  57. expect(onBlur).toHaveBeenCalledOnce()
  58. })
  59. it('suppresses the bubble while disabled without remounting the anchor', () => {
  60. const { rerender } = render(
  61. <Tooltip label="Rail" disabled>
  62. <button type="button">anchor</button>
  63. </Tooltip>,
  64. )
  65. const anchor = screen.getByText('anchor')
  66. fireEvent.mouseEnter(anchor)
  67. expect(screen.queryByRole('tooltip')).toBeNull()
  68. rerender(
  69. <Tooltip label="Rail">
  70. <button type="button">anchor</button>
  71. </Tooltip>,
  72. )
  73. // Same DOM node: toggling disabled never remounted the anchor.
  74. expect(screen.getByText('anchor')).toBe(anchor)
  75. fireEvent.mouseEnter(anchor)
  76. expect(screen.getByRole('tooltip')).toBeTruthy()
  77. })
  78. it('keeps the bubble while either hover or focus is still active', () => {
  79. render(
  80. <Tooltip label="Sticky">
  81. <button type="button">anchor</button>
  82. </Tooltip>,
  83. )
  84. const anchor = screen.getByText('anchor')
  85. // Focused AND hovered: leaving with the mouse must not drop the bubble.
  86. fireEvent.focus(anchor)
  87. fireEvent.mouseEnter(anchor)
  88. fireEvent.mouseLeave(anchor)
  89. expect(screen.getByRole('tooltip')).toBeTruthy()
  90. fireEvent.blur(anchor)
  91. expect(screen.queryByRole('tooltip')).toBeNull()
  92. // Symmetric: blurring while still hovered keeps it, mouseleave ends it.
  93. fireEvent.mouseEnter(anchor)
  94. fireEvent.focus(anchor)
  95. fireEvent.blur(anchor)
  96. expect(screen.getByRole('tooltip')).toBeTruthy()
  97. fireEvent.mouseLeave(anchor)
  98. expect(screen.queryByRole('tooltip')).toBeNull()
  99. })
  100. it('drops an already-visible bubble when disabled flips mid-hover', () => {
  101. const { rerender } = render(
  102. <Tooltip label="Rail">
  103. <button type="button">anchor</button>
  104. </Tooltip>,
  105. )
  106. fireEvent.mouseEnter(screen.getByText('anchor'))
  107. expect(screen.getByRole('tooltip')).toBeTruthy()
  108. // e.g. clicking a rail control expands the sidebar: no mouseleave fires.
  109. rerender(
  110. <Tooltip label="Rail" disabled>
  111. <button type="button">anchor</button>
  112. </Tooltip>,
  113. )
  114. expect(screen.queryByRole('tooltip')).toBeNull()
  115. })
  116. })