@startsimpli/ui 0.4.6 → 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.
- package/package.json +2 -1
- package/src/__mocks__/next/link.js +11 -0
- package/src/components/ActivityTimeline.tsx +173 -0
- package/src/components/LogActivityDialog.tsx +303 -0
- package/src/components/QuickLogButtons.tsx +32 -0
- package/src/components/account/__tests__/account.test.tsx +315 -0
- package/src/components/badge/StageBadge.tsx +31 -0
- package/src/components/badge/index.ts +3 -0
- package/src/components/command-palette/CommandGroup.tsx +23 -0
- package/src/components/command-palette/CommandPalette.tsx +327 -0
- package/src/components/command-palette/CommandResultItem.tsx +59 -0
- package/src/components/command-palette/__tests__/CommandGroup.test.tsx +81 -0
- package/src/components/command-palette/__tests__/CommandResultItem.test.tsx +166 -0
- package/src/components/command-palette/__tests__/command-palette-context.test.tsx +166 -0
- package/src/components/command-palette/__tests__/useCommandPaletteSearch.test.ts +271 -0
- package/src/components/command-palette/command-palette-context.tsx +51 -0
- package/src/components/command-palette/index.ts +9 -0
- package/src/components/command-palette/useCommandPaletteSearch.ts +114 -0
- package/src/components/compose/__tests__/compose.test.tsx +656 -0
- package/src/components/compose/compose-header.tsx +72 -0
- package/src/components/compose/compose-loading.tsx +13 -0
- package/src/components/compose/index.ts +6 -0
- package/src/components/compose/save-status-indicator.tsx +57 -0
- package/src/components/compose/send-confirmation-dialog.tsx +87 -0
- package/src/components/compose/subject-input.tsx +25 -0
- package/src/components/compose/useAutoSave.ts +93 -0
- package/src/components/dashboard/DashboardGrid.tsx +32 -0
- package/src/components/dashboard/DashboardSection.tsx +32 -0
- package/src/components/dashboard/MetricCard.tsx +129 -0
- package/src/components/dashboard/PeriodSelector.tsx +55 -0
- package/src/components/dashboard/PipelineFunnel.tsx +126 -0
- package/src/components/dashboard/SparklineTrend.tsx +102 -0
- package/src/components/dashboard/TopCampaigns.tsx +132 -0
- package/src/components/dashboard/__tests__/dashboard.test.tsx +785 -0
- package/src/components/dashboard/index.ts +20 -0
- package/src/components/dialog/ConfirmDialog.tsx +72 -0
- package/src/components/dialog/__tests__/ConfirmDialog.test.tsx +126 -0
- package/src/components/dialog/index.ts +3 -0
- package/src/components/email-dialogs/__tests__/email-dialogs.test.tsx +982 -0
- package/src/components/email-dialogs/index.ts +14 -0
- package/src/components/email-dialogs/merge-fields.tsx +196 -0
- package/src/components/email-dialogs/preview-dialog.tsx +194 -0
- package/src/components/email-dialogs/schedule-dialog.tsx +297 -0
- package/src/components/email-dialogs/template-picker.tsx +225 -0
- package/src/components/email-dialogs/test-send-dialog.tsx +188 -0
- package/src/components/email-editor/BlockRenderer.tsx +120 -0
- package/src/components/email-editor/__tests__/BlockRenderer.test.tsx +332 -0
- package/src/components/email-editor/__tests__/block-renderers.test.ts +624 -0
- package/src/components/email-editor/__tests__/email-html-renderer.test.ts +376 -0
- package/src/components/email-editor/add-block-menu.tsx +151 -0
- package/src/components/email-editor/block-toolbar.tsx +73 -0
- package/src/components/email-editor/blocks/__tests__/blocks.test.tsx +818 -0
- package/src/components/email-editor/blocks/button-block.tsx +44 -0
- package/src/components/email-editor/blocks/divider-block.tsx +43 -0
- package/src/components/email-editor/blocks/footer-block.tsx +39 -0
- package/src/components/email-editor/blocks/header-block.tsx +39 -0
- package/src/components/email-editor/blocks/image-block.tsx +61 -0
- package/src/components/email-editor/blocks/index.ts +9 -0
- package/src/components/email-editor/blocks/metrics-block.tsx +198 -0
- package/src/components/email-editor/blocks/social-block.tsx +75 -0
- package/src/components/email-editor/blocks/spacer-block.tsx +26 -0
- package/src/components/email-editor/blocks/text-block.tsx +75 -0
- package/src/components/email-editor/editor-sidebar.tsx +66 -0
- package/src/components/email-editor/email-editor.tsx +497 -0
- package/src/components/email-editor/hooks/__tests__/useDragDrop.test.ts +355 -0
- package/src/components/email-editor/hooks/__tests__/useEmailEditorState.test.ts +551 -0
- package/src/components/email-editor/hooks/useDragDrop.ts +181 -0
- package/src/components/email-editor/hooks/useEmailEditorState.ts +426 -0
- package/src/components/email-editor/index.ts +51 -0
- package/src/components/email-editor/panels/BlockPropertyPanel.tsx +637 -0
- package/src/components/email-editor/panels/GlobalStylesPanel.tsx +108 -0
- package/src/components/email-editor/panels/SectionSettingsPanel.tsx +80 -0
- package/src/components/email-editor/panels/__tests__/BlockPropertyPanel.test.tsx +707 -0
- package/src/components/email-editor/panels/__tests__/GlobalStylesPanel.test.tsx +226 -0
- package/src/components/email-editor/panels/index.ts +3 -0
- package/src/components/email-editor/renderer/block-renderers.ts +209 -0
- package/src/components/email-editor/renderer/email-html-renderer.ts +128 -0
- package/src/components/email-editor/types.ts +413 -0
- package/src/components/email-editor/utils/defaults.ts +116 -0
- package/src/components/email-editor/utils/undo-redo.ts +59 -0
- package/src/components/enrichment/EnrichButton.tsx +33 -0
- package/src/components/enrichment/EnrichmentProgress.tsx +66 -0
- package/src/components/enrichment/QualityBadge.tsx +43 -0
- package/src/components/enrichment/__tests__/enrichment.test.tsx +184 -0
- package/src/components/enrichment/index.ts +8 -0
- package/src/components/gantt/GanttBoardView.tsx +71 -0
- package/src/components/gantt/GanttChart.tsx +140 -887
- package/src/components/gantt/GanttFilterBar.tsx +100 -0
- package/src/components/gantt/GanttListView.tsx +63 -0
- package/src/components/gantt/GanttTimelineView.tsx +215 -0
- package/src/components/gantt/__tests__/GanttBoardView.test.tsx +305 -0
- package/src/components/gantt/__tests__/GanttFilterBar.test.tsx +544 -0
- package/src/components/gantt/__tests__/GanttListView.test.tsx +337 -0
- package/src/components/gantt/__tests__/GanttTimelineView.test.tsx +375 -0
- package/src/components/gantt/__tests__/gantt-utils.test.ts +341 -0
- package/src/components/gantt/__tests__/useGanttState.test.ts +535 -0
- package/src/components/gantt/hooks/useGanttState.ts +644 -0
- package/src/components/gantt/index.ts +10 -0
- package/src/components/gantt/types.ts +5 -5
- package/src/components/index.ts +46 -0
- package/src/components/integrations/ConnectionStatus.tsx +77 -0
- package/src/components/integrations/IntegrationCard.tsx +92 -0
- package/src/components/integrations/__tests__/integrations.test.tsx +191 -0
- package/src/components/integrations/index.ts +5 -0
- package/src/components/kanban/KanbanBoard.tsx +103 -0
- package/src/components/kanban/__tests__/kanban.test.tsx +157 -0
- package/src/components/kanban/index.ts +2 -0
- package/src/components/lists/CreateListDialog.tsx +158 -0
- package/src/components/lists/ListCard.tsx +77 -0
- package/src/components/lists/__tests__/lists.test.tsx +263 -0
- package/src/components/lists/index.ts +5 -0
- package/src/components/loading/__tests__/loading.test.tsx +114 -0
- package/src/components/navigation/__tests__/navigation.test.tsx +194 -0
- package/src/components/pipeline/StageTransitionModal.tsx +146 -0
- package/src/components/pipeline/__tests__/pipeline.test.tsx +169 -0
- package/src/components/pipeline/index.ts +2 -0
- package/src/components/settings/SettingsCard.tsx +33 -0
- package/src/components/settings/SettingsLayout.tsx +28 -0
- package/src/components/settings/SettingsNav.tsx +42 -0
- package/src/components/settings/__tests__/settings.test.tsx +181 -0
- package/src/components/settings/index.ts +6 -0
- package/src/components/wizard/__tests__/wizard.test.tsx +97 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'
|
|
2
|
+
import { ProfileForm } from '../profile-form'
|
|
3
|
+
import { ChangePasswordForm } from '../change-password-form'
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
function changeInput(element: HTMLElement, value: string) {
|
|
10
|
+
fireEvent.change(element, { target: { value } })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// ProfileForm
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const defaultProfileProps = {
|
|
18
|
+
initialValues: { firstName: 'Jane', lastName: 'Doe', email: 'jane@example.com' },
|
|
19
|
+
onSubmit: jest.fn(),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('ProfileForm', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
jest.clearAllMocks()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('renders all fields with initial values', () => {
|
|
28
|
+
render(<ProfileForm {...defaultProfileProps} />)
|
|
29
|
+
|
|
30
|
+
expect(screen.getByLabelText('Email')).toHaveValue('jane@example.com')
|
|
31
|
+
expect(screen.getByLabelText('First name')).toHaveValue('Jane')
|
|
32
|
+
expect(screen.getByLabelText('Last name')).toHaveValue('Doe')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('email field is always disabled', () => {
|
|
36
|
+
render(<ProfileForm {...defaultProfileProps} />)
|
|
37
|
+
expect(screen.getByLabelText('Email')).toBeDisabled()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('save button is disabled when no changes have been made', () => {
|
|
41
|
+
render(<ProfileForm {...defaultProfileProps} />)
|
|
42
|
+
expect(screen.getByRole('button', { name: /save changes/i })).toBeDisabled()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('save button becomes enabled after editing first name', () => {
|
|
46
|
+
render(<ProfileForm {...defaultProfileProps} />)
|
|
47
|
+
changeInput(screen.getByLabelText('First name'), 'John')
|
|
48
|
+
expect(screen.getByRole('button', { name: /save changes/i })).toBeEnabled()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('save button becomes enabled after editing last name', () => {
|
|
52
|
+
render(<ProfileForm {...defaultProfileProps} />)
|
|
53
|
+
changeInput(screen.getByLabelText('Last name'), 'Smith')
|
|
54
|
+
expect(screen.getByRole('button', { name: /save changes/i })).toBeEnabled()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('save button is disabled again when edits are reverted', () => {
|
|
58
|
+
render(<ProfileForm {...defaultProfileProps} />)
|
|
59
|
+
changeInput(screen.getByLabelText('First name'), 'John')
|
|
60
|
+
expect(screen.getByRole('button', { name: /save changes/i })).toBeEnabled()
|
|
61
|
+
changeInput(screen.getByLabelText('First name'), 'Jane')
|
|
62
|
+
expect(screen.getByRole('button', { name: /save changes/i })).toBeDisabled()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('calls onSubmit with trimmed first_name and last_name', async () => {
|
|
66
|
+
const onSubmit = jest.fn().mockResolvedValue(undefined)
|
|
67
|
+
render(<ProfileForm {...defaultProfileProps} onSubmit={onSubmit} />)
|
|
68
|
+
|
|
69
|
+
changeInput(screen.getByLabelText('First name'), ' John ')
|
|
70
|
+
fireEvent.submit(screen.getByRole('button', { name: /save changes/i }).closest('form')!)
|
|
71
|
+
|
|
72
|
+
await waitFor(() => {
|
|
73
|
+
expect(onSubmit).toHaveBeenCalledWith({
|
|
74
|
+
first_name: 'John',
|
|
75
|
+
last_name: 'Doe',
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('shows success message after successful submit', async () => {
|
|
81
|
+
const onSubmit = jest.fn().mockResolvedValue(undefined)
|
|
82
|
+
render(<ProfileForm {...defaultProfileProps} onSubmit={onSubmit} />)
|
|
83
|
+
|
|
84
|
+
changeInput(screen.getByLabelText('First name'), 'John')
|
|
85
|
+
fireEvent.submit(screen.getByRole('button', { name: /save changes/i }).closest('form')!)
|
|
86
|
+
|
|
87
|
+
await waitFor(() => {
|
|
88
|
+
expect(screen.getByText('Profile updated.')).toBeInTheDocument()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('shows error message when onSubmit throws', async () => {
|
|
93
|
+
const onSubmit = jest.fn().mockRejectedValue(new Error('Server error'))
|
|
94
|
+
render(<ProfileForm {...defaultProfileProps} onSubmit={onSubmit} />)
|
|
95
|
+
|
|
96
|
+
changeInput(screen.getByLabelText('First name'), 'John')
|
|
97
|
+
fireEvent.submit(screen.getByRole('button', { name: /save changes/i }).closest('form')!)
|
|
98
|
+
|
|
99
|
+
await waitFor(() => {
|
|
100
|
+
expect(screen.getByText('Server error')).toBeInTheDocument()
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('shows generic error message when thrown value is not an Error', async () => {
|
|
105
|
+
const onSubmit = jest.fn().mockRejectedValue('oops')
|
|
106
|
+
render(<ProfileForm {...defaultProfileProps} onSubmit={onSubmit} />)
|
|
107
|
+
|
|
108
|
+
changeInput(screen.getByLabelText('First name'), 'John')
|
|
109
|
+
fireEvent.submit(screen.getByRole('button', { name: /save changes/i }).closest('form')!)
|
|
110
|
+
|
|
111
|
+
await waitFor(() => {
|
|
112
|
+
expect(screen.getByText('Failed to update profile')).toBeInTheDocument()
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('disables inputs when disabled prop is true', () => {
|
|
117
|
+
render(<ProfileForm {...defaultProfileProps} disabled />)
|
|
118
|
+
expect(screen.getByLabelText('First name')).toBeDisabled()
|
|
119
|
+
expect(screen.getByLabelText('Last name')).toBeDisabled()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('renders card title', () => {
|
|
123
|
+
render(<ProfileForm {...defaultProfileProps} />)
|
|
124
|
+
expect(screen.getByText('Profile')).toBeInTheDocument()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('renders card description', () => {
|
|
128
|
+
render(<ProfileForm {...defaultProfileProps} />)
|
|
129
|
+
expect(screen.getByText('Update your personal information.')).toBeInTheDocument()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('renders email-cannot-be-changed hint', () => {
|
|
133
|
+
render(<ProfileForm {...defaultProfileProps} />)
|
|
134
|
+
expect(screen.getByText('Email cannot be changed.')).toBeInTheDocument()
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// ChangePasswordForm
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
const defaultPasswordProps = {
|
|
143
|
+
onSubmit: jest.fn(),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
describe('ChangePasswordForm', () => {
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
jest.clearAllMocks()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('renders all three password fields', () => {
|
|
152
|
+
render(<ChangePasswordForm {...defaultPasswordProps} />)
|
|
153
|
+
expect(screen.getByLabelText('Current password')).toBeInTheDocument()
|
|
154
|
+
expect(screen.getByLabelText('New password')).toBeInTheDocument()
|
|
155
|
+
expect(screen.getByLabelText('Confirm new password')).toBeInTheDocument()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('submit button is disabled when fields are empty', () => {
|
|
159
|
+
render(<ChangePasswordForm {...defaultPasswordProps} />)
|
|
160
|
+
expect(screen.getByRole('button', { name: /change password/i })).toBeDisabled()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('submit button is disabled when new password is less than 8 characters', () => {
|
|
164
|
+
render(<ChangePasswordForm {...defaultPasswordProps} />)
|
|
165
|
+
|
|
166
|
+
changeInput(screen.getByLabelText('Current password'), 'oldpassword')
|
|
167
|
+
changeInput(screen.getByLabelText('New password'), 'short')
|
|
168
|
+
changeInput(screen.getByLabelText('Confirm new password'), 'short')
|
|
169
|
+
|
|
170
|
+
expect(screen.getByRole('button', { name: /change password/i })).toBeDisabled()
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('shows password length hint when new password has 1-7 chars', () => {
|
|
174
|
+
render(<ChangePasswordForm {...defaultPasswordProps} />)
|
|
175
|
+
changeInput(screen.getByLabelText('New password'), 'abc')
|
|
176
|
+
expect(screen.getByText('Must be at least 8 characters.')).toBeInTheDocument()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('hides length hint when new password is empty', () => {
|
|
180
|
+
render(<ChangePasswordForm {...defaultPasswordProps} />)
|
|
181
|
+
// Don't type anything — hint should not appear
|
|
182
|
+
expect(screen.queryByText('Must be at least 8 characters.')).not.toBeInTheDocument()
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('submit button is disabled when passwords do not match', () => {
|
|
186
|
+
render(<ChangePasswordForm {...defaultPasswordProps} />)
|
|
187
|
+
|
|
188
|
+
changeInput(screen.getByLabelText('Current password'), 'oldpassword')
|
|
189
|
+
changeInput(screen.getByLabelText('New password'), 'newpassword1')
|
|
190
|
+
changeInput(screen.getByLabelText('Confirm new password'), 'different123')
|
|
191
|
+
|
|
192
|
+
expect(screen.getByRole('button', { name: /change password/i })).toBeDisabled()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('shows mismatch hint when confirm does not match', () => {
|
|
196
|
+
render(<ChangePasswordForm {...defaultPasswordProps} />)
|
|
197
|
+
changeInput(screen.getByLabelText('New password'), 'newpassword1')
|
|
198
|
+
changeInput(screen.getByLabelText('Confirm new password'), 'different123')
|
|
199
|
+
expect(screen.getByText('Passwords do not match.')).toBeInTheDocument()
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('submit button is enabled when all fields are valid', () => {
|
|
203
|
+
render(<ChangePasswordForm {...defaultPasswordProps} />)
|
|
204
|
+
|
|
205
|
+
changeInput(screen.getByLabelText('Current password'), 'oldpassword')
|
|
206
|
+
changeInput(screen.getByLabelText('New password'), 'newpassword1')
|
|
207
|
+
changeInput(screen.getByLabelText('Confirm new password'), 'newpassword1')
|
|
208
|
+
|
|
209
|
+
expect(screen.getByRole('button', { name: /change password/i })).toBeEnabled()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('calls onSubmit with correct payload', async () => {
|
|
213
|
+
const onSubmit = jest.fn().mockResolvedValue(undefined)
|
|
214
|
+
render(<ChangePasswordForm onSubmit={onSubmit} />)
|
|
215
|
+
|
|
216
|
+
changeInput(screen.getByLabelText('Current password'), 'oldpassword')
|
|
217
|
+
changeInput(screen.getByLabelText('New password'), 'newpassword1')
|
|
218
|
+
changeInput(screen.getByLabelText('Confirm new password'), 'newpassword1')
|
|
219
|
+
|
|
220
|
+
fireEvent.click(screen.getByRole('button', { name: /change password/i }))
|
|
221
|
+
|
|
222
|
+
await waitFor(() => {
|
|
223
|
+
expect(onSubmit).toHaveBeenCalledWith({
|
|
224
|
+
old_password: 'oldpassword',
|
|
225
|
+
new_password: 'newpassword1',
|
|
226
|
+
new_password_confirm: 'newpassword1',
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('shows success message and clears fields after successful submit', async () => {
|
|
232
|
+
const onSubmit = jest.fn().mockResolvedValue(undefined)
|
|
233
|
+
render(<ChangePasswordForm onSubmit={onSubmit} />)
|
|
234
|
+
|
|
235
|
+
changeInput(screen.getByLabelText('Current password'), 'oldpassword')
|
|
236
|
+
changeInput(screen.getByLabelText('New password'), 'newpassword1')
|
|
237
|
+
changeInput(screen.getByLabelText('Confirm new password'), 'newpassword1')
|
|
238
|
+
|
|
239
|
+
fireEvent.click(screen.getByRole('button', { name: /change password/i }))
|
|
240
|
+
|
|
241
|
+
await waitFor(() => {
|
|
242
|
+
expect(screen.getByText('Password changed successfully.')).toBeInTheDocument()
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
expect(screen.getByLabelText('Current password')).toHaveValue('')
|
|
246
|
+
expect(screen.getByLabelText('New password')).toHaveValue('')
|
|
247
|
+
expect(screen.getByLabelText('Confirm new password')).toHaveValue('')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('calls onSuccess callback after successful submit', async () => {
|
|
251
|
+
const onSubmit = jest.fn().mockResolvedValue(undefined)
|
|
252
|
+
const onSuccess = jest.fn()
|
|
253
|
+
render(<ChangePasswordForm onSubmit={onSubmit} onSuccess={onSuccess} />)
|
|
254
|
+
|
|
255
|
+
changeInput(screen.getByLabelText('Current password'), 'oldpassword')
|
|
256
|
+
changeInput(screen.getByLabelText('New password'), 'newpassword1')
|
|
257
|
+
changeInput(screen.getByLabelText('Confirm new password'), 'newpassword1')
|
|
258
|
+
|
|
259
|
+
fireEvent.click(screen.getByRole('button', { name: /change password/i }))
|
|
260
|
+
|
|
261
|
+
await waitFor(() => {
|
|
262
|
+
expect(onSuccess).toHaveBeenCalledTimes(1)
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('shows error message when onSubmit throws', async () => {
|
|
267
|
+
const onSubmit = jest.fn().mockRejectedValue(new Error('Wrong password'))
|
|
268
|
+
render(<ChangePasswordForm onSubmit={onSubmit} />)
|
|
269
|
+
|
|
270
|
+
changeInput(screen.getByLabelText('Current password'), 'wrongold')
|
|
271
|
+
changeInput(screen.getByLabelText('New password'), 'newpassword1')
|
|
272
|
+
changeInput(screen.getByLabelText('Confirm new password'), 'newpassword1')
|
|
273
|
+
|
|
274
|
+
fireEvent.click(screen.getByRole('button', { name: /change password/i }))
|
|
275
|
+
|
|
276
|
+
await waitFor(() => {
|
|
277
|
+
expect(screen.getByText('Wrong password')).toBeInTheDocument()
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('shows generic error when thrown value is not Error instance', async () => {
|
|
282
|
+
const onSubmit = jest.fn().mockRejectedValue('oops')
|
|
283
|
+
render(<ChangePasswordForm onSubmit={onSubmit} />)
|
|
284
|
+
|
|
285
|
+
changeInput(screen.getByLabelText('Current password'), 'wrongold')
|
|
286
|
+
changeInput(screen.getByLabelText('New password'), 'newpassword1')
|
|
287
|
+
changeInput(screen.getByLabelText('Confirm new password'), 'newpassword1')
|
|
288
|
+
|
|
289
|
+
fireEvent.click(screen.getByRole('button', { name: /change password/i }))
|
|
290
|
+
|
|
291
|
+
await waitFor(() => {
|
|
292
|
+
expect(screen.getByText('Failed to change password')).toBeInTheDocument()
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('disables all inputs when disabled prop is true', () => {
|
|
297
|
+
render(<ChangePasswordForm {...defaultPasswordProps} disabled />)
|
|
298
|
+
expect(screen.getByLabelText('Current password')).toBeDisabled()
|
|
299
|
+
expect(screen.getByLabelText('New password')).toBeDisabled()
|
|
300
|
+
expect(screen.getByLabelText('Confirm new password')).toBeDisabled()
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('renders card heading', () => {
|
|
304
|
+
render(<ChangePasswordForm {...defaultPasswordProps} />)
|
|
305
|
+
// Use role query to specifically find the heading, not the button
|
|
306
|
+
expect(screen.getByRole('heading', { name: /change password/i })).toBeInTheDocument()
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('renders card description', () => {
|
|
310
|
+
render(<ChangePasswordForm {...defaultPasswordProps} />)
|
|
311
|
+
expect(
|
|
312
|
+
screen.getByText(/update your password/i)
|
|
313
|
+
).toBeInTheDocument()
|
|
314
|
+
})
|
|
315
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
|
|
5
|
+
export interface StageBadgeConfig {
|
|
6
|
+
label: string
|
|
7
|
+
className: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface StageBadgeProps {
|
|
11
|
+
stage: string
|
|
12
|
+
configMap: Record<string, StageBadgeConfig>
|
|
13
|
+
className?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULT_CONFIG: StageBadgeConfig = {
|
|
17
|
+
label: '',
|
|
18
|
+
className: 'bg-gray-100 text-gray-700 border-gray-200',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function StageBadge({ stage, configMap, className = '' }: StageBadgeProps) {
|
|
22
|
+
const config = configMap[stage] ?? { ...DEFAULT_CONFIG, label: stage }
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<span
|
|
26
|
+
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${config.className} ${className}`}
|
|
27
|
+
>
|
|
28
|
+
{config.label}
|
|
29
|
+
</span>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
export interface CommandGroupProps {
|
|
6
|
+
label: string
|
|
7
|
+
/** Whether to show a top border separator */
|
|
8
|
+
showBorder?: boolean
|
|
9
|
+
children: React.ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function CommandGroup({ label, showBorder = false, children }: CommandGroupProps) {
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<div className={`px-3 py-2${showBorder ? ' border-t border-gray-100' : ''}`}>
|
|
16
|
+
<p className="text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
17
|
+
{label}
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
{children}
|
|
21
|
+
</>
|
|
22
|
+
)
|
|
23
|
+
}
|