@open-mercato/ui 0.6.4-develop.4178.1.aad9ddaa95 → 0.6.4-develop.4210.1.d412061cfe
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +14 -0
- package/dist/backend/DataTable.js +28 -62
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/detail/AttachmentMetadataDialog.js +1 -1
- package/dist/backend/detail/AttachmentMetadataDialog.js.map +2 -2
- package/dist/primitives/activity-feed.js +196 -0
- package/dist/primitives/activity-feed.js.map +7 -0
- package/dist/primitives/avatar.js +140 -14
- package/dist/primitives/avatar.js.map +3 -3
- package/dist/primitives/badge.js +94 -5
- package/dist/primitives/badge.js.map +2 -2
- package/dist/primitives/button-group.js +52 -0
- package/dist/primitives/button-group.js.map +7 -0
- package/dist/primitives/color-picker.js +520 -0
- package/dist/primitives/color-picker.js.map +7 -0
- package/dist/primitives/command-menu.js +355 -0
- package/dist/primitives/command-menu.js.map +7 -0
- package/dist/primitives/dialog.js +210 -35
- package/dist/primitives/dialog.js.map +2 -2
- package/dist/primitives/drawer.js +219 -0
- package/dist/primitives/drawer.js.map +7 -0
- package/dist/primitives/notification-feed.js +213 -0
- package/dist/primitives/notification-feed.js.map +7 -0
- package/dist/primitives/notification.js +23 -2
- package/dist/primitives/notification.js.map +2 -2
- package/dist/primitives/pagination.js +245 -0
- package/dist/primitives/pagination.js.map +7 -0
- package/dist/primitives/progress.js +178 -5
- package/dist/primitives/progress.js.map +2 -2
- package/dist/primitives/rating.js +174 -0
- package/dist/primitives/rating.js.map +7 -0
- package/dist/primitives/scroll-area.js +82 -0
- package/dist/primitives/scroll-area.js.map +7 -0
- package/dist/primitives/segmented-control.js +101 -0
- package/dist/primitives/segmented-control.js.map +7 -0
- package/dist/primitives/separator.js +103 -4
- package/dist/primitives/separator.js.map +2 -2
- package/dist/primitives/slider.js +115 -0
- package/dist/primitives/slider.js.map +7 -0
- package/dist/primitives/step-indicator.js +187 -0
- package/dist/primitives/step-indicator.js.map +7 -0
- package/dist/primitives/table.js +102 -10
- package/dist/primitives/table.js.map +2 -2
- package/dist/primitives/tabs.js +122 -14
- package/dist/primitives/tabs.js.map +2 -2
- package/dist/primitives/time-picker.js +9 -11
- package/dist/primitives/time-picker.js.map +2 -2
- package/package.json +8 -4
- package/src/backend/DataTable.tsx +43 -60
- package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
- package/src/primitives/__tests__/activity-feed.test.tsx +216 -0
- package/src/primitives/__tests__/avatar.test.tsx +131 -0
- package/src/primitives/__tests__/badge.test.tsx +189 -0
- package/src/primitives/__tests__/button-group.test.tsx +117 -0
- package/src/primitives/__tests__/color-picker.test.tsx +589 -0
- package/src/primitives/__tests__/command-menu.test.tsx +231 -0
- package/src/primitives/__tests__/dialog.test.tsx +290 -0
- package/src/primitives/__tests__/drawer.test.tsx +264 -0
- package/src/primitives/__tests__/notification-feed.test.tsx +303 -0
- package/src/primitives/__tests__/notification.test.tsx +102 -2
- package/src/primitives/__tests__/pagination.test.tsx +268 -0
- package/src/primitives/__tests__/progress.test.tsx +222 -0
- package/src/primitives/__tests__/rating.test.tsx +164 -0
- package/src/primitives/__tests__/scroll-area.test.tsx +109 -0
- package/src/primitives/__tests__/segmented-control.test.tsx +129 -0
- package/src/primitives/__tests__/separator.test.tsx +143 -0
- package/src/primitives/__tests__/slider.test.tsx +86 -0
- package/src/primitives/__tests__/step-indicator.test.tsx +205 -0
- package/src/primitives/__tests__/table.test.tsx +199 -0
- package/src/primitives/__tests__/tabs.test.tsx +139 -0
- package/src/primitives/activity-feed.tsx +326 -0
- package/src/primitives/avatar.tsx +225 -22
- package/src/primitives/badge.tsx +141 -6
- package/src/primitives/button-group.tsx +100 -0
- package/src/primitives/color-picker.tsx +693 -0
- package/src/primitives/command-menu.tsx +503 -0
- package/src/primitives/dialog.tsx +315 -51
- package/src/primitives/drawer.tsx +338 -0
- package/src/primitives/notification-feed.tsx +358 -0
- package/src/primitives/notification.tsx +42 -2
- package/src/primitives/pagination.tsx +376 -0
- package/src/primitives/progress.tsx +267 -6
- package/src/primitives/rating.tsx +245 -0
- package/src/primitives/scroll-area.tsx +169 -0
- package/src/primitives/segmented-control.tsx +162 -0
- package/src/primitives/separator.tsx +163 -3
- package/src/primitives/slider.tsx +160 -0
- package/src/primitives/step-indicator.tsx +287 -0
- package/src/primitives/table.tsx +142 -11
- package/src/primitives/tabs.tsx +174 -12
- package/src/primitives/time-picker.tsx +9 -11
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { render as rtlRender, fireEvent, screen, waitFor } from '@testing-library/react'
|
|
5
|
+
import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { ColorPicker, normalizeHex, COLOR_PICKER_DEFAULT_SWATCHES } from '../color-picker'
|
|
7
|
+
|
|
8
|
+
// ColorPicker uses useT() for the eyedropper aria-label; wrap every
|
|
9
|
+
// render in an empty-dict I18nProvider so the primitive falls back
|
|
10
|
+
// to its hardcoded English defaults.
|
|
11
|
+
const render: typeof rtlRender = (ui: React.ReactElement, options?: Parameters<typeof rtlRender>[1]) =>
|
|
12
|
+
rtlRender(
|
|
13
|
+
<I18nProvider locale="en" dict={{}}>
|
|
14
|
+
{ui}
|
|
15
|
+
</I18nProvider>,
|
|
16
|
+
options,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
describe('normalizeHex', () => {
|
|
20
|
+
it('uppercases and prefixes 6-digit hex', () => {
|
|
21
|
+
expect(normalizeHex('6366f1')).toBe('#6366F1')
|
|
22
|
+
expect(normalizeHex('#6366f1')).toBe('#6366F1')
|
|
23
|
+
expect(normalizeHex(' #abc123 ')).toBe('#ABC123')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('expands 3-digit hex to 6 and uppercases', () => {
|
|
27
|
+
expect(normalizeHex('#f0a')).toBe('#FF00AA')
|
|
28
|
+
expect(normalizeHex('abc')).toBe('#AABBCC')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('returns null for invalid input', () => {
|
|
32
|
+
expect(normalizeHex('not-a-hex')).toBeNull()
|
|
33
|
+
expect(normalizeHex('#12345')).toBeNull()
|
|
34
|
+
expect(normalizeHex('#1234567')).toBeNull()
|
|
35
|
+
expect(normalizeHex('')).toBeNull()
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('ColorPicker', () => {
|
|
40
|
+
it('renders a trigger button with the current color preview and hex label', () => {
|
|
41
|
+
const { container } = render(<ColorPicker value="#6366F1" onChange={() => {}} />)
|
|
42
|
+
const trigger = container.querySelector('[data-slot="color-picker-trigger"]') as HTMLButtonElement
|
|
43
|
+
expect(trigger).not.toBeNull()
|
|
44
|
+
expect(trigger.textContent).toContain('#6366F1')
|
|
45
|
+
const preview = container.querySelector('[data-slot="color-picker-preview"]') as HTMLSpanElement
|
|
46
|
+
expect(preview.style.backgroundColor).toBeTruthy()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('opens the popover when the trigger is clicked', async () => {
|
|
50
|
+
const { container } = render(<ColorPicker value="#6366F1" onChange={() => {}} />)
|
|
51
|
+
const trigger = container.querySelector('[data-slot="color-picker-trigger"]') as HTMLButtonElement
|
|
52
|
+
fireEvent.click(trigger)
|
|
53
|
+
await waitFor(() => {
|
|
54
|
+
expect(screen.queryByText('Choose color')).toBeInTheDocument()
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('Section 1 — Choose color', () => {
|
|
59
|
+
it('renders the hue slider as a native range input', async () => {
|
|
60
|
+
render(<ColorPicker value="#6366F1" onChange={() => {}} />)
|
|
61
|
+
fireEvent.click(screen.getByRole('button'))
|
|
62
|
+
await waitFor(() => {
|
|
63
|
+
const slider = document.querySelector('[data-slot="color-picker-hue-slider"]') as HTMLInputElement
|
|
64
|
+
expect(slider).not.toBeNull()
|
|
65
|
+
expect(slider.tagName).toBe('INPUT')
|
|
66
|
+
expect(slider.type).toBe('range')
|
|
67
|
+
expect(slider.min).toBe('0')
|
|
68
|
+
expect(slider.max).toBe('360')
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('changing the hue slider fires onChange with a pure saturated color', async () => {
|
|
73
|
+
const onChange = jest.fn()
|
|
74
|
+
render(<ColorPicker value="#6366F1" onChange={onChange} />)
|
|
75
|
+
fireEvent.click(screen.getByRole('button'))
|
|
76
|
+
await waitFor(() => {
|
|
77
|
+
expect(document.querySelector('[data-slot="color-picker-hue-slider"]')).not.toBeNull()
|
|
78
|
+
})
|
|
79
|
+
const slider = document.querySelector(
|
|
80
|
+
'[data-slot="color-picker-hue-slider"]',
|
|
81
|
+
) as HTMLInputElement
|
|
82
|
+
fireEvent.change(slider, { target: { value: '0' } })
|
|
83
|
+
// Hue 0° + s=100% + l=50% = pure red #FF0000
|
|
84
|
+
expect(onChange).toHaveBeenCalledWith('#FF0000')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('honors the custom chooseLabel prop', async () => {
|
|
88
|
+
render(<ColorPicker value="#6366F1" onChange={() => {}} chooseLabel="Pick a hue" />)
|
|
89
|
+
fireEvent.click(screen.getByRole('button'))
|
|
90
|
+
await waitFor(() => {
|
|
91
|
+
expect(screen.queryByText('Pick a hue')).toBeInTheDocument()
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('Section 2 — Hex input + Eyedropper', () => {
|
|
97
|
+
it('renders the hex input when allowCustom is true (default)', async () => {
|
|
98
|
+
render(<ColorPicker value="#6366F1" onChange={() => {}} />)
|
|
99
|
+
fireEvent.click(screen.getByRole('button'))
|
|
100
|
+
await waitFor(() => {
|
|
101
|
+
expect(document.querySelector('[data-slot="color-picker-hex"]')).not.toBeNull()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('does NOT render the hex input when allowCustom=false', async () => {
|
|
106
|
+
render(<ColorPicker value="#6366F1" onChange={() => {}} allowCustom={false} />)
|
|
107
|
+
fireEvent.click(screen.getByRole('button'))
|
|
108
|
+
await waitFor(() => {
|
|
109
|
+
expect(document.querySelector('[data-slot="color-picker-swatch"]')).not.toBeNull()
|
|
110
|
+
})
|
|
111
|
+
expect(document.querySelector('[data-slot="color-picker-hex"]')).toBeNull()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('commits hex on blur when input is valid', async () => {
|
|
115
|
+
const onChange = jest.fn()
|
|
116
|
+
render(<ColorPicker value="#6366F1" onChange={onChange} />)
|
|
117
|
+
fireEvent.click(screen.getByRole('button'))
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(document.querySelector('[data-slot="color-picker-hex"]')).not.toBeNull()
|
|
120
|
+
})
|
|
121
|
+
const input = document.querySelector('[data-slot="color-picker-hex"]') as HTMLInputElement
|
|
122
|
+
fireEvent.change(input, { target: { value: '#abcdef' } })
|
|
123
|
+
fireEvent.blur(input)
|
|
124
|
+
expect(onChange).toHaveBeenCalledWith('#ABCDEF')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('shows an inline error and does NOT call onChange when hex is invalid', async () => {
|
|
128
|
+
const onChange = jest.fn()
|
|
129
|
+
render(<ColorPicker value="#6366F1" onChange={onChange} />)
|
|
130
|
+
fireEvent.click(screen.getByRole('button'))
|
|
131
|
+
await waitFor(() => {
|
|
132
|
+
expect(document.querySelector('[data-slot="color-picker-hex"]')).not.toBeNull()
|
|
133
|
+
})
|
|
134
|
+
const input = document.querySelector('[data-slot="color-picker-hex"]') as HTMLInputElement
|
|
135
|
+
fireEvent.change(input, { target: { value: 'nope' } })
|
|
136
|
+
fireEvent.blur(input)
|
|
137
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
138
|
+
await waitFor(() => {
|
|
139
|
+
expect(document.querySelector('[data-slot="color-picker-error"]')).not.toBeNull()
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('commits hex on Enter', async () => {
|
|
144
|
+
const onChange = jest.fn()
|
|
145
|
+
render(<ColorPicker value="#6366F1" onChange={onChange} />)
|
|
146
|
+
fireEvent.click(screen.getByRole('button'))
|
|
147
|
+
await waitFor(() => {
|
|
148
|
+
expect(document.querySelector('[data-slot="color-picker-hex"]')).not.toBeNull()
|
|
149
|
+
})
|
|
150
|
+
const input = document.querySelector('[data-slot="color-picker-hex"]') as HTMLInputElement
|
|
151
|
+
fireEvent.change(input, { target: { value: '#111222' } })
|
|
152
|
+
fireEvent.keyDown(input, { key: 'Enter' })
|
|
153
|
+
expect(onChange).toHaveBeenCalledWith('#111222')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('does NOT render the eyedropper button when browser lacks EyeDropper API', async () => {
|
|
157
|
+
// jsdom does not implement EyeDropper — confirm the button is absent.
|
|
158
|
+
render(<ColorPicker value="#6366F1" onChange={() => {}} />)
|
|
159
|
+
fireEvent.click(screen.getByRole('button'))
|
|
160
|
+
await waitFor(() => {
|
|
161
|
+
expect(document.querySelector('[data-slot="color-picker-hex"]')).not.toBeNull()
|
|
162
|
+
})
|
|
163
|
+
expect(document.querySelector('[data-slot="color-picker-eyedropper"]')).toBeNull()
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('Section 3 — Saved colors', () => {
|
|
168
|
+
it('renders the default 10 swatches when no swatches prop is provided', async () => {
|
|
169
|
+
render(<ColorPicker value="#6366F1" onChange={() => {}} />)
|
|
170
|
+
fireEvent.click(screen.getByRole('button'))
|
|
171
|
+
await waitFor(() => {
|
|
172
|
+
const swatches = document.querySelectorAll('[data-slot="color-picker-swatch"]')
|
|
173
|
+
expect(swatches.length).toBe(COLOR_PICKER_DEFAULT_SWATCHES.length)
|
|
174
|
+
expect(COLOR_PICKER_DEFAULT_SWATCHES.length).toBe(10)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('selected swatch dot carries an inset white ring (Figma Selected state)', async () => {
|
|
179
|
+
render(<ColorPicker value="#6366F1" onChange={() => {}} />)
|
|
180
|
+
fireEvent.click(screen.getByRole('button'))
|
|
181
|
+
await waitFor(() => {
|
|
182
|
+
const selected = document.querySelector(
|
|
183
|
+
'[data-slot="color-picker-swatch"][data-state="on"]',
|
|
184
|
+
) as HTMLButtonElement
|
|
185
|
+
expect(selected).not.toBeNull()
|
|
186
|
+
const dot = selected.querySelector(
|
|
187
|
+
'[data-slot="color-picker-swatch-dot"]',
|
|
188
|
+
) as HTMLSpanElement
|
|
189
|
+
expect(dot.className).toContain('ring-2')
|
|
190
|
+
expect(dot.className).toContain('ring-inset')
|
|
191
|
+
expect(dot.className).toContain('ring-background')
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('fires onChange with the swatch value when a swatch is clicked', async () => {
|
|
196
|
+
const onChange = jest.fn()
|
|
197
|
+
render(<ColorPicker value="#6366F1" onChange={onChange} />)
|
|
198
|
+
fireEvent.click(screen.getByRole('button'))
|
|
199
|
+
await waitFor(() => {
|
|
200
|
+
expect(document.querySelectorAll('[data-slot="color-picker-swatch"]').length).toBeGreaterThan(0)
|
|
201
|
+
})
|
|
202
|
+
const target = Array.from(
|
|
203
|
+
document.querySelectorAll('[data-slot="color-picker-swatch"]'),
|
|
204
|
+
).find((node) => node.getAttribute('aria-label') === '#22C55E') as HTMLButtonElement
|
|
205
|
+
fireEvent.click(target)
|
|
206
|
+
expect(onChange).toHaveBeenCalledWith('#22C55E')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('honors a custom swatches palette', async () => {
|
|
210
|
+
render(
|
|
211
|
+
<ColorPicker
|
|
212
|
+
value="#FF0000"
|
|
213
|
+
onChange={() => {}}
|
|
214
|
+
swatches={['#FF0000', '#00FF00', '#0000FF']}
|
|
215
|
+
/>,
|
|
216
|
+
)
|
|
217
|
+
fireEvent.click(screen.getByRole('button'))
|
|
218
|
+
await waitFor(() => {
|
|
219
|
+
const swatches = document.querySelectorAll('[data-slot="color-picker-swatch"]')
|
|
220
|
+
expect(swatches.length).toBe(3)
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('honors the savedLabel prop', async () => {
|
|
225
|
+
render(<ColorPicker value="#6366F1" onChange={() => {}} savedLabel="My palette" />)
|
|
226
|
+
fireEvent.click(screen.getByRole('button'))
|
|
227
|
+
await waitFor(() => {
|
|
228
|
+
expect(screen.queryByText('My palette')).toBeInTheDocument()
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe('Section 4 — Add new color (optional)', () => {
|
|
234
|
+
it('does NOT render the add button when onAddSwatch is omitted', async () => {
|
|
235
|
+
render(<ColorPicker value="#6366F1" onChange={() => {}} />)
|
|
236
|
+
fireEvent.click(screen.getByRole('button'))
|
|
237
|
+
await waitFor(() => {
|
|
238
|
+
expect(document.querySelector('[data-slot="color-picker-swatch"]')).not.toBeNull()
|
|
239
|
+
})
|
|
240
|
+
expect(document.querySelector('[data-slot="color-picker-add"]')).toBeNull()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('renders the add button when onAddSwatch is provided', async () => {
|
|
244
|
+
render(
|
|
245
|
+
<ColorPicker value="#6366F1" onChange={() => {}} onAddSwatch={() => {}} />,
|
|
246
|
+
)
|
|
247
|
+
fireEvent.click(screen.getByRole('button'))
|
|
248
|
+
await waitFor(() => {
|
|
249
|
+
expect(document.querySelector('[data-slot="color-picker-add"]')).not.toBeNull()
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('fires onAddSwatch with the current value when the add button is clicked', async () => {
|
|
254
|
+
const onAddSwatch = jest.fn()
|
|
255
|
+
render(
|
|
256
|
+
<ColorPicker value="#123456" onChange={() => {}} onAddSwatch={onAddSwatch} />,
|
|
257
|
+
)
|
|
258
|
+
fireEvent.click(screen.getByRole('button'))
|
|
259
|
+
await waitFor(() => {
|
|
260
|
+
expect(document.querySelector('[data-slot="color-picker-add"]')).not.toBeNull()
|
|
261
|
+
})
|
|
262
|
+
const addButton = document.querySelector('[data-slot="color-picker-add"]') as HTMLButtonElement
|
|
263
|
+
fireEvent.click(addButton)
|
|
264
|
+
expect(onAddSwatch).toHaveBeenCalledWith('#123456')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('honors the addLabel prop', async () => {
|
|
268
|
+
render(
|
|
269
|
+
<ColorPicker
|
|
270
|
+
value="#6366F1"
|
|
271
|
+
onChange={() => {}}
|
|
272
|
+
onAddSwatch={() => {}}
|
|
273
|
+
addLabel="Save color"
|
|
274
|
+
/>,
|
|
275
|
+
)
|
|
276
|
+
fireEvent.click(screen.getByRole('button'))
|
|
277
|
+
await waitFor(() => {
|
|
278
|
+
expect(screen.queryByText('Save color')).toBeInTheDocument()
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
describe('Section 2 — extras (opacity / trash)', () => {
|
|
284
|
+
it('renders the "100%" opacity badge inside the hex container when showOpacity=true', async () => {
|
|
285
|
+
render(<ColorPicker value="#6366F1" onChange={() => {}} showOpacity />)
|
|
286
|
+
fireEvent.click(screen.getByRole('button'))
|
|
287
|
+
await waitFor(() => {
|
|
288
|
+
expect(document.querySelector('[data-slot="color-picker-opacity"]')).not.toBeNull()
|
|
289
|
+
})
|
|
290
|
+
expect(
|
|
291
|
+
document.querySelector('[data-slot="color-picker-opacity"]')?.textContent,
|
|
292
|
+
).toBe('100%')
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('does NOT render the opacity badge by default', async () => {
|
|
296
|
+
render(<ColorPicker value="#6366F1" onChange={() => {}} />)
|
|
297
|
+
fireEvent.click(screen.getByRole('button'))
|
|
298
|
+
await waitFor(() => {
|
|
299
|
+
expect(document.querySelector('[data-slot="color-picker-hex"]')).not.toBeNull()
|
|
300
|
+
})
|
|
301
|
+
expect(document.querySelector('[data-slot="color-picker-opacity"]')).toBeNull()
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('does NOT render the remove button when onRemoveColor is omitted', async () => {
|
|
305
|
+
render(<ColorPicker value="#6366F1" onChange={() => {}} />)
|
|
306
|
+
fireEvent.click(screen.getByRole('button'))
|
|
307
|
+
await waitFor(() => {
|
|
308
|
+
expect(document.querySelector('[data-slot="color-picker-hex"]')).not.toBeNull()
|
|
309
|
+
})
|
|
310
|
+
expect(document.querySelector('[data-slot="color-picker-remove"]')).toBeNull()
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('renders the remove (trash) button when onRemoveColor is provided', async () => {
|
|
314
|
+
render(
|
|
315
|
+
<ColorPicker
|
|
316
|
+
value="#6366F1"
|
|
317
|
+
onChange={() => {}}
|
|
318
|
+
onRemoveColor={() => {}}
|
|
319
|
+
/>,
|
|
320
|
+
)
|
|
321
|
+
fireEvent.click(screen.getByRole('button'))
|
|
322
|
+
await waitFor(() => {
|
|
323
|
+
expect(document.querySelector('[data-slot="color-picker-remove"]')).not.toBeNull()
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('fires onRemoveColor with the current value when the trash button is clicked', async () => {
|
|
328
|
+
const onRemoveColor = jest.fn()
|
|
329
|
+
render(
|
|
330
|
+
<ColorPicker
|
|
331
|
+
value="#ABCDEF"
|
|
332
|
+
onChange={() => {}}
|
|
333
|
+
onRemoveColor={onRemoveColor}
|
|
334
|
+
/>,
|
|
335
|
+
)
|
|
336
|
+
fireEvent.click(screen.getByRole('button'))
|
|
337
|
+
await waitFor(() => {
|
|
338
|
+
expect(document.querySelector('[data-slot="color-picker-remove"]')).not.toBeNull()
|
|
339
|
+
})
|
|
340
|
+
const removeButton = document.querySelector(
|
|
341
|
+
'[data-slot="color-picker-remove"]',
|
|
342
|
+
) as HTMLButtonElement
|
|
343
|
+
fireEvent.click(removeButton)
|
|
344
|
+
expect(onRemoveColor).toHaveBeenCalledWith('#ABCDEF')
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
describe('Section 3 — Edit link', () => {
|
|
349
|
+
it('does NOT render the Edit link when onEditSavedColors is omitted', async () => {
|
|
350
|
+
render(<ColorPicker value="#6366F1" onChange={() => {}} />)
|
|
351
|
+
fireEvent.click(screen.getByRole('button'))
|
|
352
|
+
await waitFor(() => {
|
|
353
|
+
expect(document.querySelector('[data-slot="color-picker-swatch"]')).not.toBeNull()
|
|
354
|
+
})
|
|
355
|
+
expect(document.querySelector('[data-slot="color-picker-edit"]')).toBeNull()
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('renders the Edit link in the saved-colors header when onEditSavedColors is provided', async () => {
|
|
359
|
+
render(
|
|
360
|
+
<ColorPicker
|
|
361
|
+
value="#6366F1"
|
|
362
|
+
onChange={() => {}}
|
|
363
|
+
onEditSavedColors={() => {}}
|
|
364
|
+
/>,
|
|
365
|
+
)
|
|
366
|
+
fireEvent.click(screen.getByRole('button'))
|
|
367
|
+
await waitFor(() => {
|
|
368
|
+
expect(document.querySelector('[data-slot="color-picker-edit"]')).not.toBeNull()
|
|
369
|
+
})
|
|
370
|
+
expect(
|
|
371
|
+
document.querySelector('[data-slot="color-picker-edit"]')?.textContent,
|
|
372
|
+
).toBe('Edit')
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('fires onEditSavedColors when the Edit link is clicked', async () => {
|
|
376
|
+
const onEditSavedColors = jest.fn()
|
|
377
|
+
render(
|
|
378
|
+
<ColorPicker
|
|
379
|
+
value="#6366F1"
|
|
380
|
+
onChange={() => {}}
|
|
381
|
+
onEditSavedColors={onEditSavedColors}
|
|
382
|
+
/>,
|
|
383
|
+
)
|
|
384
|
+
fireEvent.click(screen.getByRole('button'))
|
|
385
|
+
await waitFor(() => {
|
|
386
|
+
expect(document.querySelector('[data-slot="color-picker-edit"]')).not.toBeNull()
|
|
387
|
+
})
|
|
388
|
+
const editLink = document.querySelector(
|
|
389
|
+
'[data-slot="color-picker-edit"]',
|
|
390
|
+
) as HTMLButtonElement
|
|
391
|
+
fireEvent.click(editLink)
|
|
392
|
+
expect(onEditSavedColors).toHaveBeenCalled()
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('honors the editLabel prop', async () => {
|
|
396
|
+
render(
|
|
397
|
+
<ColorPicker
|
|
398
|
+
value="#6366F1"
|
|
399
|
+
onChange={() => {}}
|
|
400
|
+
onEditSavedColors={() => {}}
|
|
401
|
+
editLabel="Manage"
|
|
402
|
+
/>,
|
|
403
|
+
)
|
|
404
|
+
fireEvent.click(screen.getByRole('button'))
|
|
405
|
+
await waitFor(() => {
|
|
406
|
+
expect(screen.queryByText('Manage')).toBeInTheDocument()
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
describe('persistKey — uncontrolled swatches with localStorage', () => {
|
|
412
|
+
beforeEach(() => {
|
|
413
|
+
window.localStorage.clear()
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it('hydrates the swatches from localStorage on mount when persistKey is set', async () => {
|
|
417
|
+
window.localStorage.setItem(
|
|
418
|
+
'color-picker:test-1',
|
|
419
|
+
JSON.stringify(['#FF0000', '#00FF00', '#0000FF']),
|
|
420
|
+
)
|
|
421
|
+
render(<ColorPicker value="#6366F1" onChange={() => {}} persistKey="color-picker:test-1" />)
|
|
422
|
+
fireEvent.click(screen.getByRole('button'))
|
|
423
|
+
await waitFor(() => {
|
|
424
|
+
const swatches = document.querySelectorAll('[data-slot="color-picker-swatch"]')
|
|
425
|
+
expect(swatches.length).toBe(3)
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('falls back to defaultSwatches when localStorage is empty', async () => {
|
|
430
|
+
render(
|
|
431
|
+
<ColorPicker
|
|
432
|
+
value="#6366F1"
|
|
433
|
+
onChange={() => {}}
|
|
434
|
+
persistKey="color-picker:test-empty"
|
|
435
|
+
defaultSwatches={['#000000', '#FFFFFF']}
|
|
436
|
+
/>,
|
|
437
|
+
)
|
|
438
|
+
fireEvent.click(screen.getByRole('button'))
|
|
439
|
+
await waitFor(() => {
|
|
440
|
+
const swatches = document.querySelectorAll('[data-slot="color-picker-swatch"]')
|
|
441
|
+
expect(swatches.length).toBe(2)
|
|
442
|
+
})
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it('renders the add + trash buttons automatically when persistKey is set (no callbacks needed)', async () => {
|
|
446
|
+
render(<ColorPicker value="#6366F1" onChange={() => {}} persistKey="color-picker:test-auto" />)
|
|
447
|
+
fireEvent.click(screen.getByRole('button'))
|
|
448
|
+
await waitFor(() => {
|
|
449
|
+
expect(document.querySelector('[data-slot="color-picker-add"]')).not.toBeNull()
|
|
450
|
+
expect(document.querySelector('[data-slot="color-picker-remove"]')).not.toBeNull()
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
it('persists newly-added color to localStorage', async () => {
|
|
455
|
+
render(
|
|
456
|
+
<ColorPicker
|
|
457
|
+
value="#ABCDEF"
|
|
458
|
+
onChange={() => {}}
|
|
459
|
+
persistKey="color-picker:test-add"
|
|
460
|
+
defaultSwatches={['#000000']}
|
|
461
|
+
/>,
|
|
462
|
+
)
|
|
463
|
+
fireEvent.click(screen.getByRole('button'))
|
|
464
|
+
await waitFor(() => {
|
|
465
|
+
expect(document.querySelector('[data-slot="color-picker-add"]')).not.toBeNull()
|
|
466
|
+
})
|
|
467
|
+
const addButton = document.querySelector(
|
|
468
|
+
'[data-slot="color-picker-add"]',
|
|
469
|
+
) as HTMLButtonElement
|
|
470
|
+
fireEvent.click(addButton)
|
|
471
|
+
const stored = JSON.parse(window.localStorage.getItem('color-picker:test-add') ?? '[]')
|
|
472
|
+
expect(stored).toContain('#ABCDEF')
|
|
473
|
+
expect(stored).toContain('#000000')
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
it('persists removal of current color from localStorage', async () => {
|
|
477
|
+
window.localStorage.setItem(
|
|
478
|
+
'color-picker:test-remove',
|
|
479
|
+
JSON.stringify(['#FF0000', '#00FF00', '#0000FF']),
|
|
480
|
+
)
|
|
481
|
+
render(
|
|
482
|
+
<ColorPicker value="#00FF00" onChange={() => {}} persistKey="color-picker:test-remove" />,
|
|
483
|
+
)
|
|
484
|
+
fireEvent.click(screen.getByRole('button'))
|
|
485
|
+
await waitFor(() => {
|
|
486
|
+
expect(document.querySelector('[data-slot="color-picker-remove"]')).not.toBeNull()
|
|
487
|
+
})
|
|
488
|
+
const trash = document.querySelector('[data-slot="color-picker-remove"]') as HTMLButtonElement
|
|
489
|
+
fireEvent.click(trash)
|
|
490
|
+
const stored = JSON.parse(window.localStorage.getItem('color-picker:test-remove') ?? '[]')
|
|
491
|
+
expect(stored).not.toContain('#00FF00')
|
|
492
|
+
expect(stored).toContain('#FF0000')
|
|
493
|
+
expect(stored).toContain('#0000FF')
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it('does NOT duplicate when adding a color that is already in the palette', async () => {
|
|
497
|
+
window.localStorage.setItem(
|
|
498
|
+
'color-picker:test-dedup',
|
|
499
|
+
JSON.stringify(['#AABBCC']),
|
|
500
|
+
)
|
|
501
|
+
render(
|
|
502
|
+
<ColorPicker value="#AABBCC" onChange={() => {}} persistKey="color-picker:test-dedup" />,
|
|
503
|
+
)
|
|
504
|
+
fireEvent.click(screen.getByRole('button'))
|
|
505
|
+
await waitFor(() => {
|
|
506
|
+
expect(document.querySelector('[data-slot="color-picker-add"]')).not.toBeNull()
|
|
507
|
+
})
|
|
508
|
+
const addButton = document.querySelector(
|
|
509
|
+
'[data-slot="color-picker-add"]',
|
|
510
|
+
) as HTMLButtonElement
|
|
511
|
+
fireEvent.click(addButton)
|
|
512
|
+
const stored = JSON.parse(window.localStorage.getItem('color-picker:test-dedup') ?? '[]')
|
|
513
|
+
expect(stored.length).toBe(1)
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
it('still fires onAddSwatch / onRemoveColor as notification callbacks in persisted mode', async () => {
|
|
517
|
+
const onAddSwatch = jest.fn()
|
|
518
|
+
const onRemoveColor = jest.fn()
|
|
519
|
+
window.localStorage.setItem(
|
|
520
|
+
'color-picker:test-notify',
|
|
521
|
+
JSON.stringify(['#111111']),
|
|
522
|
+
)
|
|
523
|
+
render(
|
|
524
|
+
<ColorPicker
|
|
525
|
+
value="#222222"
|
|
526
|
+
onChange={() => {}}
|
|
527
|
+
persistKey="color-picker:test-notify"
|
|
528
|
+
onAddSwatch={onAddSwatch}
|
|
529
|
+
onRemoveColor={onRemoveColor}
|
|
530
|
+
/>,
|
|
531
|
+
)
|
|
532
|
+
fireEvent.click(screen.getByRole('button'))
|
|
533
|
+
await waitFor(() => {
|
|
534
|
+
expect(document.querySelector('[data-slot="color-picker-add"]')).not.toBeNull()
|
|
535
|
+
})
|
|
536
|
+
fireEvent.click(document.querySelector('[data-slot="color-picker-add"]') as HTMLButtonElement)
|
|
537
|
+
expect(onAddSwatch).toHaveBeenCalledWith('#222222')
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
it('controlled mode (swatches prop) takes priority over persistKey', async () => {
|
|
541
|
+
window.localStorage.setItem(
|
|
542
|
+
'color-picker:test-priority',
|
|
543
|
+
JSON.stringify(['#ABCDEF', '#FEDCBA']),
|
|
544
|
+
)
|
|
545
|
+
render(
|
|
546
|
+
<ColorPicker
|
|
547
|
+
value="#6366F1"
|
|
548
|
+
onChange={() => {}}
|
|
549
|
+
swatches={['#FF0000']}
|
|
550
|
+
persistKey="color-picker:test-priority"
|
|
551
|
+
/>,
|
|
552
|
+
)
|
|
553
|
+
fireEvent.click(screen.getByRole('button'))
|
|
554
|
+
await waitFor(() => {
|
|
555
|
+
const swatches = document.querySelectorAll('[data-slot="color-picker-swatch"]')
|
|
556
|
+
expect(swatches.length).toBe(1)
|
|
557
|
+
})
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
it('ignores corrupt JSON in localStorage gracefully', async () => {
|
|
561
|
+
window.localStorage.setItem('color-picker:test-corrupt', 'not-json{')
|
|
562
|
+
render(
|
|
563
|
+
<ColorPicker
|
|
564
|
+
value="#6366F1"
|
|
565
|
+
onChange={() => {}}
|
|
566
|
+
persistKey="color-picker:test-corrupt"
|
|
567
|
+
defaultSwatches={['#AAAAAA']}
|
|
568
|
+
/>,
|
|
569
|
+
)
|
|
570
|
+
fireEvent.click(screen.getByRole('button'))
|
|
571
|
+
await waitFor(() => {
|
|
572
|
+
const swatches = document.querySelectorAll('[data-slot="color-picker-swatch"]')
|
|
573
|
+
expect(swatches.length).toBe(1)
|
|
574
|
+
})
|
|
575
|
+
})
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it('disables the trigger when disabled prop is set', () => {
|
|
579
|
+
const { container } = render(<ColorPicker value="#6366F1" onChange={() => {}} disabled />)
|
|
580
|
+
const trigger = container.querySelector('[data-slot="color-picker-trigger"]') as HTMLButtonElement
|
|
581
|
+
expect(trigger.disabled).toBe(true)
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
it('forwards ref to the trigger element', () => {
|
|
585
|
+
const ref = React.createRef<HTMLButtonElement>()
|
|
586
|
+
render(<ColorPicker ref={ref} value="#6366F1" onChange={() => {}} />)
|
|
587
|
+
expect(ref.current?.getAttribute('data-slot')).toBe('color-picker-trigger')
|
|
588
|
+
})
|
|
589
|
+
})
|