@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,191 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react'
2
+ import { IntegrationCard } from '../IntegrationCard'
3
+ import { ConnectionStatus } from '../ConnectionStatus'
4
+
5
+ // Simple icon stub usable as React.ElementType
6
+ const TestIcon = () => <svg data-testid="test-icon" />
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // IntegrationCard
10
+ // ---------------------------------------------------------------------------
11
+
12
+ describe('IntegrationCard', () => {
13
+ const baseProps = {
14
+ name: 'Salesforce',
15
+ description: 'Sync leads and deals from Salesforce CRM.',
16
+ icon: TestIcon,
17
+ status: 'available' as const,
18
+ }
19
+
20
+ it('renders name and description', () => {
21
+ render(<IntegrationCard {...baseProps} />)
22
+ expect(screen.getByText('Salesforce')).toBeInTheDocument()
23
+ expect(screen.getByText('Sync leads and deals from Salesforce CRM.')).toBeInTheDocument()
24
+ })
25
+
26
+ it('shows "Available" badge for available status', () => {
27
+ render(<IntegrationCard {...baseProps} />)
28
+ expect(screen.getByText('Available')).toBeInTheDocument()
29
+ })
30
+
31
+ it('shows "Connected" badge for connected status', () => {
32
+ render(<IntegrationCard {...baseProps} status="connected" />)
33
+ expect(screen.getByText('Connected')).toBeInTheDocument()
34
+ })
35
+
36
+ it('shows "Coming Soon" badge for coming-soon status', () => {
37
+ render(<IntegrationCard {...baseProps} status="coming-soon" />)
38
+ expect(screen.getByText('Coming Soon')).toBeInTheDocument()
39
+ })
40
+
41
+ it('renders icon', () => {
42
+ render(<IntegrationCard {...baseProps} />)
43
+ expect(screen.getByTestId('test-icon')).toBeInTheDocument()
44
+ })
45
+
46
+ it('shows Configure link for clickable statuses', () => {
47
+ render(<IntegrationCard {...baseProps} onClick={jest.fn()} />)
48
+ expect(screen.getByText('Configure')).toBeInTheDocument()
49
+ })
50
+
51
+ it('does not show Configure link for coming-soon status', () => {
52
+ render(<IntegrationCard {...baseProps} status="coming-soon" />)
53
+ expect(screen.queryByText('Configure')).not.toBeInTheDocument()
54
+ })
55
+
56
+ it('calls onClick when clicked for available status', () => {
57
+ const onClick = jest.fn()
58
+ render(<IntegrationCard {...baseProps} onClick={onClick} />)
59
+ fireEvent.click(screen.getByRole('button'))
60
+ expect(onClick).toHaveBeenCalledTimes(1)
61
+ })
62
+
63
+ it('calls onClick on Enter key press for clickable cards', () => {
64
+ const onClick = jest.fn()
65
+ render(<IntegrationCard {...baseProps} onClick={onClick} />)
66
+ fireEvent.keyDown(screen.getByRole('button'), { key: 'Enter' })
67
+ expect(onClick).toHaveBeenCalledTimes(1)
68
+ })
69
+
70
+ it('calls onClick on Space key press for clickable cards', () => {
71
+ const onClick = jest.fn()
72
+ render(<IntegrationCard {...baseProps} onClick={onClick} />)
73
+ fireEvent.keyDown(screen.getByRole('button'), { key: ' ' })
74
+ expect(onClick).toHaveBeenCalledTimes(1)
75
+ })
76
+
77
+ it('does not have role=button for coming-soon', () => {
78
+ render(<IntegrationCard {...baseProps} status="coming-soon" />)
79
+ expect(screen.queryByRole('button')).not.toBeInTheDocument()
80
+ })
81
+
82
+ it('shows lastSync when provided', () => {
83
+ render(<IntegrationCard {...baseProps} status="connected" lastSync="2 hours ago" />)
84
+ expect(screen.getByText('Last synced: 2 hours ago')).toBeInTheDocument()
85
+ })
86
+
87
+ it('does not show lastSync when not provided', () => {
88
+ render(<IntegrationCard {...baseProps} />)
89
+ expect(screen.queryByText(/last synced/i)).not.toBeInTheDocument()
90
+ })
91
+
92
+ it('applies custom className', () => {
93
+ const { container } = render(
94
+ <IntegrationCard {...baseProps} className="my-class" />
95
+ )
96
+ expect(container.firstChild).toHaveClass('my-class')
97
+ })
98
+
99
+ it('applies opacity for coming-soon status', () => {
100
+ const { container } = render(
101
+ <IntegrationCard {...baseProps} status="coming-soon" />
102
+ )
103
+ expect(container.firstChild).toHaveClass('opacity-75')
104
+ })
105
+ })
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // ConnectionStatus
109
+ // ---------------------------------------------------------------------------
110
+
111
+ describe('ConnectionStatus', () => {
112
+ const baseProps = {
113
+ providerName: 'Gmail',
114
+ providerIcon: TestIcon,
115
+ connected: false,
116
+ }
117
+
118
+ it('renders provider name', () => {
119
+ render(<ConnectionStatus {...baseProps} />)
120
+ expect(screen.getByText('Gmail')).toBeInTheDocument()
121
+ })
122
+
123
+ it('renders provider icon', () => {
124
+ render(<ConnectionStatus {...baseProps} />)
125
+ expect(screen.getByTestId('test-icon')).toBeInTheDocument()
126
+ })
127
+
128
+ it('shows "Not connected" when connected is false', () => {
129
+ render(<ConnectionStatus {...baseProps} />)
130
+ expect(screen.getByText('Not connected')).toBeInTheDocument()
131
+ })
132
+
133
+ it('shows Connect button when not connected', () => {
134
+ render(<ConnectionStatus {...baseProps} onConnect={jest.fn()} />)
135
+ expect(screen.getByRole('button', { name: /connect/i })).toBeInTheDocument()
136
+ })
137
+
138
+ it('shows Disconnect button when connected', () => {
139
+ render(<ConnectionStatus {...baseProps} connected onDisconnect={jest.fn()} />)
140
+ expect(screen.getByRole('button', { name: /disconnect/i })).toBeInTheDocument()
141
+ })
142
+
143
+ it('shows accountLabel when connected', () => {
144
+ render(
145
+ <ConnectionStatus
146
+ {...baseProps}
147
+ connected
148
+ accountLabel="jane@example.com"
149
+ />
150
+ )
151
+ expect(screen.getByText('jane@example.com')).toBeInTheDocument()
152
+ })
153
+
154
+ it('does not show accountLabel when not connected', () => {
155
+ render(
156
+ <ConnectionStatus
157
+ {...baseProps}
158
+ connected={false}
159
+ accountLabel="jane@example.com"
160
+ />
161
+ )
162
+ expect(screen.queryByText('jane@example.com')).not.toBeInTheDocument()
163
+ })
164
+
165
+ it('calls onConnect when Connect is clicked', () => {
166
+ const onConnect = jest.fn()
167
+ render(<ConnectionStatus {...baseProps} onConnect={onConnect} />)
168
+ fireEvent.click(screen.getByRole('button', { name: /connect/i }))
169
+ expect(onConnect).toHaveBeenCalledTimes(1)
170
+ })
171
+
172
+ it('calls onDisconnect when Disconnect is clicked', () => {
173
+ const onDisconnect = jest.fn()
174
+ render(<ConnectionStatus {...baseProps} connected onDisconnect={onDisconnect} />)
175
+ fireEvent.click(screen.getByRole('button', { name: /disconnect/i }))
176
+ expect(onDisconnect).toHaveBeenCalledTimes(1)
177
+ })
178
+
179
+ it('shows loading state when isLoading is true', () => {
180
+ render(<ConnectionStatus {...baseProps} isLoading />)
181
+ expect(screen.getByText('Loading...')).toBeInTheDocument()
182
+ expect(screen.getByText('Loading...').closest('button')).toBeDisabled()
183
+ })
184
+
185
+ it('applies custom className', () => {
186
+ const { container } = render(
187
+ <ConnectionStatus {...baseProps} className="my-class" />
188
+ )
189
+ expect(container.firstChild).toHaveClass('my-class')
190
+ })
191
+ })
@@ -0,0 +1,157 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { KanbanBoard } from '../KanbanBoard'
3
+
4
+ interface Card {
5
+ id: string
6
+ title: string
7
+ }
8
+
9
+ const columns = [
10
+ { id: 'todo', label: 'To Do', color: '#6366f1' },
11
+ { id: 'in-progress', label: 'In Progress', color: '#f59e0b' },
12
+ { id: 'done', label: 'Done' },
13
+ ]
14
+
15
+ const items: Record<string, Card[]> = {
16
+ todo: [{ id: '1', title: 'Task One' }, { id: '2', title: 'Task Two' }],
17
+ 'in-progress': [{ id: '3', title: 'Task Three' }],
18
+ done: [],
19
+ }
20
+
21
+ describe('KanbanBoard', () => {
22
+ const renderCard = (item: Card) => (
23
+ <div key={item.id} data-testid={`card-${item.id}`}>
24
+ {item.title}
25
+ </div>
26
+ )
27
+
28
+ it('renders all column labels', () => {
29
+ render(
30
+ <KanbanBoard columns={columns} items={items} renderCard={renderCard} />
31
+ )
32
+ expect(screen.getByText('To Do')).toBeInTheDocument()
33
+ expect(screen.getByText('In Progress')).toBeInTheDocument()
34
+ expect(screen.getByText('Done')).toBeInTheDocument()
35
+ })
36
+
37
+ it('renders all card items', () => {
38
+ render(
39
+ <KanbanBoard columns={columns} items={items} renderCard={renderCard} />
40
+ )
41
+ expect(screen.getByText('Task One')).toBeInTheDocument()
42
+ expect(screen.getByText('Task Two')).toBeInTheDocument()
43
+ expect(screen.getByText('Task Three')).toBeInTheDocument()
44
+ })
45
+
46
+ it('shows item counts per column in the default header', () => {
47
+ render(
48
+ <KanbanBoard columns={columns} items={items} renderCard={renderCard} />
49
+ )
50
+ // todo has 2, in-progress has 1, done has 0
51
+ const counts = screen.getAllByText(/^[0-9]+$/)
52
+ const countValues = counts.map((el) => el.textContent)
53
+ expect(countValues).toContain('2')
54
+ expect(countValues).toContain('1')
55
+ expect(countValues).toContain('0')
56
+ })
57
+
58
+ it('shows empty column message when column has no items', () => {
59
+ render(
60
+ <KanbanBoard columns={columns} items={items} renderCard={renderCard} />
61
+ )
62
+ expect(screen.getByText('No items')).toBeInTheDocument()
63
+ })
64
+
65
+ it('shows custom empty column message', () => {
66
+ render(
67
+ <KanbanBoard
68
+ columns={columns}
69
+ items={items}
70
+ renderCard={renderCard}
71
+ emptyColumnMessage="Nothing here yet"
72
+ />
73
+ )
74
+ expect(screen.getByText('Nothing here yet')).toBeInTheDocument()
75
+ })
76
+
77
+ it('renders color dot for columns that have a color', () => {
78
+ const { container } = render(
79
+ <KanbanBoard columns={columns} items={items} renderCard={renderCard} />
80
+ )
81
+ // Two columns have colors (todo, in-progress), one does not (done)
82
+ const colorDots = container.querySelectorAll('[aria-hidden="true"]')
83
+ expect(colorDots.length).toBe(2)
84
+ })
85
+
86
+ it('uses custom column header when renderColumnHeader is provided', () => {
87
+ const renderColumnHeader = (col: typeof columns[0]) => (
88
+ <div data-testid={`custom-header-${col.id}`}>Custom: {col.label}</div>
89
+ )
90
+ render(
91
+ <KanbanBoard
92
+ columns={columns}
93
+ items={items}
94
+ renderCard={renderCard}
95
+ renderColumnHeader={renderColumnHeader}
96
+ />
97
+ )
98
+ expect(screen.getByTestId('custom-header-todo')).toBeInTheDocument()
99
+ expect(screen.getByText('Custom: To Do')).toBeInTheDocument()
100
+ })
101
+
102
+ it('renders column footer when renderColumnFooter is provided', () => {
103
+ const renderColumnFooter = (col: typeof columns[0]) => (
104
+ <div data-testid={`footer-${col.id}`}>Footer for {col.label}</div>
105
+ )
106
+ render(
107
+ <KanbanBoard
108
+ columns={columns}
109
+ items={items}
110
+ renderCard={renderCard}
111
+ renderColumnFooter={renderColumnFooter}
112
+ />
113
+ )
114
+ expect(screen.getByTestId('footer-todo')).toBeInTheDocument()
115
+ expect(screen.getByText('Footer for To Do')).toBeInTheDocument()
116
+ })
117
+
118
+ it('handles missing items for a column gracefully', () => {
119
+ // Only provide items for one column
120
+ const partialItems: Record<string, Card[]> = {
121
+ todo: [{ id: '1', title: 'Task One' }],
122
+ }
123
+ render(
124
+ <KanbanBoard columns={columns} items={partialItems} renderCard={renderCard} />
125
+ )
126
+ // The other two columns should show the empty message
127
+ const emptyMessages = screen.getAllByText('No items')
128
+ expect(emptyMessages.length).toBe(2)
129
+ })
130
+
131
+ it('applies custom className to container', () => {
132
+ const { container } = render(
133
+ <KanbanBoard
134
+ columns={columns}
135
+ items={items}
136
+ renderCard={renderCard}
137
+ className="my-board"
138
+ />
139
+ )
140
+ expect(container.firstChild).toHaveClass('my-board')
141
+ })
142
+
143
+ it('applies ring styles to column that isColumnOver returns true for', () => {
144
+ const isColumnOver = (id: string) => id === 'todo'
145
+ const { container } = render(
146
+ <KanbanBoard
147
+ columns={columns}
148
+ items={items}
149
+ renderCard={renderCard}
150
+ isColumnOver={isColumnOver}
151
+ />
152
+ )
153
+ // The first column div should have ring classes
154
+ const firstCol = container.querySelector('[style*="320"]')
155
+ expect(firstCol).toHaveClass('ring-2')
156
+ })
157
+ })
@@ -0,0 +1,263 @@
1
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react'
2
+ import { ListCard } from '../ListCard'
3
+ import { CreateListDialog } from '../CreateListDialog'
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // ListCard
7
+ // ---------------------------------------------------------------------------
8
+
9
+ describe('ListCard', () => {
10
+ const baseProps = {
11
+ name: 'My Prospect List',
12
+ sourceType: 'static',
13
+ memberCount: 42,
14
+ createdAt: 'Jan 1, 2025',
15
+ }
16
+
17
+ it('renders the list name', () => {
18
+ render(<ListCard {...baseProps} />)
19
+ expect(screen.getByText('My Prospect List')).toBeInTheDocument()
20
+ })
21
+
22
+ it('renders member count with singular "member"', () => {
23
+ render(<ListCard {...baseProps} memberCount={1} />)
24
+ expect(screen.getByText('1 member')).toBeInTheDocument()
25
+ })
26
+
27
+ it('renders member count with plural "members"', () => {
28
+ render(<ListCard {...baseProps} memberCount={42} />)
29
+ expect(screen.getByText('42 members')).toBeInTheDocument()
30
+ })
31
+
32
+ it('renders zero members correctly', () => {
33
+ render(<ListCard {...baseProps} memberCount={0} />)
34
+ expect(screen.getByText('0 members')).toBeInTheDocument()
35
+ })
36
+
37
+ it('renders large member count with locale formatting', () => {
38
+ render(<ListCard {...baseProps} memberCount={1500} />)
39
+ expect(screen.getByText('1,500 members')).toBeInTheDocument()
40
+ })
41
+
42
+ it('renders the source type badge', () => {
43
+ render(<ListCard {...baseProps} />)
44
+ expect(screen.getByText('static')).toBeInTheDocument()
45
+ })
46
+
47
+ it('renders the createdAt text', () => {
48
+ render(<ListCard {...baseProps} />)
49
+ expect(screen.getByText('Created Jan 1, 2025')).toBeInTheDocument()
50
+ })
51
+
52
+ it('applies blue badge styles for static source type', () => {
53
+ render(<ListCard {...baseProps} sourceType="static" />)
54
+ const badge = screen.getByText('static')
55
+ expect(badge).toHaveClass('bg-blue-100', 'text-blue-700')
56
+ })
57
+
58
+ it('applies purple badge styles for funnel source type', () => {
59
+ render(<ListCard {...baseProps} sourceType="funnel" />)
60
+ const badge = screen.getByText('funnel')
61
+ expect(badge).toHaveClass('bg-purple-100', 'text-purple-700')
62
+ })
63
+
64
+ it('applies green badge styles for query source type', () => {
65
+ render(<ListCard {...baseProps} sourceType="query" />)
66
+ const badge = screen.getByText('query')
67
+ expect(badge).toHaveClass('bg-green-100', 'text-green-700')
68
+ })
69
+
70
+ it('applies gray badge for unknown source type', () => {
71
+ render(<ListCard {...baseProps} sourceType="unknown-type" />)
72
+ const badge = screen.getByText('unknown-type')
73
+ expect(badge).toHaveClass('bg-gray-100', 'text-gray-700')
74
+ })
75
+
76
+ it('has role=button when onClick is provided', () => {
77
+ render(<ListCard {...baseProps} onClick={jest.fn()} />)
78
+ expect(screen.getByRole('button')).toBeInTheDocument()
79
+ })
80
+
81
+ it('does not have role=button when onClick is not provided', () => {
82
+ render(<ListCard {...baseProps} />)
83
+ expect(screen.queryByRole('button')).not.toBeInTheDocument()
84
+ })
85
+
86
+ it('calls onClick when clicked', () => {
87
+ const onClick = jest.fn()
88
+ render(<ListCard {...baseProps} onClick={onClick} />)
89
+ fireEvent.click(screen.getByRole('button'))
90
+ expect(onClick).toHaveBeenCalledTimes(1)
91
+ })
92
+
93
+ it('calls onClick when Enter key is pressed', () => {
94
+ const onClick = jest.fn()
95
+ render(<ListCard {...baseProps} onClick={onClick} />)
96
+ fireEvent.keyDown(screen.getByRole('button'), { key: 'Enter' })
97
+ expect(onClick).toHaveBeenCalledTimes(1)
98
+ })
99
+
100
+ it('calls onClick when Space key is pressed', () => {
101
+ const onClick = jest.fn()
102
+ render(<ListCard {...baseProps} onClick={onClick} />)
103
+ fireEvent.keyDown(screen.getByRole('button'), { key: ' ' })
104
+ expect(onClick).toHaveBeenCalledTimes(1)
105
+ })
106
+
107
+ it('applies custom className', () => {
108
+ const { container } = render(<ListCard {...baseProps} className="custom-card" />)
109
+ expect(container.firstChild).toHaveClass('custom-card')
110
+ })
111
+ })
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // CreateListDialog
115
+ // ---------------------------------------------------------------------------
116
+
117
+ describe('CreateListDialog', () => {
118
+ const baseProps = {
119
+ open: true,
120
+ onClose: jest.fn(),
121
+ onSubmit: jest.fn(),
122
+ }
123
+
124
+ beforeEach(() => {
125
+ jest.clearAllMocks()
126
+ })
127
+
128
+ it('renders nothing when open is false', () => {
129
+ render(<CreateListDialog {...baseProps} open={false} />)
130
+ expect(screen.queryByText('Create List')).not.toBeInTheDocument()
131
+ })
132
+
133
+ it('renders dialog when open is true', () => {
134
+ render(<CreateListDialog {...baseProps} />)
135
+ expect(screen.getByText('Create List')).toBeInTheDocument()
136
+ })
137
+
138
+ it('renders name, description, and source type fields', () => {
139
+ render(<CreateListDialog {...baseProps} />)
140
+ expect(screen.getByLabelText(/name/i)).toBeInTheDocument()
141
+ expect(screen.getByLabelText(/description/i)).toBeInTheDocument()
142
+ expect(screen.getByLabelText(/source type/i)).toBeInTheDocument()
143
+ })
144
+
145
+ it('renders default source type options', () => {
146
+ render(<CreateListDialog {...baseProps} />)
147
+ expect(screen.getByRole('option', { name: 'Static' })).toBeInTheDocument()
148
+ expect(screen.getByRole('option', { name: 'Funnel' })).toBeInTheDocument()
149
+ expect(screen.getByRole('option', { name: 'Query' })).toBeInTheDocument()
150
+ })
151
+
152
+ it('renders custom source types when provided', () => {
153
+ render(
154
+ <CreateListDialog
155
+ {...baseProps}
156
+ sourceTypes={[{ value: 'csv', label: 'CSV Upload' }]}
157
+ />
158
+ )
159
+ expect(screen.getByRole('option', { name: 'CSV Upload' })).toBeInTheDocument()
160
+ expect(screen.queryByRole('option', { name: 'Static' })).not.toBeInTheDocument()
161
+ })
162
+
163
+ it('Create button is disabled when name is empty', () => {
164
+ render(<CreateListDialog {...baseProps} />)
165
+ expect(screen.getByRole('button', { name: /^create$/i })).toBeDisabled()
166
+ })
167
+
168
+ it('Create button is enabled after entering a name', () => {
169
+ render(<CreateListDialog {...baseProps} />)
170
+ fireEvent.change(screen.getByLabelText(/name/i), { target: { value: 'New List' } })
171
+ expect(screen.getByRole('button', { name: /^create$/i })).toBeEnabled()
172
+ })
173
+
174
+ it('calls onSubmit with correct data', () => {
175
+ const onSubmit = jest.fn()
176
+ render(<CreateListDialog {...baseProps} onSubmit={onSubmit} />)
177
+
178
+ fireEvent.change(screen.getByLabelText(/name/i), { target: { value: 'Sales Leads' } })
179
+ fireEvent.change(screen.getByLabelText(/description/i), { target: { value: 'Top prospects' } })
180
+
181
+ fireEvent.click(screen.getByRole('button', { name: /^create$/i }))
182
+
183
+ expect(onSubmit).toHaveBeenCalledWith({
184
+ name: 'Sales Leads',
185
+ description: 'Top prospects',
186
+ sourceType: 'static',
187
+ })
188
+ })
189
+
190
+ it('trims name and description on submit', () => {
191
+ const onSubmit = jest.fn()
192
+ render(<CreateListDialog {...baseProps} onSubmit={onSubmit} />)
193
+
194
+ fireEvent.change(screen.getByLabelText(/name/i), { target: { value: ' Trimmed ' } })
195
+ fireEvent.click(screen.getByRole('button', { name: /^create$/i }))
196
+
197
+ expect(onSubmit).toHaveBeenCalledWith(
198
+ expect.objectContaining({ name: 'Trimmed' })
199
+ )
200
+ })
201
+
202
+ it('does not call onSubmit when name is only whitespace', () => {
203
+ const onSubmit = jest.fn()
204
+ render(<CreateListDialog {...baseProps} onSubmit={onSubmit} />)
205
+
206
+ fireEvent.change(screen.getByLabelText(/name/i), { target: { value: ' ' } })
207
+ fireEvent.submit(screen.getByLabelText(/name/i).closest('form')!)
208
+
209
+ expect(onSubmit).not.toHaveBeenCalled()
210
+ })
211
+
212
+ it('calls onClose when Cancel is clicked', () => {
213
+ const onClose = jest.fn()
214
+ render(<CreateListDialog {...baseProps} onClose={onClose} />)
215
+ fireEvent.click(screen.getByRole('button', { name: /cancel/i }))
216
+ expect(onClose).toHaveBeenCalledTimes(1)
217
+ })
218
+
219
+ it('calls onClose when backdrop is clicked', () => {
220
+ const onClose = jest.fn()
221
+ const { container } = render(<CreateListDialog {...baseProps} onClose={onClose} />)
222
+ const backdrop = container.querySelector('[aria-hidden="true"]')
223
+ fireEvent.click(backdrop!)
224
+ expect(onClose).toHaveBeenCalledTimes(1)
225
+ })
226
+
227
+ it('does not call onClose when backdrop clicked during submit', () => {
228
+ const onClose = jest.fn()
229
+ const { container } = render(
230
+ <CreateListDialog {...baseProps} onClose={onClose} isSubmitting />
231
+ )
232
+ const backdrop = container.querySelector('[aria-hidden="true"]')
233
+ fireEvent.click(backdrop!)
234
+ expect(onClose).not.toHaveBeenCalled()
235
+ })
236
+
237
+ it('shows Creating... and disables fields when isSubmitting', () => {
238
+ render(<CreateListDialog {...baseProps} isSubmitting />)
239
+ expect(screen.getByText('Creating...')).toBeInTheDocument()
240
+ expect(screen.getByLabelText(/name/i)).toBeDisabled()
241
+ expect(screen.getByLabelText(/description/i)).toBeDisabled()
242
+ })
243
+
244
+ it('resets fields when dialog reopens', () => {
245
+ const { rerender } = render(<CreateListDialog {...baseProps} open={false} />)
246
+ rerender(<CreateListDialog {...baseProps} open />)
247
+ expect(screen.getByLabelText(/name/i)).toHaveValue('')
248
+ expect(screen.getByLabelText(/description/i)).toHaveValue('')
249
+ })
250
+
251
+ it('submits the selected source type', () => {
252
+ const onSubmit = jest.fn()
253
+ render(<CreateListDialog {...baseProps} onSubmit={onSubmit} />)
254
+
255
+ fireEvent.change(screen.getByLabelText(/name/i), { target: { value: 'My List' } })
256
+ fireEvent.change(screen.getByLabelText(/source type/i), { target: { value: 'funnel' } })
257
+ fireEvent.click(screen.getByRole('button', { name: /^create$/i }))
258
+
259
+ expect(onSubmit).toHaveBeenCalledWith(
260
+ expect.objectContaining({ sourceType: 'funnel' })
261
+ )
262
+ })
263
+ })