@startsimpli/ui 0.4.7 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/package.json +1 -1
  2. package/src/__mocks__/next/link.js +11 -0
  3. package/src/components/account/__tests__/account.test.tsx +315 -0
  4. package/src/components/command-palette/CommandGroup.tsx +23 -0
  5. package/src/components/command-palette/CommandPalette.tsx +183 -200
  6. package/src/components/command-palette/CommandResultItem.tsx +59 -0
  7. package/src/components/command-palette/__tests__/CommandGroup.test.tsx +81 -0
  8. package/src/components/command-palette/__tests__/CommandResultItem.test.tsx +166 -0
  9. package/src/components/command-palette/__tests__/command-palette-context.test.tsx +166 -0
  10. package/src/components/command-palette/__tests__/useCommandPaletteSearch.test.ts +271 -0
  11. package/src/components/command-palette/index.ts +6 -0
  12. package/src/components/command-palette/useCommandPaletteSearch.ts +114 -0
  13. package/src/components/compose/__tests__/compose.test.tsx +656 -0
  14. package/src/components/dashboard/PipelineFunnel.tsx +126 -0
  15. package/src/components/dashboard/TopCampaigns.tsx +132 -0
  16. package/src/components/dashboard/__tests__/dashboard.test.tsx +785 -0
  17. package/src/components/dashboard/index.ts +6 -0
  18. package/src/components/dialog/ConfirmDialog.tsx +72 -0
  19. package/src/components/dialog/__tests__/ConfirmDialog.test.tsx +126 -0
  20. package/src/components/dialog/index.ts +3 -0
  21. package/src/components/email-dialogs/__tests__/email-dialogs.test.tsx +982 -0
  22. package/src/components/email-editor/BlockRenderer.tsx +120 -0
  23. package/src/components/email-editor/__tests__/BlockRenderer.test.tsx +332 -0
  24. package/src/components/email-editor/__tests__/block-renderers.test.ts +624 -0
  25. package/src/components/email-editor/__tests__/email-html-renderer.test.ts +376 -0
  26. package/src/components/email-editor/blocks/__tests__/blocks.test.tsx +818 -0
  27. package/src/components/email-editor/editor-sidebar.tsx +6 -731
  28. package/src/components/email-editor/email-editor.tsx +78 -467
  29. package/src/components/email-editor/hooks/__tests__/useDragDrop.test.ts +355 -0
  30. package/src/components/email-editor/hooks/__tests__/useEmailEditorState.test.ts +551 -0
  31. package/src/components/email-editor/hooks/useDragDrop.ts +181 -0
  32. package/src/components/email-editor/hooks/useEmailEditorState.ts +426 -0
  33. package/src/components/email-editor/index.ts +1 -0
  34. package/src/components/email-editor/panels/BlockPropertyPanel.tsx +637 -0
  35. package/src/components/email-editor/panels/GlobalStylesPanel.tsx +108 -0
  36. package/src/components/email-editor/panels/SectionSettingsPanel.tsx +80 -0
  37. package/src/components/email-editor/panels/__tests__/BlockPropertyPanel.test.tsx +707 -0
  38. package/src/components/email-editor/panels/__tests__/GlobalStylesPanel.test.tsx +226 -0
  39. package/src/components/email-editor/panels/index.ts +3 -0
  40. package/src/components/enrichment/__tests__/enrichment.test.tsx +184 -0
  41. package/src/components/gantt/GanttBoardView.tsx +71 -0
  42. package/src/components/gantt/GanttChart.tsx +134 -881
  43. package/src/components/gantt/GanttFilterBar.tsx +100 -0
  44. package/src/components/gantt/GanttListView.tsx +63 -0
  45. package/src/components/gantt/GanttTimelineView.tsx +215 -0
  46. package/src/components/gantt/__tests__/GanttBoardView.test.tsx +305 -0
  47. package/src/components/gantt/__tests__/GanttFilterBar.test.tsx +544 -0
  48. package/src/components/gantt/__tests__/GanttListView.test.tsx +337 -0
  49. package/src/components/gantt/__tests__/GanttTimelineView.test.tsx +375 -0
  50. package/src/components/gantt/__tests__/gantt-utils.test.ts +341 -0
  51. package/src/components/gantt/__tests__/useGanttState.test.ts +535 -0
  52. package/src/components/gantt/hooks/useGanttState.ts +644 -0
  53. package/src/components/gantt/index.ts +10 -0
  54. package/src/components/integrations/__tests__/integrations.test.tsx +191 -0
  55. package/src/components/kanban/__tests__/kanban.test.tsx +157 -0
  56. package/src/components/lists/__tests__/lists.test.tsx +263 -0
  57. package/src/components/loading/__tests__/loading.test.tsx +114 -0
  58. package/src/components/navigation/__tests__/navigation.test.tsx +194 -0
  59. package/src/components/pipeline/__tests__/pipeline.test.tsx +169 -0
  60. package/src/components/settings/__tests__/settings.test.tsx +181 -0
  61. package/src/components/wizard/__tests__/wizard.test.tsx +97 -0
