@opensaas/stack-ui 0.1.7 → 0.4.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 +140 -0
- package/dist/components/AdminUI.d.ts +5 -4
- package/dist/components/AdminUI.d.ts.map +1 -1
- package/dist/components/AdminUI.js +2 -2
- package/dist/components/Dashboard.d.ts +4 -4
- package/dist/components/Dashboard.d.ts.map +1 -1
- package/dist/components/Dashboard.js +4 -3
- package/dist/components/ItemForm.d.ts +4 -4
- package/dist/components/ItemForm.d.ts.map +1 -1
- package/dist/components/ItemForm.js +9 -5
- package/dist/components/ItemFormClient.d.ts.map +1 -1
- package/dist/components/ItemFormClient.js +78 -60
- package/dist/components/ListView.d.ts +4 -4
- package/dist/components/ListView.d.ts.map +1 -1
- package/dist/components/ListView.js +18 -11
- package/dist/components/Navigation.d.ts +5 -4
- 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/primitives/button.d.ts.map +1 -1
- package/dist/styles/globals.css +24 -0
- package/package.json +32 -23
- package/src/components/AdminUI.tsx +8 -5
- package/src/components/Dashboard.tsx +7 -10
- package/src/components/ItemForm.tsx +14 -10
- package/src/components/ItemFormClient.tsx +84 -62
- package/src/components/ListView.tsx +23 -21
- package/src/components/Navigation.tsx +14 -25
- package/src/components/UserMenu.tsx +44 -0
- package/src/components/fields/TextField.tsx +7 -2
- package/src/index.ts +2 -0
- package/src/primitives/button.tsx +1 -2
- 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,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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { userEvent } from 'vitest/browser'
|
|
4
|
+
import { Button } from '../../../src/primitives/button.js'
|
|
5
|
+
|
|
6
|
+
describe('Button (Browser)', () => {
|
|
7
|
+
it('should render with default variant', async () => {
|
|
8
|
+
render(<Button>Click me</Button>)
|
|
9
|
+
|
|
10
|
+
const button = screen.getByRole('button', { name: 'Click me' })
|
|
11
|
+
expect(button).toBeInTheDocument()
|
|
12
|
+
expect(button).toHaveClass('bg-primary')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should handle click events', async () => {
|
|
16
|
+
let clicked = false
|
|
17
|
+
const handleClick = () => {
|
|
18
|
+
clicked = true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
render(<Button onClick={handleClick}>Click me</Button>)
|
|
22
|
+
|
|
23
|
+
const button = screen.getByRole('button', { name: 'Click me' })
|
|
24
|
+
await userEvent.click(button)
|
|
25
|
+
|
|
26
|
+
expect(clicked).toBe(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should render different variants correctly', async () => {
|
|
30
|
+
const { rerender } = render(<Button variant="destructive">Delete</Button>)
|
|
31
|
+
let button = screen.getByRole('button', { name: 'Delete' })
|
|
32
|
+
expect(button).toHaveClass('bg-destructive')
|
|
33
|
+
|
|
34
|
+
rerender(<Button variant="outline">Cancel</Button>)
|
|
35
|
+
button = screen.getByRole('button', { name: 'Cancel' })
|
|
36
|
+
expect(button).toHaveClass('border')
|
|
37
|
+
|
|
38
|
+
rerender(<Button variant="ghost">Ghost</Button>)
|
|
39
|
+
button = screen.getByRole('button', { name: 'Ghost' })
|
|
40
|
+
expect(button).toHaveClass('hover:bg-accent')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should render different sizes correctly', async () => {
|
|
44
|
+
const { rerender } = render(<Button size="sm">Small</Button>)
|
|
45
|
+
let button = screen.getByRole('button', { name: 'Small' })
|
|
46
|
+
expect(button).toHaveClass('h-9')
|
|
47
|
+
|
|
48
|
+
rerender(<Button size="lg">Large</Button>)
|
|
49
|
+
button = screen.getByRole('button', { name: 'Large' })
|
|
50
|
+
expect(button).toHaveClass('h-11')
|
|
51
|
+
|
|
52
|
+
rerender(<Button size="icon">Icon</Button>)
|
|
53
|
+
button = screen.getByRole('button', { name: 'Icon' })
|
|
54
|
+
expect(button).toHaveClass('h-10', 'w-10')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should be disabled when disabled prop is true', async () => {
|
|
58
|
+
render(<Button disabled>Disabled</Button>)
|
|
59
|
+
|
|
60
|
+
const button = screen.getByRole('button', { name: 'Disabled' })
|
|
61
|
+
expect(button).toBeDisabled()
|
|
62
|
+
expect(button).toHaveClass('disabled:opacity-50')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should not trigger click when disabled', async () => {
|
|
66
|
+
let clicked = false
|
|
67
|
+
const handleClick = () => {
|
|
68
|
+
clicked = true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
render(
|
|
72
|
+
<Button disabled onClick={handleClick}>
|
|
73
|
+
Disabled
|
|
74
|
+
</Button>,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const button = screen.getByRole('button', { name: 'Disabled' })
|
|
78
|
+
|
|
79
|
+
// Verify button is disabled - browsers prevent clicking disabled buttons
|
|
80
|
+
expect(button).toBeDisabled()
|
|
81
|
+
expect(button).toHaveClass('disabled:pointer-events-none')
|
|
82
|
+
|
|
83
|
+
// In real browsers, disabled buttons cannot be clicked
|
|
84
|
+
// The pointer-events-none class prevents any interaction
|
|
85
|
+
expect(clicked).toBe(false)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should handle keyboard navigation', async () => {
|
|
89
|
+
let clicked = false
|
|
90
|
+
const handleClick = () => {
|
|
91
|
+
clicked = true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
render(<Button onClick={handleClick}>Press Enter</Button>)
|
|
95
|
+
|
|
96
|
+
const button = screen.getByRole('button', { name: 'Press Enter' })
|
|
97
|
+
button.focus()
|
|
98
|
+
expect(document.activeElement).toBe(button)
|
|
99
|
+
|
|
100
|
+
await userEvent.keyboard('{Enter}')
|
|
101
|
+
expect(clicked).toBe(true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should support custom className', async () => {
|
|
105
|
+
render(<Button className="custom-class">Custom</Button>)
|
|
106
|
+
|
|
107
|
+
const button = screen.getByRole('button', { name: 'Custom' })
|
|
108
|
+
expect(button).toHaveClass('custom-class')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should render as child component when asChild is true', async () => {
|
|
112
|
+
render(
|
|
113
|
+
<Button asChild>
|
|
114
|
+
<a href="/test">Link Button</a>
|
|
115
|
+
</Button>,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const link = screen.getByRole('link', { name: 'Link Button' })
|
|
119
|
+
expect(link).toBeInTheDocument()
|
|
120
|
+
expect(link).toHaveAttribute('href', '/test')
|
|
121
|
+
})
|
|
122
|
+
})
|