@open-mercato/ui 0.6.1-develop.3246.1.dbef9d7392 → 0.6.1-develop.3260.1.dc719cac84
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/__integration__/TC-AI-UI-003-aichat-registry.spec.tsx +13 -0
- package/dist/ai/AiAssistantLauncher.js +8 -0
- package/dist/ai/AiAssistantLauncher.js.map +2 -2
- package/dist/ai/AiChat.js +226 -58
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/AiDock.js +3 -2
- package/dist/ai/AiDock.js.map +2 -2
- package/dist/ai/ModelPicker.js +186 -0
- package/dist/ai/ModelPicker.js.map +7 -0
- package/dist/ai/index.js +3 -1
- package/dist/ai/index.js.map +2 -2
- package/dist/ai/useAiChat.js +12 -16
- package/dist/ai/useAiChat.js.map +2 -2
- package/dist/backend/CrudForm.js +46 -129
- package/dist/backend/CrudForm.js.map +3 -3
- package/dist/backend/DataTable.js +10 -3
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/EmptyState.js +1 -47
- package/dist/backend/EmptyState.js.map +2 -2
- package/dist/backend/FilterOverlay.js +31 -26
- package/dist/backend/FilterOverlay.js.map +2 -2
- package/dist/backend/FlashMessages.js +23 -25
- package/dist/backend/FlashMessages.js.map +2 -2
- package/dist/backend/JsonBuilder.js +4 -2
- package/dist/backend/JsonBuilder.js.map +2 -2
- package/dist/backend/LogList.js +49 -0
- package/dist/backend/LogList.js.map +7 -0
- package/dist/backend/crud/CollapsibleGroup.js +13 -3
- package/dist/backend/crud/CollapsibleGroup.js.map +2 -2
- package/dist/backend/detail/InlineEditors.js +80 -32
- package/dist/backend/detail/InlineEditors.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js +60 -6
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/dist/backend/inputs/DatePicker.js +6 -142
- package/dist/backend/inputs/DatePicker.js.map +2 -2
- package/dist/backend/inputs/DateTimePicker.js +6 -182
- package/dist/backend/inputs/DateTimePicker.js.map +2 -2
- package/dist/backend/inputs/TagsInput.js +16 -19
- package/dist/backend/inputs/TagsInput.js.map +2 -2
- package/dist/backend/inputs/TimeInput.js +1 -1
- package/dist/backend/inputs/TimeInput.js.map +2 -2
- package/dist/backend/inputs/TimePicker.js +83 -72
- package/dist/backend/inputs/TimePicker.js.map +2 -2
- package/dist/backend/utils/customFieldForms.js +2 -2
- package/dist/backend/utils/customFieldForms.js.map +1 -1
- package/dist/portal/components/PortalEmptyState.js +13 -9
- package/dist/portal/components/PortalEmptyState.js.map +2 -2
- package/dist/primitives/accordion.js +137 -0
- package/dist/primitives/accordion.js.map +7 -0
- package/dist/primitives/alert.js +180 -12
- package/dist/primitives/alert.js.map +2 -2
- package/dist/primitives/calendar.js +122 -32
- package/dist/primitives/calendar.js.map +2 -2
- package/dist/primitives/compact-select.js +47 -0
- package/dist/primitives/compact-select.js.map +7 -0
- package/dist/primitives/counter-input.js +174 -0
- package/dist/primitives/counter-input.js.map +7 -0
- package/dist/primitives/date-picker-helpers.js +79 -0
- package/dist/primitives/date-picker-helpers.js.map +7 -0
- package/dist/primitives/date-picker.js +216 -0
- package/dist/primitives/date-picker.js.map +7 -0
- package/dist/primitives/date-range-picker.js +239 -0
- package/dist/primitives/date-range-picker.js.map +7 -0
- package/dist/primitives/digit-input.js +157 -0
- package/dist/primitives/digit-input.js.map +7 -0
- package/dist/primitives/empty-state.js +93 -0
- package/dist/primitives/empty-state.js.map +7 -0
- package/dist/primitives/inline-input.js +29 -0
- package/dist/primitives/inline-input.js.map +7 -0
- package/dist/primitives/inline-select.js +41 -0
- package/dist/primitives/inline-select.js.map +7 -0
- package/dist/primitives/notification-stack.js +116 -0
- package/dist/primitives/notification-stack.js.map +7 -0
- package/dist/primitives/notification.js +62 -0
- package/dist/primitives/notification.js.map +7 -0
- package/dist/primitives/rich-editor.js +1230 -0
- package/dist/primitives/rich-editor.js.map +7 -0
- package/dist/primitives/select.js +1 -0
- package/dist/primitives/select.js.map +2 -2
- package/dist/primitives/skeleton.js +55 -0
- package/dist/primitives/skeleton.js.map +7 -0
- package/dist/primitives/tag-input.js +186 -0
- package/dist/primitives/tag-input.js.map +7 -0
- package/dist/primitives/tag.js +66 -13
- package/dist/primitives/tag.js.map +2 -2
- package/dist/primitives/time-picker.js +793 -0
- package/dist/primitives/time-picker.js.map +7 -0
- package/package.json +4 -3
- package/src/ai/AiAssistantLauncher.tsx +8 -0
- package/src/ai/AiChat.tsx +229 -11
- package/src/ai/AiDock.tsx +3 -2
- package/src/ai/ModelPicker.tsx +248 -0
- package/src/ai/__tests__/AiAssistantLauncher.test.tsx +58 -0
- package/src/ai/__tests__/AiChat.conversation.test.tsx +16 -0
- package/src/ai/__tests__/AiChat.debug.test.tsx +16 -0
- package/src/ai/__tests__/AiChat.registry.test.tsx +16 -0
- package/src/ai/__tests__/AiChat.test.tsx +180 -0
- package/src/ai/__tests__/AiChat.thinking-unmount.test.tsx +16 -0
- package/src/ai/__tests__/AiDock.test.tsx +12 -1
- package/src/ai/__tests__/ModelPicker.test.tsx +252 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/useAiChat.ts +29 -16
- package/src/backend/CrudForm.tsx +61 -113
- package/src/backend/DataTable.tsx +17 -5
- package/src/backend/EmptyState.tsx +13 -63
- package/src/backend/FilterOverlay.tsx +38 -22
- package/src/backend/FlashMessages.tsx +23 -24
- package/src/backend/JsonBuilder.tsx +4 -2
- package/src/backend/LogList.tsx +109 -0
- package/src/backend/__tests__/CrudForm.datetime.test.tsx +12 -7
- package/src/backend/__tests__/FilterOverlay.dateRange.test.tsx +89 -0
- package/src/backend/__tests__/LogList.test.tsx +163 -0
- package/src/backend/crud/CollapsibleGroup.tsx +17 -3
- package/src/backend/detail/InlineEditors.tsx +78 -25
- package/src/backend/detail/__tests__/InlineEditors.test.tsx +81 -1
- package/src/backend/inputs/ComboboxInput.tsx +81 -9
- package/src/backend/inputs/DatePicker.tsx +28 -155
- package/src/backend/inputs/DateTimePicker.tsx +25 -193
- package/src/backend/inputs/TagsInput.tsx +10 -13
- package/src/backend/inputs/TimeInput.tsx +2 -1
- package/src/backend/inputs/TimePicker.tsx +99 -70
- package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +133 -0
- package/src/backend/inputs/__tests__/TimePicker.test.tsx +2 -2
- package/src/backend/utils/__tests__/richTextSanitizer.test.ts +5 -2
- package/src/backend/utils/customFieldForms.ts +2 -2
- package/src/portal/components/PortalEmptyState.tsx +27 -22
- package/src/primitives/__tests__/accordion.test.tsx +197 -0
- package/src/primitives/__tests__/alert.test.tsx +204 -0
- package/src/primitives/__tests__/compact-select.test.tsx +98 -0
- package/src/primitives/__tests__/counter-input.test.tsx +165 -0
- package/src/primitives/__tests__/date-picker.test.tsx +221 -0
- package/src/primitives/__tests__/date-range-picker.test.tsx +173 -0
- package/src/primitives/__tests__/digit-input.test.tsx +168 -0
- package/src/primitives/__tests__/empty-state.test.tsx +154 -0
- package/src/primitives/__tests__/inline-input.test.tsx +100 -0
- package/src/primitives/__tests__/inline-select.test.tsx +103 -0
- package/src/primitives/__tests__/notification-stack.test.tsx +174 -0
- package/src/primitives/__tests__/notification.test.tsx +119 -0
- package/src/primitives/__tests__/rich-editor.test.tsx +411 -0
- package/src/primitives/__tests__/skeleton.test.tsx +76 -0
- package/src/primitives/__tests__/tag-input.test.tsx +198 -0
- package/src/primitives/__tests__/tag.test.tsx +123 -0
- package/src/primitives/__tests__/time-picker.test.tsx +392 -0
- package/src/primitives/accordion.tsx +225 -0
- package/src/primitives/alert.tsx +234 -24
- package/src/primitives/calendar.tsx +146 -38
- package/src/primitives/compact-select.tsx +59 -0
- package/src/primitives/counter-input.tsx +211 -0
- package/src/primitives/date-picker-helpers.ts +96 -0
- package/src/primitives/date-picker.tsx +290 -0
- package/src/primitives/date-range-picker.tsx +281 -0
- package/src/primitives/digit-input.tsx +208 -0
- package/src/primitives/empty-state.tsx +140 -0
- package/src/primitives/inline-input.tsx +55 -0
- package/src/primitives/inline-select.tsx +74 -0
- package/src/primitives/notification-stack.tsx +185 -0
- package/src/primitives/notification.tsx +136 -0
- package/src/primitives/rich-editor.tsx +1578 -0
- package/src/primitives/select.tsx +1 -0
- package/src/primitives/skeleton.tsx +70 -0
- package/src/primitives/tag-input.tsx +223 -0
- package/src/primitives/tag.tsx +69 -10
- package/src/primitives/time-picker.tsx +1059 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
7
|
+
import { DigitInput } from '../digit-input'
|
|
8
|
+
|
|
9
|
+
describe('DigitInput primitive', () => {
|
|
10
|
+
it('renders 6 cells by default', () => {
|
|
11
|
+
const { container } = render(<DigitInput />)
|
|
12
|
+
const cells = container.querySelectorAll('[data-slot="digit-input-cell"]')
|
|
13
|
+
expect(cells).toHaveLength(6)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('honors the length prop', () => {
|
|
17
|
+
const { container } = render(<DigitInput length={4} />)
|
|
18
|
+
const cells = container.querySelectorAll('[data-slot="digit-input-cell"]')
|
|
19
|
+
expect(cells).toHaveLength(4)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('renders the value distributed across cells', () => {
|
|
23
|
+
const { container } = render(<DigitInput value="12" length={4} onChange={() => {}} />)
|
|
24
|
+
const cells = container.querySelectorAll<HTMLInputElement>('[data-slot="digit-input-cell"]')
|
|
25
|
+
expect(cells[0].value).toBe('1')
|
|
26
|
+
expect(cells[1].value).toBe('2')
|
|
27
|
+
expect(cells[2].value).toBe('')
|
|
28
|
+
expect(cells[3].value).toBe('')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('focuses the next cell after typing a digit', () => {
|
|
32
|
+
const { container } = render(<DigitInput length={4} onChange={() => {}} />)
|
|
33
|
+
const cells = container.querySelectorAll<HTMLInputElement>('[data-slot="digit-input-cell"]')
|
|
34
|
+
cells[0].focus()
|
|
35
|
+
fireEvent.change(cells[0], { target: { value: '7' } })
|
|
36
|
+
expect(document.activeElement).toBe(cells[1])
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('commits typed digit via onChange', () => {
|
|
40
|
+
const onChange = jest.fn()
|
|
41
|
+
const { container } = render(<DigitInput length={4} onChange={onChange} />)
|
|
42
|
+
const cells = container.querySelectorAll<HTMLInputElement>('[data-slot="digit-input-cell"]')
|
|
43
|
+
fireEvent.change(cells[0], { target: { value: '5' } })
|
|
44
|
+
expect(onChange).toHaveBeenCalledWith('5')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('rejects non-digit characters when inputMode is numeric (default)', () => {
|
|
48
|
+
const onChange = jest.fn()
|
|
49
|
+
const { container } = render(<DigitInput length={4} onChange={onChange} />)
|
|
50
|
+
const cells = container.querySelectorAll<HTMLInputElement>('[data-slot="digit-input-cell"]')
|
|
51
|
+
fireEvent.change(cells[0], { target: { value: 'a' } })
|
|
52
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('accepts non-digit characters when inputMode="text"', () => {
|
|
56
|
+
const onChange = jest.fn()
|
|
57
|
+
const { container } = render(<DigitInput length={4} inputMode="text" onChange={onChange} />)
|
|
58
|
+
const cells = container.querySelectorAll<HTMLInputElement>('[data-slot="digit-input-cell"]')
|
|
59
|
+
fireEvent.change(cells[0], { target: { value: 'X' } })
|
|
60
|
+
expect(onChange).toHaveBeenCalledWith('X')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('Backspace on an empty cell focuses the previous cell and clears its value', () => {
|
|
64
|
+
const onChange = jest.fn()
|
|
65
|
+
const { container } = render(
|
|
66
|
+
<DigitInput value="12" length={4} onChange={onChange} />,
|
|
67
|
+
)
|
|
68
|
+
const cells = container.querySelectorAll<HTMLInputElement>('[data-slot="digit-input-cell"]')
|
|
69
|
+
cells[2].focus()
|
|
70
|
+
fireEvent.keyDown(cells[2], { key: 'Backspace' })
|
|
71
|
+
expect(document.activeElement).toBe(cells[1])
|
|
72
|
+
expect(onChange).toHaveBeenCalledWith('1')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('ArrowLeft moves focus to the previous cell', () => {
|
|
76
|
+
const { container } = render(<DigitInput length={4} onChange={() => {}} />)
|
|
77
|
+
const cells = container.querySelectorAll<HTMLInputElement>('[data-slot="digit-input-cell"]')
|
|
78
|
+
cells[2].focus()
|
|
79
|
+
fireEvent.keyDown(cells[2], { key: 'ArrowLeft' })
|
|
80
|
+
expect(document.activeElement).toBe(cells[1])
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('ArrowRight moves focus to the next cell', () => {
|
|
84
|
+
const { container } = render(<DigitInput length={4} onChange={() => {}} />)
|
|
85
|
+
const cells = container.querySelectorAll<HTMLInputElement>('[data-slot="digit-input-cell"]')
|
|
86
|
+
cells[1].focus()
|
|
87
|
+
fireEvent.keyDown(cells[1], { key: 'ArrowRight' })
|
|
88
|
+
expect(document.activeElement).toBe(cells[2])
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('distributes a pasted string across cells and fires onComplete when filled', () => {
|
|
92
|
+
const onChange = jest.fn()
|
|
93
|
+
const onComplete = jest.fn()
|
|
94
|
+
const { container } = render(
|
|
95
|
+
<DigitInput length={4} onChange={onChange} onComplete={onComplete} />,
|
|
96
|
+
)
|
|
97
|
+
const cells = container.querySelectorAll<HTMLInputElement>('[data-slot="digit-input-cell"]')
|
|
98
|
+
fireEvent.paste(cells[0], {
|
|
99
|
+
clipboardData: { getData: () => '5678' },
|
|
100
|
+
})
|
|
101
|
+
expect(onChange).toHaveBeenCalledWith('5678')
|
|
102
|
+
expect(onComplete).toHaveBeenCalledWith('5678')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('filters non-digit characters out of pasted strings in numeric mode', () => {
|
|
106
|
+
const onChange = jest.fn()
|
|
107
|
+
const { container } = render(<DigitInput length={4} onChange={onChange} />)
|
|
108
|
+
const cells = container.querySelectorAll<HTMLInputElement>('[data-slot="digit-input-cell"]')
|
|
109
|
+
fireEvent.paste(cells[0], {
|
|
110
|
+
clipboardData: { getData: () => '1a2b3c4d' },
|
|
111
|
+
})
|
|
112
|
+
expect(onChange).toHaveBeenCalledWith('1234')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('renders each cell as type="password" when mask is true', () => {
|
|
116
|
+
const { container } = render(
|
|
117
|
+
<DigitInput value="12" length={2} mask onChange={() => {}} />,
|
|
118
|
+
)
|
|
119
|
+
const cells = container.querySelectorAll<HTMLInputElement>('[data-slot="digit-input-cell"]')
|
|
120
|
+
cells.forEach((cell) => expect(cell.type).toBe('password'))
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('disables every cell when disabled prop is set', () => {
|
|
124
|
+
const { container } = render(<DigitInput length={4} disabled onChange={() => {}} />)
|
|
125
|
+
const cells = container.querySelectorAll<HTMLInputElement>('[data-slot="digit-input-cell"]')
|
|
126
|
+
cells.forEach((cell) => expect(cell).toBeDisabled())
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('uncontrolled mode keeps internal state and emits onChange', () => {
|
|
130
|
+
const onChange = jest.fn()
|
|
131
|
+
const { container } = render(<DigitInput length={4} onChange={onChange} />)
|
|
132
|
+
const cells = container.querySelectorAll<HTMLInputElement>('[data-slot="digit-input-cell"]')
|
|
133
|
+
fireEvent.change(cells[0], { target: { value: '1' } })
|
|
134
|
+
fireEvent.change(cells[1], { target: { value: '2' } })
|
|
135
|
+
expect(onChange).toHaveBeenLastCalledWith('12')
|
|
136
|
+
expect(cells[0].value).toBe('1')
|
|
137
|
+
expect(cells[1].value).toBe('2')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('aria-invalid propagates to the group wrapper and to each cell', () => {
|
|
141
|
+
const { container } = render(
|
|
142
|
+
<DigitInput length={2} value="12" onChange={() => {}} aria-invalid />,
|
|
143
|
+
)
|
|
144
|
+
const wrapper = container.querySelector('[data-slot="digit-input"]')
|
|
145
|
+
expect(wrapper).toHaveAttribute('aria-invalid', 'true')
|
|
146
|
+
const cells = container.querySelectorAll('[data-slot="digit-input-cell"]')
|
|
147
|
+
cells.forEach((cell) => expect(cell).toHaveAttribute('aria-invalid', 'true'))
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('uses a provided aria-label for the group and cell labels', () => {
|
|
151
|
+
render(<DigitInput length={3} aria-label="Two factor code" onChange={() => {}} />)
|
|
152
|
+
expect(screen.getByRole('group', { name: 'Two factor code' })).toBeInTheDocument()
|
|
153
|
+
expect(screen.getByLabelText('Two factor code digit 1')).toBeInTheDocument()
|
|
154
|
+
expect(screen.getByLabelText('Two factor code digit 2')).toBeInTheDocument()
|
|
155
|
+
expect(screen.getByLabelText('Two factor code digit 3')).toBeInTheDocument()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('forwards id and name only to the first cell', () => {
|
|
159
|
+
const { container } = render(
|
|
160
|
+
<DigitInput length={3} id="otp" name="code" onChange={() => {}} />,
|
|
161
|
+
)
|
|
162
|
+
const cells = container.querySelectorAll<HTMLInputElement>('[data-slot="digit-input-cell"]')
|
|
163
|
+
expect(cells[0].id).toBe('otp')
|
|
164
|
+
expect(cells[0].name).toBe('code')
|
|
165
|
+
expect(cells[1].id).toBe('')
|
|
166
|
+
expect(cells[1].name).toBe('')
|
|
167
|
+
})
|
|
168
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
7
|
+
import { EmptyState } from '../empty-state'
|
|
8
|
+
|
|
9
|
+
describe('EmptyState primitive', () => {
|
|
10
|
+
it('renders the title', () => {
|
|
11
|
+
render(<EmptyState title="No results" />)
|
|
12
|
+
expect(screen.getByText('No results')).toBeInTheDocument()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('renders description when provided', () => {
|
|
16
|
+
render(<EmptyState title="Empty" description="Try adjusting filters" />)
|
|
17
|
+
expect(screen.getByText('Try adjusting filters')).toBeInTheDocument()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('renders an icon when provided', () => {
|
|
21
|
+
const Icon = () => <svg data-testid="empty-icon" />
|
|
22
|
+
render(<EmptyState title="None" icon={<Icon />} />)
|
|
23
|
+
expect(screen.getByTestId('empty-icon')).toBeInTheDocument()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('renders custom actions ReactNode when provided', () => {
|
|
27
|
+
render(
|
|
28
|
+
<EmptyState
|
|
29
|
+
title="Empty"
|
|
30
|
+
actions={<button type="button">Custom action</button>}
|
|
31
|
+
/>,
|
|
32
|
+
)
|
|
33
|
+
expect(screen.getByRole('button', { name: 'Custom action' })).toBeInTheDocument()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('renders the legacy action object as a default outline button', () => {
|
|
37
|
+
const onClick = jest.fn()
|
|
38
|
+
render(
|
|
39
|
+
<EmptyState
|
|
40
|
+
title="Empty"
|
|
41
|
+
action={{ label: 'Add item', onClick }}
|
|
42
|
+
/>,
|
|
43
|
+
)
|
|
44
|
+
const button = screen.getByRole('button', { name: /Add item/i })
|
|
45
|
+
expect(button).toBeInTheDocument()
|
|
46
|
+
fireEvent.click(button)
|
|
47
|
+
expect(onClick).toHaveBeenCalledTimes(1)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('renders legacy actionLabel + onAction as a button (BC for old backend EmptyState)', () => {
|
|
51
|
+
const onAction = jest.fn()
|
|
52
|
+
render(
|
|
53
|
+
<EmptyState title="Empty" actionLabel="Create" onAction={onAction} />,
|
|
54
|
+
)
|
|
55
|
+
const button = screen.getByRole('button', { name: /Create/i })
|
|
56
|
+
fireEvent.click(button)
|
|
57
|
+
expect(onAction).toHaveBeenCalledTimes(1)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('prefers actions over legacy action when both are provided', () => {
|
|
61
|
+
render(
|
|
62
|
+
<EmptyState
|
|
63
|
+
title="Empty"
|
|
64
|
+
actions={<button type="button">Primary</button>}
|
|
65
|
+
action={{ label: 'Legacy', onClick: () => {} }}
|
|
66
|
+
/>,
|
|
67
|
+
)
|
|
68
|
+
expect(screen.getByRole('button', { name: 'Primary' })).toBeInTheDocument()
|
|
69
|
+
expect(screen.queryByRole('button', { name: /Legacy/i })).not.toBeInTheDocument()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('default variant renders dashed border + bg-muted/30', () => {
|
|
73
|
+
const { container } = render(<EmptyState title="Default" />)
|
|
74
|
+
const root = container.querySelector('[data-slot="empty-state"]')
|
|
75
|
+
expect(root!.className).toContain('border-dashed')
|
|
76
|
+
expect(root!.className).toContain('bg-muted/30')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('subtle variant has no border and no muted background', () => {
|
|
80
|
+
const { container } = render(<EmptyState title="Subtle" variant="subtle" />)
|
|
81
|
+
const root = container.querySelector('[data-slot="empty-state"]')
|
|
82
|
+
expect(root!.className).not.toContain('border-dashed')
|
|
83
|
+
expect(root!.className).not.toContain('bg-muted/30')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('subtle variant wraps icon in a round muted box (Figma illustration tile style)', () => {
|
|
87
|
+
const Icon = () => <svg data-testid="boxed-icon" />
|
|
88
|
+
const { container } = render(
|
|
89
|
+
<EmptyState title="Subtle" variant="subtle" icon={<Icon />} />,
|
|
90
|
+
)
|
|
91
|
+
const iconWrapper = container.querySelector('[data-slot="empty-state"] > div')
|
|
92
|
+
expect(iconWrapper!.className).toContain('bg-muted')
|
|
93
|
+
expect(iconWrapper!.className).toContain('rounded-full')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('illustration prop renders without an icon-box wrapper (Figma illustrations bring own bg)', () => {
|
|
97
|
+
const Illustration = () => <svg data-testid="figma-illustration" width="120" height="120" />
|
|
98
|
+
const { container } = render(
|
|
99
|
+
<EmptyState
|
|
100
|
+
title="With illustration"
|
|
101
|
+
variant="subtle"
|
|
102
|
+
illustration={<Illustration />}
|
|
103
|
+
/>,
|
|
104
|
+
)
|
|
105
|
+
expect(container.querySelector('[data-testid="figma-illustration"]')).toBeInTheDocument()
|
|
106
|
+
const wrapper = container.querySelector('[data-slot="empty-state"] > div')
|
|
107
|
+
expect(wrapper!.className).not.toContain('bg-muted')
|
|
108
|
+
expect(wrapper!.className).not.toContain('rounded-full')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('illustration takes precedence over icon when both are provided', () => {
|
|
112
|
+
const Illustration = () => <svg data-testid="illustration" />
|
|
113
|
+
const Icon = () => <svg data-testid="icon" />
|
|
114
|
+
render(
|
|
115
|
+
<EmptyState
|
|
116
|
+
title="Both"
|
|
117
|
+
illustration={<Illustration />}
|
|
118
|
+
icon={<Icon />}
|
|
119
|
+
/>,
|
|
120
|
+
)
|
|
121
|
+
expect(screen.getByTestId('illustration')).toBeInTheDocument()
|
|
122
|
+
expect(screen.queryByTestId('icon')).not.toBeInTheDocument()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('size variants apply correct padding tokens', () => {
|
|
126
|
+
const { container: sm } = render(<EmptyState title="X" size="sm" />)
|
|
127
|
+
expect(sm.querySelector('[data-slot="empty-state"]')!.className).toContain('py-6')
|
|
128
|
+
|
|
129
|
+
const { container: dft } = render(<EmptyState title="X" />)
|
|
130
|
+
expect(dft.querySelector('[data-slot="empty-state"]')!.className).toContain('py-10')
|
|
131
|
+
|
|
132
|
+
const { container: lg } = render(<EmptyState title="X" size="lg" />)
|
|
133
|
+
expect(lg.querySelector('[data-slot="empty-state"]')!.className).toContain('py-16')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('forwards className to the root element', () => {
|
|
137
|
+
const { container } = render(
|
|
138
|
+
<EmptyState title="Empty" className="custom-class" />,
|
|
139
|
+
)
|
|
140
|
+
expect(container.querySelector('[data-slot="empty-state"]')!.className).toContain('custom-class')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('renders children between description and actions', () => {
|
|
144
|
+
render(
|
|
145
|
+
<EmptyState
|
|
146
|
+
title="Empty"
|
|
147
|
+
description="No items"
|
|
148
|
+
>
|
|
149
|
+
<p data-testid="custom-child">Extra content</p>
|
|
150
|
+
</EmptyState>,
|
|
151
|
+
)
|
|
152
|
+
expect(screen.getByTestId('custom-child')).toBeInTheDocument()
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
7
|
+
import { InlineInput } from '../inline-input'
|
|
8
|
+
|
|
9
|
+
describe('InlineInput primitive', () => {
|
|
10
|
+
it('renders an input element', () => {
|
|
11
|
+
render(<InlineInput placeholder="Edit value" />)
|
|
12
|
+
expect(screen.getByPlaceholderText('Edit value')).toBeInTheDocument()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('renders without a visible border at rest (border-transparent + bg-transparent)', () => {
|
|
16
|
+
const { container } = render(<InlineInput value="hello" onChange={() => {}} />)
|
|
17
|
+
const wrapper = container.querySelector('[data-slot="input-wrapper"]')
|
|
18
|
+
expect(wrapper).toHaveClass('border-transparent')
|
|
19
|
+
expect(wrapper).toHaveClass('bg-transparent')
|
|
20
|
+
expect(wrapper).toHaveClass('shadow-none')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('reveals a hover border by default', () => {
|
|
24
|
+
const { container } = render(<InlineInput value="x" onChange={() => {}} />)
|
|
25
|
+
const wrapper = container.querySelector('[data-slot="input-wrapper"]')
|
|
26
|
+
expect(wrapper).toHaveClass('hover:border-input')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('omits the hover border when showBorderOnHover is false', () => {
|
|
30
|
+
const { container } = render(
|
|
31
|
+
<InlineInput value="x" onChange={() => {}} showBorderOnHover={false} />,
|
|
32
|
+
)
|
|
33
|
+
const wrapper = container.querySelector('[data-slot="input-wrapper"]')
|
|
34
|
+
expect(wrapper).not.toHaveClass('hover:border-input')
|
|
35
|
+
expect(wrapper).toHaveClass('hover:bg-transparent')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('defaults to the sm size (h-8)', () => {
|
|
39
|
+
const { container } = render(<InlineInput value="x" onChange={() => {}} />)
|
|
40
|
+
const wrapper = container.querySelector('[data-slot="input-wrapper"]')
|
|
41
|
+
expect(wrapper).toHaveClass('h-8')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('supports size="default" (h-9)', () => {
|
|
45
|
+
const { container } = render(
|
|
46
|
+
<InlineInput value="x" onChange={() => {}} size="default" />,
|
|
47
|
+
)
|
|
48
|
+
const wrapper = container.querySelector('[data-slot="input-wrapper"]')
|
|
49
|
+
expect(wrapper).toHaveClass('h-9')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('forwards value / onChange like a regular Input', () => {
|
|
53
|
+
const onChange = jest.fn()
|
|
54
|
+
render(<InlineInput value="hello" onChange={onChange} />)
|
|
55
|
+
const input = screen.getByDisplayValue('hello') as HTMLInputElement
|
|
56
|
+
fireEvent.change(input, { target: { value: 'world' } })
|
|
57
|
+
expect(onChange).toHaveBeenCalled()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('forwards onBlur for save-on-blur consumer flows', () => {
|
|
61
|
+
const onBlur = jest.fn()
|
|
62
|
+
render(<InlineInput value="hello" onChange={() => {}} onBlur={onBlur} />)
|
|
63
|
+
const input = screen.getByDisplayValue('hello')
|
|
64
|
+
fireEvent.blur(input)
|
|
65
|
+
expect(onBlur).toHaveBeenCalled()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('forwards refs to the underlying input element', () => {
|
|
69
|
+
const ref = React.createRef<HTMLInputElement>()
|
|
70
|
+
render(<InlineInput ref={ref} value="hello" onChange={() => {}} />)
|
|
71
|
+
expect(ref.current).not.toBeNull()
|
|
72
|
+
expect(ref.current?.tagName).toBe('INPUT')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('merges consumer className alongside the borderless baseline', () => {
|
|
76
|
+
const { container } = render(
|
|
77
|
+
<InlineInput value="x" onChange={() => {}} className="text-right font-mono" />,
|
|
78
|
+
)
|
|
79
|
+
const wrapper = container.querySelector('[data-slot="input-wrapper"]')
|
|
80
|
+
expect(wrapper).toHaveClass('text-right')
|
|
81
|
+
expect(wrapper).toHaveClass('font-mono')
|
|
82
|
+
expect(wrapper).toHaveClass('border-transparent')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('renders aria-invalid via the underlying Input wrapper', () => {
|
|
86
|
+
const { container } = render(
|
|
87
|
+
<InlineInput value="x" onChange={() => {}} aria-invalid />,
|
|
88
|
+
)
|
|
89
|
+
const input = container.querySelector('input')
|
|
90
|
+
expect(input).toHaveAttribute('aria-invalid', 'true')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('honors a custom input type', () => {
|
|
94
|
+
const { container } = render(
|
|
95
|
+
<InlineInput value="user@example.com" onChange={() => {}} type="email" />,
|
|
96
|
+
)
|
|
97
|
+
const input = container.querySelector('input')
|
|
98
|
+
expect(input).toHaveAttribute('type', 'email')
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import { render, screen } from '@testing-library/react'
|
|
7
|
+
import {
|
|
8
|
+
InlineSelectTrigger,
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectValue,
|
|
13
|
+
} from '../inline-select'
|
|
14
|
+
|
|
15
|
+
function renderInlineSelect(
|
|
16
|
+
props: Partial<React.ComponentProps<typeof InlineSelectTrigger>> = {},
|
|
17
|
+
) {
|
|
18
|
+
return render(
|
|
19
|
+
<Select defaultValue="open">
|
|
20
|
+
<InlineSelectTrigger aria-label="Status" {...props}>
|
|
21
|
+
<SelectValue />
|
|
22
|
+
</InlineSelectTrigger>
|
|
23
|
+
<SelectContent>
|
|
24
|
+
<SelectItem value="open">Open</SelectItem>
|
|
25
|
+
<SelectItem value="closed">Closed</SelectItem>
|
|
26
|
+
</SelectContent>
|
|
27
|
+
</Select>,
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('InlineSelect primitive', () => {
|
|
32
|
+
it('renders the trigger borderless at rest', () => {
|
|
33
|
+
const { container } = renderInlineSelect()
|
|
34
|
+
const trigger = container.querySelector('[data-slot="inline-select-trigger"]')
|
|
35
|
+
expect(trigger).not.toBeNull()
|
|
36
|
+
expect(trigger).toHaveClass('border-transparent')
|
|
37
|
+
expect(trigger).toHaveClass('bg-transparent')
|
|
38
|
+
expect(trigger).toHaveClass('shadow-none')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('shows the hover border by default', () => {
|
|
42
|
+
const { container } = renderInlineSelect()
|
|
43
|
+
const trigger = container.querySelector('[data-slot="inline-select-trigger"]')
|
|
44
|
+
expect(trigger).toHaveClass('hover:border-input')
|
|
45
|
+
expect(trigger).toHaveClass('hover:bg-muted/40')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('omits the hover border when showBorderOnHover is false', () => {
|
|
49
|
+
const { container } = renderInlineSelect({ showBorderOnHover: false })
|
|
50
|
+
const trigger = container.querySelector('[data-slot="inline-select-trigger"]')
|
|
51
|
+
expect(trigger).not.toHaveClass('hover:border-input')
|
|
52
|
+
expect(trigger).toHaveClass('hover:bg-transparent')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('defaults to the sm size (h-8)', () => {
|
|
56
|
+
const { container } = renderInlineSelect()
|
|
57
|
+
const trigger = container.querySelector('[data-slot="inline-select-trigger"]')
|
|
58
|
+
expect(trigger).toHaveClass('h-8')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('supports size="default" (h-9)', () => {
|
|
62
|
+
const { container } = renderInlineSelect({ size: 'default' })
|
|
63
|
+
const trigger = container.querySelector('[data-slot="inline-select-trigger"]')
|
|
64
|
+
expect(trigger).toHaveClass('h-9')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('forwards refs to the underlying button trigger', () => {
|
|
68
|
+
const ref = React.createRef<HTMLButtonElement>()
|
|
69
|
+
render(
|
|
70
|
+
<Select defaultValue="a">
|
|
71
|
+
<InlineSelectTrigger ref={ref} aria-label="Pick">
|
|
72
|
+
<SelectValue />
|
|
73
|
+
</InlineSelectTrigger>
|
|
74
|
+
<SelectContent>
|
|
75
|
+
<SelectItem value="a">A</SelectItem>
|
|
76
|
+
</SelectContent>
|
|
77
|
+
</Select>,
|
|
78
|
+
)
|
|
79
|
+
expect(ref.current).not.toBeNull()
|
|
80
|
+
expect(ref.current?.tagName).toBe('BUTTON')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('merges consumer className alongside the borderless baseline', () => {
|
|
84
|
+
const { container } = renderInlineSelect({
|
|
85
|
+
className: 'text-right font-mono',
|
|
86
|
+
})
|
|
87
|
+
const trigger = container.querySelector('[data-slot="inline-select-trigger"]')
|
|
88
|
+
expect(trigger).toHaveClass('text-right')
|
|
89
|
+
expect(trigger).toHaveClass('font-mono')
|
|
90
|
+
expect(trigger).toHaveClass('border-transparent')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('forwards aria-label to the Radix trigger', () => {
|
|
94
|
+
renderInlineSelect({ 'aria-label': 'Pipeline stage' })
|
|
95
|
+
expect(screen.getByRole('combobox', { name: 'Pipeline stage' })).toBeInTheDocument()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('renders aria-invalid styling when aria-invalid is set', () => {
|
|
99
|
+
const { container } = renderInlineSelect({ 'aria-invalid': true })
|
|
100
|
+
const trigger = container.querySelector('[aria-invalid="true"]')
|
|
101
|
+
expect(trigger).not.toBeNull()
|
|
102
|
+
})
|
|
103
|
+
})
|