@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.
Files changed (61) hide show
  1. package/.turbo/turbo-build.log +3 -2
  2. package/CHANGELOG.md +140 -0
  3. package/dist/components/AdminUI.d.ts +5 -4
  4. package/dist/components/AdminUI.d.ts.map +1 -1
  5. package/dist/components/AdminUI.js +2 -2
  6. package/dist/components/Dashboard.d.ts +4 -4
  7. package/dist/components/Dashboard.d.ts.map +1 -1
  8. package/dist/components/Dashboard.js +4 -3
  9. package/dist/components/ItemForm.d.ts +4 -4
  10. package/dist/components/ItemForm.d.ts.map +1 -1
  11. package/dist/components/ItemForm.js +9 -5
  12. package/dist/components/ItemFormClient.d.ts.map +1 -1
  13. package/dist/components/ItemFormClient.js +78 -60
  14. package/dist/components/ListView.d.ts +4 -4
  15. package/dist/components/ListView.d.ts.map +1 -1
  16. package/dist/components/ListView.js +18 -11
  17. package/dist/components/Navigation.d.ts +5 -4
  18. package/dist/components/Navigation.d.ts.map +1 -1
  19. package/dist/components/Navigation.js +3 -2
  20. package/dist/components/UserMenu.d.ts +11 -0
  21. package/dist/components/UserMenu.d.ts.map +1 -0
  22. package/dist/components/UserMenu.js +18 -0
  23. package/dist/components/fields/TextField.d.ts +2 -1
  24. package/dist/components/fields/TextField.d.ts.map +1 -1
  25. package/dist/components/fields/TextField.js +4 -2
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -0
  29. package/dist/primitives/button.d.ts +1 -1
  30. package/dist/primitives/button.d.ts.map +1 -1
  31. package/dist/styles/globals.css +24 -0
  32. package/package.json +32 -23
  33. package/src/components/AdminUI.tsx +8 -5
  34. package/src/components/Dashboard.tsx +7 -10
  35. package/src/components/ItemForm.tsx +14 -10
  36. package/src/components/ItemFormClient.tsx +84 -62
  37. package/src/components/ListView.tsx +23 -21
  38. package/src/components/Navigation.tsx +14 -25
  39. package/src/components/UserMenu.tsx +44 -0
  40. package/src/components/fields/TextField.tsx +7 -2
  41. package/src/index.ts +2 -0
  42. package/src/primitives/button.tsx +1 -2
  43. package/tests/browser/README.md +154 -0
  44. package/tests/browser/fields/CheckboxField.browser.test.tsx +245 -0
  45. package/tests/browser/fields/SelectField.browser.test.tsx +263 -0
  46. package/tests/browser/fields/TextField.browser.test.tsx +204 -0
  47. package/tests/browser/fields/__screenshots__/CheckboxField.browser.test.tsx/CheckboxField--Browser--edit-mode-should-not-be-clickable-when-disabled-1.png +0 -0
  48. package/tests/browser/fields/__screenshots__/CheckboxField.browser.test.tsx/CheckboxField--Browser--edit-mode-should-toggle-state-with-multiple-clicks-1.png +0 -0
  49. package/tests/browser/fields/__screenshots__/TextField.browser.test.tsx/TextField--Browser--edit-mode-should-handle-special-characters-1.png +0 -0
  50. package/tests/browser/fields/__screenshots__/TextField.browser.test.tsx/TextField--Browser--edit-mode-should-support-copy-and-paste-1.png +0 -0
  51. package/tests/browser/primitives/Button.browser.test.tsx +122 -0
  52. package/tests/browser/primitives/Dialog.browser.test.tsx +279 -0
  53. package/tests/browser/primitives/__screenshots__/Button.browser.test.tsx/Button--Browser--should-not-trigger-click-when-disabled-1.png +0 -0
  54. package/tests/components/CheckboxField.test.tsx +130 -0
  55. package/tests/components/DeleteButton.test.tsx +331 -0
  56. package/tests/components/IntegerField.test.tsx +147 -0
  57. package/tests/components/ListTable.test.tsx +457 -0
  58. package/tests/components/ListViewClient.test.tsx +415 -0
  59. package/tests/components/SearchBar.test.tsx +254 -0
  60. package/tests/components/SelectField.test.tsx +192 -0
  61. 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
+ })
@@ -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
+ })