@@ -0,0 +1,544 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react'
2
+ import { GanttFilterBar } from '../GanttFilterBar'
3
+ import type { GanttFilterState } from '../types'
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Fixtures
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const emptyFilters: GanttFilterState = {
10
+ search: '',
11
+ statuses: [],
12
+ categories: [],
13
+ dateRange: { start: null, end: null },
14
+ }
15
+
16
+ function makeFilters(overrides: Partial<GanttFilterState> = {}): GanttFilterState {
17
+ return { ...emptyFilters, ...overrides }
18
+ }
19
+
20
+ const STATUSES = ['not_started', 'in_progress', 'completed']
21
+ const CATEGORIES = ['product', 'team', 'financial']
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Search input
25
+ // ---------------------------------------------------------------------------
26
+
27
+ describe('GanttFilterBar — search input', () => {
28
+ it('renders a search input with placeholder text', () => {
29
+ const onFilterChange = jest.fn()
30
+ render(
31
+ <GanttFilterBar
32
+ filters={emptyFilters}
33
+ onFilterChange={onFilterChange}
34
+ uniqueStatuses={[]}
35
+ uniqueCategories={[]}
36
+ />
37
+ )
38
+ expect(screen.getByPlaceholderText('Search items...')).toBeInTheDocument()
39
+ })
40
+
41
+ it('reflects the current search value', () => {
42
+ const onFilterChange = jest.fn()
43
+ render(
44
+ <GanttFilterBar
45
+ filters={makeFilters({ search: 'hello' })}
46
+ onFilterChange={onFilterChange}
47
+ uniqueStatuses={[]}
48
+ uniqueCategories={[]}
49
+ />
50
+ )
51
+ expect(screen.getByPlaceholderText('Search items...')).toHaveValue('hello')
52
+ })
53
+
54
+ it('calls onFilterChange with updated search when text is typed', () => {
55
+ const onFilterChange = jest.fn()
56
+ render(
57
+ <GanttFilterBar
58
+ filters={emptyFilters}
59
+ onFilterChange={onFilterChange}
60
+ uniqueStatuses={[]}
61
+ uniqueCategories={[]}
62
+ />
63
+ )
64
+ fireEvent.change(screen.getByPlaceholderText('Search items...'), {
65
+ target: { value: 'alpha' },
66
+ })
67
+ expect(onFilterChange).toHaveBeenCalledTimes(1)
68
+ expect(onFilterChange).toHaveBeenCalledWith(
69
+ expect.objectContaining({ search: 'alpha' })
70
+ )
71
+ })
72
+
73
+ it('preserves existing filter values when search changes', () => {
74
+ const onFilterChange = jest.fn()
75
+ const filters = makeFilters({ statuses: ['completed'], search: '' })
76
+ render(
77
+ <GanttFilterBar
78
+ filters={filters}
79
+ onFilterChange={onFilterChange}
80
+ uniqueStatuses={STATUSES}
81
+ uniqueCategories={[]}
82
+ />
83
+ )
84
+ fireEvent.change(screen.getByPlaceholderText('Search items...'), {
85
+ target: { value: 'new term' },
86
+ })
87
+ const called = onFilterChange.mock.calls[0][0] as GanttFilterState
88
+ expect(called.statuses).toEqual(['completed'])
89
+ expect(called.search).toBe('new term')
90
+ })
91
+ })
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Status dropdown
95
+ // ---------------------------------------------------------------------------
96
+
97
+ describe('GanttFilterBar — status dropdown', () => {
98
+ it('does not render status select when uniqueStatuses is empty', () => {
99
+ const onFilterChange = jest.fn()
100
+ render(
101
+ <GanttFilterBar
102
+ filters={emptyFilters}
103
+ onFilterChange={onFilterChange}
104
+ uniqueStatuses={[]}
105
+ uniqueCategories={[]}
106
+ />
107
+ )
108
+ // The select for statuses should be absent
109
+ const selects = screen.queryAllByRole('combobox')
110
+ expect(selects).toHaveLength(0)
111
+ })
112
+
113
+ it('renders status options for each unique status', () => {
114
+ const onFilterChange = jest.fn()
115
+ render(
116
+ <GanttFilterBar
117
+ filters={emptyFilters}
118
+ onFilterChange={onFilterChange}
119
+ uniqueStatuses={STATUSES}
120
+ uniqueCategories={[]}
121
+ />
122
+ )
123
+ // The default empty option + one per status
124
+ const select = screen.getByRole('combobox')
125
+ expect(select).toBeInTheDocument()
126
+ expect(screen.getByRole('option', { name: /not started/ })).toBeInTheDocument()
127
+ expect(screen.getByRole('option', { name: /in progress/ })).toBeInTheDocument()
128
+ expect(screen.getByRole('option', { name: /completed/ })).toBeInTheDocument()
129
+ })
130
+
131
+ it('shows active count in the status label when statuses are selected', () => {
132
+ const onFilterChange = jest.fn()
133
+ render(
134
+ <GanttFilterBar
135
+ filters={makeFilters({ statuses: ['completed', 'in_progress'] })}
136
+ onFilterChange={onFilterChange}
137
+ uniqueStatuses={STATUSES}
138
+ uniqueCategories={[]}
139
+ />
140
+ )
141
+ expect(screen.getByRole('option', { name: /Status \(2\)/ })).toBeInTheDocument()
142
+ })
143
+
144
+ it('calls onFilterChange with the new status added when a status is selected', () => {
145
+ const onFilterChange = jest.fn()
146
+ render(
147
+ <GanttFilterBar
148
+ filters={emptyFilters}
149
+ onFilterChange={onFilterChange}
150
+ uniqueStatuses={STATUSES}
151
+ uniqueCategories={[]}
152
+ />
153
+ )
154
+ fireEvent.change(screen.getByRole('combobox'), { target: { value: 'completed' } })
155
+ const called = onFilterChange.mock.calls[0][0] as GanttFilterState
156
+ expect(called.statuses).toContain('completed')
157
+ })
158
+
159
+ it('removes a status when it is selected a second time (toggle off)', () => {
160
+ const onFilterChange = jest.fn()
161
+ // Start with 'completed' already active
162
+ render(
163
+ <GanttFilterBar
164
+ filters={makeFilters({ statuses: ['completed'] })}
165
+ onFilterChange={onFilterChange}
166
+ uniqueStatuses={STATUSES}
167
+ uniqueCategories={[]}
168
+ />
169
+ )
170
+ fireEvent.change(screen.getByRole('combobox'), { target: { value: 'completed' } })
171
+ const called = onFilterChange.mock.calls[0][0] as GanttFilterState
172
+ expect(called.statuses).not.toContain('completed')
173
+ })
174
+
175
+ it('does not call onFilterChange when the empty placeholder option is selected', () => {
176
+ const onFilterChange = jest.fn()
177
+ render(
178
+ <GanttFilterBar
179
+ filters={emptyFilters}
180
+ onFilterChange={onFilterChange}
181
+ uniqueStatuses={STATUSES}
182
+ uniqueCategories={[]}
183
+ />
184
+ )
185
+ fireEvent.change(screen.getByRole('combobox'), { target: { value: '' } })
186
+ expect(onFilterChange).not.toHaveBeenCalled()
187
+ })
188
+ })
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Category dropdown
192
+ // ---------------------------------------------------------------------------
193
+
194
+ describe('GanttFilterBar — category dropdown', () => {
195
+ it('does not render category select when uniqueCategories is empty', () => {
196
+ const onFilterChange = jest.fn()
197
+ render(
198
+ <GanttFilterBar
199
+ filters={emptyFilters}
200
+ onFilterChange={onFilterChange}
201
+ uniqueStatuses={[]}
202
+ uniqueCategories={[]}
203
+ />
204
+ )
205
+ expect(screen.queryAllByRole('combobox')).toHaveLength(0)
206
+ })
207
+
208
+ it('renders a category select with one option per category', () => {
209
+ const onFilterChange = jest.fn()
210
+ render(
211
+ <GanttFilterBar
212
+ filters={emptyFilters}
213
+ onFilterChange={onFilterChange}
214
+ uniqueStatuses={[]}
215
+ uniqueCategories={CATEGORIES}
216
+ />
217
+ )
218
+ const selects = screen.getAllByRole('combobox')
219
+ expect(selects).toHaveLength(1)
220
+ expect(screen.getByRole('option', { name: 'product' })).toBeInTheDocument()
221
+ expect(screen.getByRole('option', { name: 'team' })).toBeInTheDocument()
222
+ expect(screen.getByRole('option', { name: 'financial' })).toBeInTheDocument()
223
+ })
224
+
225
+ it('renders both status and category selects when both arrays have values', () => {
226
+ const onFilterChange = jest.fn()
227
+ render(
228
+ <GanttFilterBar
229
+ filters={emptyFilters}
230
+ onFilterChange={onFilterChange}
231
+ uniqueStatuses={STATUSES}
232
+ uniqueCategories={CATEGORIES}
233
+ />
234
+ )
235
+ expect(screen.getAllByRole('combobox')).toHaveLength(2)
236
+ })
237
+
238
+ it('calls onFilterChange with category added when a category is selected', () => {
239
+ const onFilterChange = jest.fn()
240
+ render(
241
+ <GanttFilterBar
242
+ filters={emptyFilters}
243
+ onFilterChange={onFilterChange}
244
+ uniqueStatuses={[]}
245
+ uniqueCategories={CATEGORIES}
246
+ />
247
+ )
248
+ fireEvent.change(screen.getByRole('combobox'), { target: { value: 'product' } })
249
+ const called = onFilterChange.mock.calls[0][0] as GanttFilterState
250
+ expect(called.categories).toContain('product')
251
+ })
252
+
253
+ it('removes a category when toggled off', () => {
254
+ const onFilterChange = jest.fn()
255
+ render(
256
+ <GanttFilterBar
257
+ filters={makeFilters({ categories: ['product'] })}
258
+ onFilterChange={onFilterChange}
259
+ uniqueStatuses={[]}
260
+ uniqueCategories={CATEGORIES}
261
+ />
262
+ )
263
+ fireEvent.change(screen.getByRole('combobox'), { target: { value: 'product' } })
264
+ const called = onFilterChange.mock.calls[0][0] as GanttFilterState
265
+ expect(called.categories).not.toContain('product')
266
+ })
267
+
268
+ it('shows active count in the category label when categories are selected', () => {
269
+ const onFilterChange = jest.fn()
270
+ render(
271
+ <GanttFilterBar
272
+ filters={makeFilters({ categories: ['product', 'team'] })}
273
+ onFilterChange={onFilterChange}
274
+ uniqueStatuses={[]}
275
+ uniqueCategories={CATEGORIES}
276
+ />
277
+ )
278
+ expect(screen.getByRole('option', { name: /Category \(2\)/ })).toBeInTheDocument()
279
+ })
280
+ })
281
+
282
+ // ---------------------------------------------------------------------------
283
+ // Date range inputs
284
+ // ---------------------------------------------------------------------------
285
+
286
+ describe('GanttFilterBar — date range inputs', () => {
287
+ it('renders two date inputs', () => {
288
+ const onFilterChange = jest.fn()
289
+ render(
290
+ <GanttFilterBar
291
+ filters={emptyFilters}
292
+ onFilterChange={onFilterChange}
293
+ uniqueStatuses={[]}
294
+ uniqueCategories={[]}
295
+ />
296
+ )
297
+ const dateInputs = screen.getAllByDisplayValue('')
298
+ // Both date inputs start empty plus the search input — filter by type
299
+ const typedDateInputs = document.querySelectorAll('input[type="date"]')
300
+ expect(typedDateInputs).toHaveLength(2)
301
+ })
302
+
303
+ it('populates the start date input when dateRange.start is set', () => {
304
+ const onFilterChange = jest.fn()
305
+ // Use local-time constructor (year, month-1, day) to avoid UTC→local timezone shift
306
+ render(
307
+ <GanttFilterBar
308
+ filters={makeFilters({ dateRange: { start: new Date(2025, 2, 15), end: null } })}
309
+ onFilterChange={onFilterChange}
310
+ uniqueStatuses={[]}
311
+ uniqueCategories={[]}
312
+ />
313
+ )
314
+ const dateInputs = document.querySelectorAll('input[type="date"]')
315
+ expect((dateInputs[0] as HTMLInputElement).value).toBe('2025-03-15')
316
+ })
317
+
318
+ it('populates the end date input when dateRange.end is set', () => {
319
+ const onFilterChange = jest.fn()
320
+ // Use local-time constructor (year, month-1, day) to avoid UTC→local timezone shift
321
+ render(
322
+ <GanttFilterBar
323
+ filters={makeFilters({ dateRange: { start: null, end: new Date(2025, 5, 30) } })}
324
+ onFilterChange={onFilterChange}
325
+ uniqueStatuses={[]}
326
+ uniqueCategories={[]}
327
+ />
328
+ )
329
+ const dateInputs = document.querySelectorAll('input[type="date"]')
330
+ expect((dateInputs[1] as HTMLInputElement).value).toBe('2025-06-30')
331
+ })
332
+
333
+ it('calls onFilterChange with updated start date when start input changes', () => {
334
+ const onFilterChange = jest.fn()
335
+ render(
336
+ <GanttFilterBar
337
+ filters={emptyFilters}
338
+ onFilterChange={onFilterChange}
339
+ uniqueStatuses={[]}
340
+ uniqueCategories={[]}
341
+ />
342
+ )
343
+ const dateInputs = document.querySelectorAll('input[type="date"]')
344
+ fireEvent.change(dateInputs[0], { target: { value: '2025-01-01' } })
345
+ const called = onFilterChange.mock.calls[0][0] as GanttFilterState
346
+ expect(called.dateRange.start).not.toBeNull()
347
+ })
348
+
349
+ it('sets start date to null when start input is cleared', () => {
350
+ const onFilterChange = jest.fn()
351
+ render(
352
+ <GanttFilterBar
353
+ filters={makeFilters({ dateRange: { start: new Date('2025-01-01'), end: null } })}
354
+ onFilterChange={onFilterChange}
355
+ uniqueStatuses={[]}
356
+ uniqueCategories={[]}
357
+ />
358
+ )
359
+ const dateInputs = document.querySelectorAll('input[type="date"]')
360
+ fireEvent.change(dateInputs[0], { target: { value: '' } })
361
+ const called = onFilterChange.mock.calls[0][0] as GanttFilterState
362
+ expect(called.dateRange.start).toBeNull()
363
+ })
364
+ })
365
+
366
+ // ---------------------------------------------------------------------------
367
+ // Clear button
368
+ // ---------------------------------------------------------------------------
369
+
370
+ describe('GanttFilterBar — clear button', () => {
371
+ it('does not render the clear button when no filters are active', () => {
372
+ const onFilterChange = jest.fn()
373
+ render(
374
+ <GanttFilterBar
375
+ filters={emptyFilters}
376
+ onFilterChange={onFilterChange}
377
+ uniqueStatuses={[]}
378
+ uniqueCategories={[]}
379
+ />
380
+ )
381
+ expect(screen.queryByRole('button', { name: 'Clear' })).not.toBeInTheDocument()
382
+ })
383
+
384
+ it('renders the clear button when search is active', () => {
385
+ const onFilterChange = jest.fn()
386
+ render(
387
+ <GanttFilterBar
388
+ filters={makeFilters({ search: 'foo' })}
389
+ onFilterChange={onFilterChange}
390
+ uniqueStatuses={[]}
391
+ uniqueCategories={[]}
392
+ />
393
+ )
394
+ expect(screen.getByRole('button', { name: 'Clear' })).toBeInTheDocument()
395
+ })
396
+
397
+ it('renders the clear button when a status filter is active', () => {
398
+ const onFilterChange = jest.fn()
399
+ render(
400
+ <GanttFilterBar
401
+ filters={makeFilters({ statuses: ['completed'] })}
402
+ onFilterChange={onFilterChange}
403
+ uniqueStatuses={STATUSES}
404
+ uniqueCategories={[]}
405
+ />
406
+ )
407
+ expect(screen.getByRole('button', { name: 'Clear' })).toBeInTheDocument()
408
+ })
409
+
410
+ it('renders the clear button when a category filter is active', () => {
411
+ const onFilterChange = jest.fn()
412
+ render(
413
+ <GanttFilterBar
414
+ filters={makeFilters({ categories: ['product'] })}
415
+ onFilterChange={onFilterChange}
416
+ uniqueStatuses={[]}
417
+ uniqueCategories={CATEGORIES}
418
+ />
419
+ )
420
+ expect(screen.getByRole('button', { name: 'Clear' })).toBeInTheDocument()
421
+ })
422
+
423
+ it('calls onFilterChange with all filters reset when Clear is clicked', () => {
424
+ const onFilterChange = jest.fn()
425
+ render(
426
+ <GanttFilterBar
427
+ filters={makeFilters({ search: 'foo', statuses: ['completed'], categories: ['product'] })}
428
+ onFilterChange={onFilterChange}
429
+ uniqueStatuses={STATUSES}
430
+ uniqueCategories={CATEGORIES}
431
+ />
432
+ )
433
+ fireEvent.click(screen.getByRole('button', { name: 'Clear' }))
434
+ expect(onFilterChange).toHaveBeenCalledWith({
435
+ search: '',
436
+ statuses: [],
437
+ categories: [],
438
+ dateRange: { start: null, end: null },
439
+ })
440
+ })
441
+ })
442
+
443
+ // ---------------------------------------------------------------------------
444
+ // Filter pills
445
+ // ---------------------------------------------------------------------------
446
+
447
+ describe('GanttFilterBar — filter pills', () => {
448
+ it('renders a pill for each active status', () => {
449
+ const onFilterChange = jest.fn()
450
+ render(
451
+ <GanttFilterBar
452
+ filters={makeFilters({ statuses: ['completed', 'in_progress'] })}
453
+ onFilterChange={onFilterChange}
454
+ uniqueStatuses={STATUSES}
455
+ uniqueCategories={[]}
456
+ />
457
+ )
458
+ // Underscores replaced with spaces in pill text
459
+ expect(screen.getByText('completed')).toBeInTheDocument()
460
+ expect(screen.getByText('in progress')).toBeInTheDocument()
461
+ })
462
+
463
+ it('renders a pill for each active category', () => {
464
+ const onFilterChange = jest.fn()
465
+ render(
466
+ <GanttFilterBar
467
+ filters={makeFilters({ categories: ['product', 'team'] })}
468
+ onFilterChange={onFilterChange}
469
+ uniqueStatuses={[]}
470
+ uniqueCategories={CATEGORIES}
471
+ />
472
+ )
473
+ expect(screen.getByText('product')).toBeInTheDocument()
474
+ expect(screen.getByText('team')).toBeInTheDocument()
475
+ })
476
+
477
+ it('removes a status pill when its dismiss button is clicked', () => {
478
+ const onFilterChange = jest.fn()
479
+ render(
480
+ <GanttFilterBar
481
+ filters={makeFilters({ statuses: ['completed'] })}
482
+ onFilterChange={onFilterChange}
483
+ uniqueStatuses={STATUSES}
484
+ uniqueCategories={[]}
485
+ />
486
+ )
487
+ // The pill has a button labelled "x" next to the status text
488
+ const pillButtons = screen.getAllByRole('button')
489
+ // Filter out the 'Clear' button — this pill is the small x button inside the pill
490
+ const dismissButtons = pillButtons.filter((btn) => btn.textContent === 'x')
491
+ expect(dismissButtons).toHaveLength(1)
492
+ fireEvent.click(dismissButtons[0])
493
+ const called = onFilterChange.mock.calls[0][0] as GanttFilterState
494
+ expect(called.statuses).not.toContain('completed')
495
+ })
496
+
497
+ it('removes a category pill when its dismiss button is clicked', () => {
498
+ const onFilterChange = jest.fn()
499
+ render(
500
+ <GanttFilterBar
501
+ filters={makeFilters({ categories: ['product'] })}
502
+ onFilterChange={onFilterChange}
503
+ uniqueStatuses={[]}
504
+ uniqueCategories={CATEGORIES}
505
+ />
506
+ )
507
+ const dismissButtons = screen.getAllByRole('button').filter((btn) => btn.textContent === 'x')
508
+ expect(dismissButtons).toHaveLength(1)
509
+ fireEvent.click(dismissButtons[0])
510
+ const called = onFilterChange.mock.calls[0][0] as GanttFilterState
511
+ expect(called.categories).not.toContain('product')
512
+ })
513
+
514
+ it('preserves other pills when one status pill is dismissed', () => {
515
+ const onFilterChange = jest.fn()
516
+ render(
517
+ <GanttFilterBar
518
+ filters={makeFilters({ statuses: ['completed', 'in_progress'] })}
519
+ onFilterChange={onFilterChange}
520
+ uniqueStatuses={STATUSES}
521
+ uniqueCategories={[]}
522
+ />
523
+ )
524
+ // Two pills rendered — dismiss the first
525
+ const dismissButtons = screen.getAllByRole('button').filter((btn) => btn.textContent === 'x')
526
+ fireEvent.click(dismissButtons[0])
527
+ const called = onFilterChange.mock.calls[0][0] as GanttFilterState
528
+ expect(called.statuses).toHaveLength(1)
529
+ })
530
+
531
+ it('renders no pills when filters are empty', () => {
532
+ const onFilterChange = jest.fn()
533
+ render(
534
+ <GanttFilterBar
535
+ filters={emptyFilters}
536
+ onFilterChange={onFilterChange}
537
+ uniqueStatuses={STATUSES}
538
+ uniqueCategories={CATEGORIES}
539
+ />
540
+ )
541
+ const dismissButtons = screen.queryAllByRole('button').filter((btn) => btn.textContent === 'x')
542
+ expect(dismissButtons).toHaveLength(0)
543
+ })
544
+ })