@opensaas/stack-ui 0.1.6 → 0.3.0
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 +3 -2
- package/CHANGELOG.md +11 -0
- package/dist/components/AdminUI.d.ts +2 -1
- package/dist/components/AdminUI.d.ts.map +1 -1
- package/dist/components/AdminUI.js +2 -2
- package/dist/components/ItemFormClient.d.ts.map +1 -1
- package/dist/components/ItemFormClient.js +78 -60
- package/dist/components/Navigation.d.ts +2 -1
- package/dist/components/Navigation.d.ts.map +1 -1
- package/dist/components/Navigation.js +3 -2
- package/dist/components/UserMenu.d.ts +11 -0
- package/dist/components/UserMenu.d.ts.map +1 -0
- package/dist/components/UserMenu.js +18 -0
- package/dist/components/fields/TextField.d.ts +2 -1
- package/dist/components/fields/TextField.d.ts.map +1 -1
- package/dist/components/fields/TextField.js +4 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/primitives/button.d.ts +1 -1
- package/dist/styles/globals.css +25 -1
- package/package.json +14 -5
- package/src/components/AdminUI.tsx +3 -0
- package/src/components/ItemFormClient.tsx +84 -62
- package/src/components/Navigation.tsx +9 -20
- package/src/components/UserMenu.tsx +44 -0
- package/src/components/fields/TextField.tsx +7 -2
- package/src/index.ts +2 -0
- package/tests/browser/README.md +154 -0
- package/tests/browser/fields/CheckboxField.browser.test.tsx +245 -0
- package/tests/browser/fields/SelectField.browser.test.tsx +263 -0
- package/tests/browser/fields/TextField.browser.test.tsx +204 -0
- package/tests/browser/fields/__screenshots__/CheckboxField.browser.test.tsx/CheckboxField--Browser--edit-mode-should-not-be-clickable-when-disabled-1.png +0 -0
- package/tests/browser/fields/__screenshots__/CheckboxField.browser.test.tsx/CheckboxField--Browser--edit-mode-should-toggle-state-with-multiple-clicks-1.png +0 -0
- package/tests/browser/fields/__screenshots__/TextField.browser.test.tsx/TextField--Browser--edit-mode-should-handle-special-characters-1.png +0 -0
- package/tests/browser/fields/__screenshots__/TextField.browser.test.tsx/TextField--Browser--edit-mode-should-support-copy-and-paste-1.png +0 -0
- package/tests/browser/primitives/Button.browser.test.tsx +122 -0
- package/tests/browser/primitives/Dialog.browser.test.tsx +279 -0
- package/tests/browser/primitives/__screenshots__/Button.browser.test.tsx/Button--Browser--should-not-trigger-click-when-disabled-1.png +0 -0
- package/tests/components/CheckboxField.test.tsx +130 -0
- package/tests/components/DeleteButton.test.tsx +331 -0
- package/tests/components/IntegerField.test.tsx +147 -0
- package/tests/components/ListTable.test.tsx +457 -0
- package/tests/components/ListViewClient.test.tsx +415 -0
- package/tests/components/SearchBar.test.tsx +254 -0
- package/tests/components/SelectField.test.tsx +192 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen, act } from '@testing-library/react'
|
|
3
|
+
import { userEvent } from 'vitest/browser'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { CheckboxField } from '../../../src/components/fields/CheckboxField.js'
|
|
6
|
+
|
|
7
|
+
describe('CheckboxField (Browser)', () => {
|
|
8
|
+
describe('edit mode', () => {
|
|
9
|
+
it('should render checkbox with label', async () => {
|
|
10
|
+
render(<CheckboxField name="active" value={false} onChange={() => {}} label="Is Active" />)
|
|
11
|
+
|
|
12
|
+
expect(screen.getByLabelText('Is Active')).toBeInTheDocument()
|
|
13
|
+
expect(screen.getByRole('checkbox')).toBeInTheDocument()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should display checked state when value is true', async () => {
|
|
17
|
+
render(<CheckboxField name="active" value={true} onChange={() => {}} label="Is Active" />)
|
|
18
|
+
|
|
19
|
+
const checkbox = screen.getByRole('checkbox')
|
|
20
|
+
expect(checkbox).toBeChecked()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should display unchecked state when value is false', async () => {
|
|
24
|
+
render(<CheckboxField name="active" value={false} onChange={() => {}} label="Is Active" />)
|
|
25
|
+
|
|
26
|
+
const checkbox = screen.getByRole('checkbox')
|
|
27
|
+
expect(checkbox).not.toBeChecked()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should call onChange with true when clicked', async () => {
|
|
31
|
+
let currentValue = false
|
|
32
|
+
const handleChange = (value: boolean) => {
|
|
33
|
+
currentValue = value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
render(
|
|
37
|
+
<CheckboxField
|
|
38
|
+
name="active"
|
|
39
|
+
value={currentValue}
|
|
40
|
+
onChange={handleChange}
|
|
41
|
+
label="Is Active"
|
|
42
|
+
/>,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const checkbox = screen.getByRole('checkbox')
|
|
46
|
+
await userEvent.click(checkbox)
|
|
47
|
+
|
|
48
|
+
expect(currentValue).toBe(true)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should call onChange with false when unchecked', async () => {
|
|
52
|
+
let currentValue = true
|
|
53
|
+
const handleChange = (value: boolean) => {
|
|
54
|
+
currentValue = value
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
render(
|
|
58
|
+
<CheckboxField
|
|
59
|
+
name="active"
|
|
60
|
+
value={currentValue}
|
|
61
|
+
onChange={handleChange}
|
|
62
|
+
label="Is Active"
|
|
63
|
+
/>,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const checkbox = screen.getByRole('checkbox')
|
|
67
|
+
await userEvent.click(checkbox)
|
|
68
|
+
|
|
69
|
+
expect(currentValue).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should toggle state with multiple clicks', async () => {
|
|
73
|
+
function TestComponent() {
|
|
74
|
+
const [value, setValue] = React.useState(false)
|
|
75
|
+
return (
|
|
76
|
+
<CheckboxField
|
|
77
|
+
name="active"
|
|
78
|
+
value={value}
|
|
79
|
+
onChange={setValue}
|
|
80
|
+
label="Is Active"
|
|
81
|
+
data-testid="checkbox-field"
|
|
82
|
+
/>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await act(async () => {
|
|
87
|
+
render(<TestComponent />)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const checkbox = screen.getByRole('checkbox')
|
|
91
|
+
|
|
92
|
+
// First click - check
|
|
93
|
+
expect(checkbox).not.toBeChecked()
|
|
94
|
+
await act(async () => {
|
|
95
|
+
await userEvent.click(checkbox)
|
|
96
|
+
})
|
|
97
|
+
expect(checkbox).toBeChecked()
|
|
98
|
+
|
|
99
|
+
// Second click - uncheck
|
|
100
|
+
await act(async () => {
|
|
101
|
+
await userEvent.click(checkbox)
|
|
102
|
+
})
|
|
103
|
+
expect(checkbox).not.toBeChecked()
|
|
104
|
+
|
|
105
|
+
// Third click - check again
|
|
106
|
+
await act(async () => {
|
|
107
|
+
await userEvent.click(checkbox)
|
|
108
|
+
})
|
|
109
|
+
expect(checkbox).toBeChecked()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should display error message', async () => {
|
|
113
|
+
render(
|
|
114
|
+
<CheckboxField
|
|
115
|
+
name="terms"
|
|
116
|
+
value={false}
|
|
117
|
+
onChange={() => {}}
|
|
118
|
+
label="Accept Terms"
|
|
119
|
+
error="You must accept the terms"
|
|
120
|
+
/>,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
expect(screen.getByText('You must accept the terms')).toBeInTheDocument()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should be disabled when disabled prop is true', async () => {
|
|
127
|
+
render(
|
|
128
|
+
<CheckboxField
|
|
129
|
+
name="active"
|
|
130
|
+
value={false}
|
|
131
|
+
onChange={() => {}}
|
|
132
|
+
label="Is Active"
|
|
133
|
+
disabled
|
|
134
|
+
/>,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const checkbox = screen.getByRole('checkbox')
|
|
138
|
+
expect(checkbox).toBeDisabled()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should not be clickable when disabled', async () => {
|
|
142
|
+
let currentValue = false
|
|
143
|
+
const handleChange = (value: boolean) => {
|
|
144
|
+
currentValue = value
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
render(
|
|
148
|
+
<CheckboxField
|
|
149
|
+
name="active"
|
|
150
|
+
value={currentValue}
|
|
151
|
+
onChange={handleChange}
|
|
152
|
+
label="Is Active"
|
|
153
|
+
disabled
|
|
154
|
+
/>,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
const checkbox = screen.getByRole('checkbox')
|
|
158
|
+
|
|
159
|
+
// Verify checkbox is disabled - browsers prevent clicking disabled checkboxes
|
|
160
|
+
expect(checkbox).toBeDisabled()
|
|
161
|
+
|
|
162
|
+
// In real browsers, disabled checkboxes cannot be clicked
|
|
163
|
+
expect(currentValue).toBe(false)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should support keyboard interaction with Space', async () => {
|
|
167
|
+
let currentValue = false
|
|
168
|
+
const handleChange = (value: boolean) => {
|
|
169
|
+
currentValue = value
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
render(
|
|
173
|
+
<CheckboxField
|
|
174
|
+
name="active"
|
|
175
|
+
value={currentValue}
|
|
176
|
+
onChange={handleChange}
|
|
177
|
+
label="Is Active"
|
|
178
|
+
/>,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
const checkbox = screen.getByRole('checkbox')
|
|
182
|
+
checkbox.focus()
|
|
183
|
+
expect(document.activeElement).toBe(checkbox)
|
|
184
|
+
|
|
185
|
+
await userEvent.keyboard(' ')
|
|
186
|
+
|
|
187
|
+
expect(currentValue).toBe(true)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should support clicking on label to toggle', async () => {
|
|
191
|
+
let currentValue = false
|
|
192
|
+
const handleChange = (value: boolean) => {
|
|
193
|
+
currentValue = value
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
render(
|
|
197
|
+
<CheckboxField
|
|
198
|
+
name="active"
|
|
199
|
+
value={currentValue}
|
|
200
|
+
onChange={handleChange}
|
|
201
|
+
label="Is Active"
|
|
202
|
+
/>,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
const label = screen.getByText('Is Active')
|
|
206
|
+
await userEvent.click(label)
|
|
207
|
+
|
|
208
|
+
expect(currentValue).toBe(true)
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe('read mode', () => {
|
|
213
|
+
it('should render "Yes" when value is true', async () => {
|
|
214
|
+
render(
|
|
215
|
+
<CheckboxField
|
|
216
|
+
name="active"
|
|
217
|
+
value={true}
|
|
218
|
+
onChange={() => {}}
|
|
219
|
+
label="Is Active"
|
|
220
|
+
mode="read"
|
|
221
|
+
/>,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
expect(screen.getByText('Is Active')).toBeInTheDocument()
|
|
225
|
+
expect(screen.getByText('Yes')).toBeInTheDocument()
|
|
226
|
+
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('should render "No" when value is false', async () => {
|
|
230
|
+
render(
|
|
231
|
+
<CheckboxField
|
|
232
|
+
name="active"
|
|
233
|
+
value={false}
|
|
234
|
+
onChange={() => {}}
|
|
235
|
+
label="Is Active"
|
|
236
|
+
mode="read"
|
|
237
|
+
/>,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
expect(screen.getByText('Is Active')).toBeInTheDocument()
|
|
241
|
+
expect(screen.getByText('No')).toBeInTheDocument()
|
|
242
|
+
expect(screen.queryByRole('checkbox')).not.toBeInTheDocument()
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
})
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen, waitFor, act } from '@testing-library/react'
|
|
3
|
+
import { userEvent } from 'vitest/browser'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { SelectField } from '../../../src/components/fields/SelectField.js'
|
|
6
|
+
|
|
7
|
+
const mockOptions = [
|
|
8
|
+
{ label: 'Option 1', value: 'option1' },
|
|
9
|
+
{ label: 'Option 2', value: 'option2' },
|
|
10
|
+
{ label: 'Option 3', value: 'option3' },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
describe('SelectField (Browser)', () => {
|
|
14
|
+
describe('edit mode', () => {
|
|
15
|
+
it('should render select field with label', async () => {
|
|
16
|
+
await act(async () => {
|
|
17
|
+
render(
|
|
18
|
+
<SelectField
|
|
19
|
+
name="status"
|
|
20
|
+
value={null}
|
|
21
|
+
onChange={() => {}}
|
|
22
|
+
label="Status"
|
|
23
|
+
options={mockOptions}
|
|
24
|
+
/>,
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
expect(screen.getByText('Status')).toBeInTheDocument()
|
|
29
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should display selected value', async () => {
|
|
33
|
+
await act(async () => {
|
|
34
|
+
render(
|
|
35
|
+
<SelectField
|
|
36
|
+
name="status"
|
|
37
|
+
value="option2"
|
|
38
|
+
onChange={() => {}}
|
|
39
|
+
label="Status"
|
|
40
|
+
options={mockOptions}
|
|
41
|
+
/>,
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
expect(screen.getByRole('combobox')).toHaveTextContent('Option 2')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should open dropdown when clicked', async () => {
|
|
49
|
+
await act(async () => {
|
|
50
|
+
render(
|
|
51
|
+
<SelectField
|
|
52
|
+
name="status"
|
|
53
|
+
value={null}
|
|
54
|
+
onChange={() => {}}
|
|
55
|
+
label="Status"
|
|
56
|
+
options={mockOptions}
|
|
57
|
+
/>,
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const trigger = screen.getByRole('combobox')
|
|
62
|
+
|
|
63
|
+
await act(async () => {
|
|
64
|
+
await userEvent.click(trigger)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
await waitFor(() => {
|
|
68
|
+
expect(screen.getByRole('option', { name: 'Option 1' })).toBeInTheDocument()
|
|
69
|
+
expect(screen.getByRole('option', { name: 'Option 2' })).toBeInTheDocument()
|
|
70
|
+
expect(screen.getByRole('option', { name: 'Option 3' })).toBeInTheDocument()
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should call onChange when option is selected', async () => {
|
|
75
|
+
let selectedValue: string | null = null
|
|
76
|
+
const handleChange = (value: string | null) => {
|
|
77
|
+
selectedValue = value
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await act(async () => {
|
|
81
|
+
render(
|
|
82
|
+
<SelectField
|
|
83
|
+
name="status"
|
|
84
|
+
value={null}
|
|
85
|
+
onChange={handleChange}
|
|
86
|
+
label="Status"
|
|
87
|
+
options={mockOptions}
|
|
88
|
+
/>,
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const trigger = screen.getByRole('combobox')
|
|
93
|
+
|
|
94
|
+
await act(async () => {
|
|
95
|
+
await userEvent.click(trigger)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
await waitFor(() => {
|
|
99
|
+
expect(screen.getByRole('option', { name: 'Option 2' })).toBeInTheDocument()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const option = screen.getByRole('option', { name: 'Option 2' })
|
|
103
|
+
|
|
104
|
+
await act(async () => {
|
|
105
|
+
await userEvent.click(option)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
expect(selectedValue).toBe('option2')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should show required indicator when required', async () => {
|
|
112
|
+
await act(async () => {
|
|
113
|
+
render(
|
|
114
|
+
<SelectField
|
|
115
|
+
name="status"
|
|
116
|
+
value={null}
|
|
117
|
+
onChange={() => {}}
|
|
118
|
+
label="Status"
|
|
119
|
+
options={mockOptions}
|
|
120
|
+
required
|
|
121
|
+
/>,
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
expect(screen.getByText('*')).toBeInTheDocument()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should display error message', async () => {
|
|
129
|
+
await act(async () => {
|
|
130
|
+
render(
|
|
131
|
+
<SelectField
|
|
132
|
+
name="status"
|
|
133
|
+
value={null}
|
|
134
|
+
onChange={() => {}}
|
|
135
|
+
label="Status"
|
|
136
|
+
options={mockOptions}
|
|
137
|
+
error="Status is required"
|
|
138
|
+
/>,
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
expect(screen.getByText('Status is required')).toBeInTheDocument()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should be disabled when disabled prop is true', async () => {
|
|
146
|
+
await act(async () => {
|
|
147
|
+
render(
|
|
148
|
+
<SelectField
|
|
149
|
+
name="status"
|
|
150
|
+
value={null}
|
|
151
|
+
onChange={() => {}}
|
|
152
|
+
label="Status"
|
|
153
|
+
options={mockOptions}
|
|
154
|
+
disabled
|
|
155
|
+
/>,
|
|
156
|
+
)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const trigger = screen.getByRole('combobox')
|
|
160
|
+
expect(trigger).toBeDisabled()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('should support keyboard navigation', async () => {
|
|
164
|
+
await act(async () => {
|
|
165
|
+
render(
|
|
166
|
+
<SelectField
|
|
167
|
+
name="status"
|
|
168
|
+
value={null}
|
|
169
|
+
onChange={() => {}}
|
|
170
|
+
label="Status"
|
|
171
|
+
options={mockOptions}
|
|
172
|
+
/>,
|
|
173
|
+
)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const trigger = screen.getByRole('combobox')
|
|
177
|
+
trigger.focus()
|
|
178
|
+
expect(document.activeElement).toBe(trigger)
|
|
179
|
+
|
|
180
|
+
// Open with Space key (more reliable than Enter for Radix Select)
|
|
181
|
+
await act(async () => {
|
|
182
|
+
await userEvent.keyboard(' ')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
await waitFor(
|
|
186
|
+
() => {
|
|
187
|
+
expect(screen.getByRole('option', { name: 'Option 1' })).toBeInTheDocument()
|
|
188
|
+
},
|
|
189
|
+
{ timeout: 3000 },
|
|
190
|
+
)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should close dropdown when Escape is pressed', async () => {
|
|
194
|
+
await act(async () => {
|
|
195
|
+
render(
|
|
196
|
+
<SelectField
|
|
197
|
+
name="status"
|
|
198
|
+
value={null}
|
|
199
|
+
onChange={() => {}}
|
|
200
|
+
label="Status"
|
|
201
|
+
options={mockOptions}
|
|
202
|
+
/>,
|
|
203
|
+
)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const trigger = screen.getByRole('combobox')
|
|
207
|
+
|
|
208
|
+
await act(async () => {
|
|
209
|
+
await userEvent.click(trigger)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
await waitFor(() => {
|
|
213
|
+
expect(screen.getByRole('option', { name: 'Option 1' })).toBeInTheDocument()
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
await act(async () => {
|
|
217
|
+
await userEvent.keyboard('{Escape}')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
await waitFor(() => {
|
|
221
|
+
expect(screen.queryByRole('option', { name: 'Option 1' })).not.toBeInTheDocument()
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
describe('read mode', () => {
|
|
227
|
+
it('should render selected option label', async () => {
|
|
228
|
+
await act(async () => {
|
|
229
|
+
render(
|
|
230
|
+
<SelectField
|
|
231
|
+
name="status"
|
|
232
|
+
value="option2"
|
|
233
|
+
onChange={() => {}}
|
|
234
|
+
label="Status"
|
|
235
|
+
options={mockOptions}
|
|
236
|
+
mode="read"
|
|
237
|
+
/>,
|
|
238
|
+
)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
expect(screen.getByText('Status')).toBeInTheDocument()
|
|
242
|
+
expect(screen.getByText('Option 2')).toBeInTheDocument()
|
|
243
|
+
expect(screen.queryByRole('combobox')).not.toBeInTheDocument()
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('should show dash when value is null', async () => {
|
|
247
|
+
await act(async () => {
|
|
248
|
+
render(
|
|
249
|
+
<SelectField
|
|
250
|
+
name="status"
|
|
251
|
+
value={null}
|
|
252
|
+
onChange={() => {}}
|
|
253
|
+
label="Status"
|
|
254
|
+
options={mockOptions}
|
|
255
|
+
mode="read"
|
|
256
|
+
/>,
|
|
257
|
+
)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
expect(screen.getByText('-')).toBeInTheDocument()
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
})
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen, waitFor, act } from '@testing-library/react'
|
|
3
|
+
import { userEvent } from 'vitest/browser'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { TextField } from '../../../src/components/fields/TextField.js'
|
|
6
|
+
|
|
7
|
+
describe('TextField (Browser)', () => {
|
|
8
|
+
describe('edit mode', () => {
|
|
9
|
+
it('should render text input with label', async () => {
|
|
10
|
+
render(<TextField name="username" value="" onChange={() => {}} label="Username" />)
|
|
11
|
+
|
|
12
|
+
expect(screen.getByLabelText('Username')).toBeInTheDocument()
|
|
13
|
+
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should display current value', async () => {
|
|
17
|
+
render(<TextField name="username" value="john" onChange={() => {}} label="Username" />)
|
|
18
|
+
|
|
19
|
+
const input = screen.getByRole('textbox')
|
|
20
|
+
expect(input).toHaveValue('john')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should call onChange when user types', async () => {
|
|
24
|
+
let currentValue = ''
|
|
25
|
+
const handleChange = (value: string) => {
|
|
26
|
+
currentValue = value
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
render(<TextField name="username" value="" onChange={handleChange} label="Username" />)
|
|
30
|
+
|
|
31
|
+
const input = screen.getByRole('textbox')
|
|
32
|
+
await userEvent.type(input, 'test')
|
|
33
|
+
|
|
34
|
+
// Check that onChange was called and value updated
|
|
35
|
+
expect(currentValue).toBeTruthy()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should handle clearing input', async () => {
|
|
39
|
+
let currentValue = 'initial'
|
|
40
|
+
const handleChange = (value: string) => {
|
|
41
|
+
currentValue = value
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
render(
|
|
45
|
+
<TextField name="username" value={currentValue} onChange={handleChange} label="Username" />,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const input = screen.getByRole('textbox')
|
|
49
|
+
await userEvent.clear(input)
|
|
50
|
+
|
|
51
|
+
expect(currentValue).toBe('')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should show required indicator when required', async () => {
|
|
55
|
+
render(<TextField name="username" value="" onChange={() => {}} label="Username" required />)
|
|
56
|
+
|
|
57
|
+
expect(screen.getByText('*')).toBeInTheDocument()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should display error message', async () => {
|
|
61
|
+
render(
|
|
62
|
+
<TextField
|
|
63
|
+
name="username"
|
|
64
|
+
value=""
|
|
65
|
+
onChange={() => {}}
|
|
66
|
+
label="Username"
|
|
67
|
+
error="Username is required"
|
|
68
|
+
/>,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
expect(screen.getByText('Username is required')).toBeInTheDocument()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should be disabled when disabled prop is true', async () => {
|
|
75
|
+
render(<TextField name="username" value="" onChange={() => {}} label="Username" disabled />)
|
|
76
|
+
|
|
77
|
+
const input = screen.getByRole('textbox')
|
|
78
|
+
expect(input).toBeDisabled()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should not accept input when disabled', async () => {
|
|
82
|
+
let currentValue = ''
|
|
83
|
+
const handleChange = (value: string) => {
|
|
84
|
+
currentValue = value
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
render(
|
|
88
|
+
<TextField
|
|
89
|
+
name="username"
|
|
90
|
+
value={currentValue}
|
|
91
|
+
onChange={handleChange}
|
|
92
|
+
label="Username"
|
|
93
|
+
disabled
|
|
94
|
+
/>,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const input = screen.getByRole('textbox')
|
|
98
|
+
await userEvent.type(input, 'test')
|
|
99
|
+
|
|
100
|
+
expect(currentValue).toBe('')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should show placeholder text', async () => {
|
|
104
|
+
render(
|
|
105
|
+
<TextField
|
|
106
|
+
name="username"
|
|
107
|
+
value=""
|
|
108
|
+
onChange={() => {}}
|
|
109
|
+
label="Username"
|
|
110
|
+
placeholder="Enter your username"
|
|
111
|
+
/>,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
expect(screen.getByPlaceholderText('Enter your username')).toBeInTheDocument()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should handle programmatic value changes', async () => {
|
|
118
|
+
function TestComponent() {
|
|
119
|
+
const [value, setValue] = React.useState('')
|
|
120
|
+
return (
|
|
121
|
+
<div>
|
|
122
|
+
<button onClick={() => setValue('programmatic value')}>Set Value</button>
|
|
123
|
+
<TextField name="username" value={value} onChange={setValue} label="Username" />
|
|
124
|
+
</div>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await act(async () => {
|
|
129
|
+
render(<TestComponent />)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const input = screen.getByRole('textbox')
|
|
133
|
+
const button = screen.getByRole('button')
|
|
134
|
+
|
|
135
|
+
// Click button to set value programmatically
|
|
136
|
+
await act(async () => {
|
|
137
|
+
await userEvent.click(button)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
await waitFor(() => {
|
|
141
|
+
expect(input).toHaveValue('programmatic value')
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should handle focus and blur events', async () => {
|
|
146
|
+
render(<TextField name="username" value="" onChange={() => {}} label="Username" />)
|
|
147
|
+
|
|
148
|
+
const input = screen.getByRole('textbox')
|
|
149
|
+
|
|
150
|
+
// Focus
|
|
151
|
+
input.focus()
|
|
152
|
+
expect(document.activeElement).toBe(input)
|
|
153
|
+
|
|
154
|
+
// Blur
|
|
155
|
+
input.blur()
|
|
156
|
+
expect(document.activeElement).not.toBe(input)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should handle text input with keyboard', async () => {
|
|
160
|
+
function TestComponent() {
|
|
161
|
+
const [value, setValue] = React.useState('')
|
|
162
|
+
return <TextField name="username" value={value} onChange={setValue} label="Username" />
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await act(async () => {
|
|
166
|
+
render(<TestComponent />)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const input = screen.getByRole('textbox')
|
|
170
|
+
|
|
171
|
+
await act(async () => {
|
|
172
|
+
await userEvent.click(input)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
await act(async () => {
|
|
176
|
+
await userEvent.keyboard('test123')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
await waitFor(() => {
|
|
180
|
+
expect(input).toHaveValue('test123')
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('read mode', () => {
|
|
186
|
+
it('should render value as text', async () => {
|
|
187
|
+
render(
|
|
188
|
+
<TextField name="username" value="john" onChange={() => {}} label="Username" mode="read" />,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
expect(screen.getByText('Username')).toBeInTheDocument()
|
|
192
|
+
expect(screen.getByText('john')).toBeInTheDocument()
|
|
193
|
+
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should show dash when value is empty', async () => {
|
|
197
|
+
render(
|
|
198
|
+
<TextField name="username" value="" onChange={() => {}} label="Username" mode="read" />,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
expect(screen.getByText('-')).toBeInTheDocument()
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
})
|
|
Binary file
|