Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

command-selector.test.tsx 10.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. import React from 'react'
  2. import { fireEvent, render, screen } from '@testing-library/react'
  3. import '@testing-library/jest-dom'
  4. import CommandSelector from '../../app/components/goto-anything/command-selector'
  5. import type { ActionItem } from '../../app/components/goto-anything/actions/types'
  6. jest.mock('react-i18next', () => ({
  7. useTranslation: () => ({
  8. t: (key: string) => key,
  9. }),
  10. }))
  11. jest.mock('cmdk', () => ({
  12. Command: {
  13. Group: ({ children, className }: any) => <div className={className}>{children}</div>,
  14. Item: ({ children, onSelect, value, className }: any) => (
  15. <div
  16. className={className}
  17. onClick={() => onSelect && onSelect()}
  18. data-value={value}
  19. data-testid={`command-item-${value}`}
  20. >
  21. {children}
  22. </div>
  23. ),
  24. },
  25. }))
  26. describe('CommandSelector', () => {
  27. const mockActions: Record<string, ActionItem> = {
  28. app: {
  29. key: '@app',
  30. shortcut: '@app',
  31. title: 'Search Applications',
  32. description: 'Search apps',
  33. search: jest.fn(),
  34. },
  35. knowledge: {
  36. key: '@knowledge',
  37. shortcut: '@kb',
  38. title: 'Search Knowledge',
  39. description: 'Search knowledge bases',
  40. search: jest.fn(),
  41. },
  42. plugin: {
  43. key: '@plugin',
  44. shortcut: '@plugin',
  45. title: 'Search Plugins',
  46. description: 'Search plugins',
  47. search: jest.fn(),
  48. },
  49. node: {
  50. key: '@node',
  51. shortcut: '@node',
  52. title: 'Search Nodes',
  53. description: 'Search workflow nodes',
  54. search: jest.fn(),
  55. },
  56. }
  57. const mockOnCommandSelect = jest.fn()
  58. const mockOnCommandValueChange = jest.fn()
  59. beforeEach(() => {
  60. jest.clearAllMocks()
  61. })
  62. describe('Basic Rendering', () => {
  63. it('should render all actions when no filter is provided', () => {
  64. render(
  65. <CommandSelector
  66. actions={mockActions}
  67. onCommandSelect={mockOnCommandSelect}
  68. />,
  69. )
  70. expect(screen.getByTestId('command-item-@app')).toBeInTheDocument()
  71. expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument()
  72. expect(screen.getByTestId('command-item-@plugin')).toBeInTheDocument()
  73. expect(screen.getByTestId('command-item-@node')).toBeInTheDocument()
  74. })
  75. it('should render empty filter as showing all actions', () => {
  76. render(
  77. <CommandSelector
  78. actions={mockActions}
  79. onCommandSelect={mockOnCommandSelect}
  80. searchFilter=""
  81. />,
  82. )
  83. expect(screen.getByTestId('command-item-@app')).toBeInTheDocument()
  84. expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument()
  85. expect(screen.getByTestId('command-item-@plugin')).toBeInTheDocument()
  86. expect(screen.getByTestId('command-item-@node')).toBeInTheDocument()
  87. })
  88. })
  89. describe('Filtering Functionality', () => {
  90. it('should filter actions based on searchFilter - single match', () => {
  91. render(
  92. <CommandSelector
  93. actions={mockActions}
  94. onCommandSelect={mockOnCommandSelect}
  95. searchFilter="k"
  96. />,
  97. )
  98. expect(screen.queryByTestId('command-item-@app')).not.toBeInTheDocument()
  99. expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument()
  100. expect(screen.queryByTestId('command-item-@plugin')).not.toBeInTheDocument()
  101. expect(screen.queryByTestId('command-item-@node')).not.toBeInTheDocument()
  102. })
  103. it('should filter actions with multiple matches', () => {
  104. render(
  105. <CommandSelector
  106. actions={mockActions}
  107. onCommandSelect={mockOnCommandSelect}
  108. searchFilter="p"
  109. />,
  110. )
  111. expect(screen.getByTestId('command-item-@app')).toBeInTheDocument()
  112. expect(screen.queryByTestId('command-item-@kb')).not.toBeInTheDocument()
  113. expect(screen.getByTestId('command-item-@plugin')).toBeInTheDocument()
  114. expect(screen.queryByTestId('command-item-@node')).not.toBeInTheDocument()
  115. })
  116. it('should be case-insensitive when filtering', () => {
  117. render(
  118. <CommandSelector
  119. actions={mockActions}
  120. onCommandSelect={mockOnCommandSelect}
  121. searchFilter="APP"
  122. />,
  123. )
  124. expect(screen.getByTestId('command-item-@app')).toBeInTheDocument()
  125. expect(screen.queryByTestId('command-item-@kb')).not.toBeInTheDocument()
  126. })
  127. it('should match partial strings', () => {
  128. render(
  129. <CommandSelector
  130. actions={mockActions}
  131. onCommandSelect={mockOnCommandSelect}
  132. searchFilter="od"
  133. />,
  134. )
  135. expect(screen.queryByTestId('command-item-@app')).not.toBeInTheDocument()
  136. expect(screen.queryByTestId('command-item-@kb')).not.toBeInTheDocument()
  137. expect(screen.queryByTestId('command-item-@plugin')).not.toBeInTheDocument()
  138. expect(screen.getByTestId('command-item-@node')).toBeInTheDocument()
  139. })
  140. })
  141. describe('Empty State', () => {
  142. it('should show empty state when no matches found', () => {
  143. render(
  144. <CommandSelector
  145. actions={mockActions}
  146. onCommandSelect={mockOnCommandSelect}
  147. searchFilter="xyz"
  148. />,
  149. )
  150. expect(screen.queryByTestId('command-item-@app')).not.toBeInTheDocument()
  151. expect(screen.queryByTestId('command-item-@kb')).not.toBeInTheDocument()
  152. expect(screen.queryByTestId('command-item-@plugin')).not.toBeInTheDocument()
  153. expect(screen.queryByTestId('command-item-@node')).not.toBeInTheDocument()
  154. expect(screen.getByText('app.gotoAnything.noMatchingCommands')).toBeInTheDocument()
  155. expect(screen.getByText('app.gotoAnything.tryDifferentSearch')).toBeInTheDocument()
  156. })
  157. it('should not show empty state when filter is empty', () => {
  158. render(
  159. <CommandSelector
  160. actions={mockActions}
  161. onCommandSelect={mockOnCommandSelect}
  162. searchFilter=""
  163. />,
  164. )
  165. expect(screen.queryByText('app.gotoAnything.noMatchingCommands')).not.toBeInTheDocument()
  166. })
  167. })
  168. describe('Selection and Highlight Management', () => {
  169. it('should call onCommandValueChange when filter changes and first item differs', () => {
  170. const { rerender } = render(
  171. <CommandSelector
  172. actions={mockActions}
  173. onCommandSelect={mockOnCommandSelect}
  174. searchFilter=""
  175. commandValue="@app"
  176. onCommandValueChange={mockOnCommandValueChange}
  177. />,
  178. )
  179. rerender(
  180. <CommandSelector
  181. actions={mockActions}
  182. onCommandSelect={mockOnCommandSelect}
  183. searchFilter="k"
  184. commandValue="@app"
  185. onCommandValueChange={mockOnCommandValueChange}
  186. />,
  187. )
  188. expect(mockOnCommandValueChange).toHaveBeenCalledWith('@kb')
  189. })
  190. it('should not call onCommandValueChange if current value still exists', () => {
  191. const { rerender } = render(
  192. <CommandSelector
  193. actions={mockActions}
  194. onCommandSelect={mockOnCommandSelect}
  195. searchFilter=""
  196. commandValue="@app"
  197. onCommandValueChange={mockOnCommandValueChange}
  198. />,
  199. )
  200. rerender(
  201. <CommandSelector
  202. actions={mockActions}
  203. onCommandSelect={mockOnCommandSelect}
  204. searchFilter="a"
  205. commandValue="@app"
  206. onCommandValueChange={mockOnCommandValueChange}
  207. />,
  208. )
  209. expect(mockOnCommandValueChange).not.toHaveBeenCalled()
  210. })
  211. it('should handle onCommandSelect callback correctly', () => {
  212. render(
  213. <CommandSelector
  214. actions={mockActions}
  215. onCommandSelect={mockOnCommandSelect}
  216. searchFilter="k"
  217. />,
  218. )
  219. const knowledgeItem = screen.getByTestId('command-item-@kb')
  220. fireEvent.click(knowledgeItem)
  221. expect(mockOnCommandSelect).toHaveBeenCalledWith('@kb')
  222. })
  223. })
  224. describe('Edge Cases', () => {
  225. it('should handle empty actions object', () => {
  226. render(
  227. <CommandSelector
  228. actions={{}}
  229. onCommandSelect={mockOnCommandSelect}
  230. searchFilter=""
  231. />,
  232. )
  233. expect(screen.getByText('app.gotoAnything.noMatchingCommands')).toBeInTheDocument()
  234. })
  235. it('should handle special characters in filter', () => {
  236. render(
  237. <CommandSelector
  238. actions={mockActions}
  239. onCommandSelect={mockOnCommandSelect}
  240. searchFilter="@"
  241. />,
  242. )
  243. expect(screen.getByTestId('command-item-@app')).toBeInTheDocument()
  244. expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument()
  245. expect(screen.getByTestId('command-item-@plugin')).toBeInTheDocument()
  246. expect(screen.getByTestId('command-item-@node')).toBeInTheDocument()
  247. })
  248. it('should handle undefined onCommandValueChange gracefully', () => {
  249. const { rerender } = render(
  250. <CommandSelector
  251. actions={mockActions}
  252. onCommandSelect={mockOnCommandSelect}
  253. searchFilter=""
  254. />,
  255. )
  256. expect(() => {
  257. rerender(
  258. <CommandSelector
  259. actions={mockActions}
  260. onCommandSelect={mockOnCommandSelect}
  261. searchFilter="k"
  262. />,
  263. )
  264. }).not.toThrow()
  265. })
  266. })
  267. describe('Backward Compatibility', () => {
  268. it('should work without searchFilter prop (backward compatible)', () => {
  269. render(
  270. <CommandSelector
  271. actions={mockActions}
  272. onCommandSelect={mockOnCommandSelect}
  273. />,
  274. )
  275. expect(screen.getByTestId('command-item-@app')).toBeInTheDocument()
  276. expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument()
  277. expect(screen.getByTestId('command-item-@plugin')).toBeInTheDocument()
  278. expect(screen.getByTestId('command-item-@node')).toBeInTheDocument()
  279. })
  280. it('should work without commandValue and onCommandValueChange props', () => {
  281. render(
  282. <CommandSelector
  283. actions={mockActions}
  284. onCommandSelect={mockOnCommandSelect}
  285. searchFilter="k"
  286. />,
  287. )
  288. expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument()
  289. expect(screen.queryByTestId('command-item-@app')).not.toBeInTheDocument()
  290. })
  291. })
  292. })