@opensaas/stack-ui 0.1.7 → 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.
Files changed (47) hide show
  1. package/.turbo/turbo-build.log +3 -2
  2. package/CHANGELOG.md +4 -0
  3. package/dist/components/AdminUI.d.ts +2 -1
  4. package/dist/components/AdminUI.d.ts.map +1 -1
  5. package/dist/components/AdminUI.js +2 -2
  6. package/dist/components/ItemFormClient.d.ts.map +1 -1
  7. package/dist/components/ItemFormClient.js +78 -60
  8. package/dist/components/Navigation.d.ts +2 -1
  9. package/dist/components/Navigation.d.ts.map +1 -1
  10. package/dist/components/Navigation.js +3 -2
  11. package/dist/components/UserMenu.d.ts +11 -0
  12. package/dist/components/UserMenu.d.ts.map +1 -0
  13. package/dist/components/UserMenu.js +18 -0
  14. package/dist/components/fields/TextField.d.ts +2 -1
  15. package/dist/components/fields/TextField.d.ts.map +1 -1
  16. package/dist/components/fields/TextField.js +4 -2
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/primitives/button.d.ts +1 -1
  21. package/dist/styles/globals.css +24 -0
  22. package/package.json +14 -5
  23. package/src/components/AdminUI.tsx +3 -0
  24. package/src/components/ItemFormClient.tsx +84 -62
  25. package/src/components/Navigation.tsx +9 -20
  26. package/src/components/UserMenu.tsx +44 -0
  27. package/src/components/fields/TextField.tsx +7 -2
  28. package/src/index.ts +2 -0
  29. package/tests/browser/README.md +154 -0
  30. package/tests/browser/fields/CheckboxField.browser.test.tsx +245 -0
  31. package/tests/browser/fields/SelectField.browser.test.tsx +263 -0
  32. package/tests/browser/fields/TextField.browser.test.tsx +204 -0
  33. package/tests/browser/fields/__screenshots__/CheckboxField.browser.test.tsx/CheckboxField--Browser--edit-mode-should-not-be-clickable-when-disabled-1.png +0 -0
  34. package/tests/browser/fields/__screenshots__/CheckboxField.browser.test.tsx/CheckboxField--Browser--edit-mode-should-toggle-state-with-multiple-clicks-1.png +0 -0
  35. package/tests/browser/fields/__screenshots__/TextField.browser.test.tsx/TextField--Browser--edit-mode-should-handle-special-characters-1.png +0 -0
  36. package/tests/browser/fields/__screenshots__/TextField.browser.test.tsx/TextField--Browser--edit-mode-should-support-copy-and-paste-1.png +0 -0
  37. package/tests/browser/primitives/Button.browser.test.tsx +122 -0
  38. package/tests/browser/primitives/Dialog.browser.test.tsx +279 -0
  39. package/tests/browser/primitives/__screenshots__/Button.browser.test.tsx/Button--Browser--should-not-trigger-click-when-disabled-1.png +0 -0
  40. package/tests/components/CheckboxField.test.tsx +130 -0
  41. package/tests/components/DeleteButton.test.tsx +331 -0
  42. package/tests/components/IntegerField.test.tsx +147 -0
  43. package/tests/components/ListTable.test.tsx +457 -0
  44. package/tests/components/ListViewClient.test.tsx +415 -0
  45. package/tests/components/SearchBar.test.tsx +254 -0
  46. package/tests/components/SelectField.test.tsx +192 -0
  47. package/vitest.config.ts +20 -0
@@ -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
+ })
@@ -0,0 +1,279 @@
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 {
6
+ Dialog,
7
+ DialogTrigger,
8
+ DialogContent,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ DialogDescription,
12
+ DialogFooter,
13
+ DialogClose,
14
+ } from '../../../src/primitives/dialog.js'
15
+ import { Button } from '../../../src/primitives/button.js'
16
+
17
+ describe('Dialog (Browser)', () => {
18
+ it('should render dialog trigger', async () => {
19
+ await act(async () => {
20
+ render(
21
+ <Dialog>
22
+ <DialogTrigger asChild>
23
+ <Button>Open Dialog</Button>
24
+ </DialogTrigger>
25
+ </Dialog>,
26
+ )
27
+ })
28
+
29
+ const trigger = screen.getByRole('button', { name: 'Open Dialog' })
30
+ expect(trigger).toBeInTheDocument()
31
+ })
32
+
33
+ it('should open dialog when trigger is clicked', async () => {
34
+ function TestDialog() {
35
+ const [open, setOpen] = React.useState(false)
36
+ return (
37
+ <Dialog open={open} onOpenChange={setOpen}>
38
+ <DialogTrigger asChild>
39
+ <Button>Open Dialog</Button>
40
+ </DialogTrigger>
41
+ <DialogContent>
42
+ <DialogHeader>
43
+ <DialogTitle>Dialog Title</DialogTitle>
44
+ <DialogDescription>Dialog description</DialogDescription>
45
+ </DialogHeader>
46
+ </DialogContent>
47
+ </Dialog>
48
+ )
49
+ }
50
+
51
+ await act(async () => {
52
+ render(<TestDialog />)
53
+ })
54
+
55
+ const trigger = screen.getByRole('button', { name: 'Open Dialog' })
56
+
57
+ await act(async () => {
58
+ await userEvent.click(trigger)
59
+ })
60
+
61
+ await waitFor(
62
+ () => {
63
+ expect(screen.getByRole('dialog')).toBeInTheDocument()
64
+ expect(screen.getByText('Dialog Title')).toBeInTheDocument()
65
+ expect(screen.getByText('Dialog description')).toBeInTheDocument()
66
+ },
67
+ { timeout: 3000 },
68
+ )
69
+ })
70
+
71
+ it('should close dialog when close button is clicked', async () => {
72
+ function TestDialog() {
73
+ const [open, setOpen] = React.useState(false)
74
+ return (
75
+ <Dialog open={open} onOpenChange={setOpen}>
76
+ <DialogTrigger asChild>
77
+ <Button>Open Dialog</Button>
78
+ </DialogTrigger>
79
+ <DialogContent>
80
+ <DialogHeader>
81
+ <DialogTitle>Dialog Title</DialogTitle>
82
+ </DialogHeader>
83
+ </DialogContent>
84
+ </Dialog>
85
+ )
86
+ }
87
+
88
+ await act(async () => {
89
+ render(<TestDialog />)
90
+ })
91
+
92
+ const trigger = screen.getByRole('button', { name: 'Open Dialog' })
93
+
94
+ await act(async () => {
95
+ await userEvent.click(trigger)
96
+ })
97
+
98
+ await waitFor(() => {
99
+ expect(screen.getByRole('dialog')).toBeInTheDocument()
100
+ })
101
+
102
+ // Click the X close button
103
+ const closeButton = screen.getByRole('button', { name: 'Close' })
104
+
105
+ await act(async () => {
106
+ await userEvent.click(closeButton)
107
+ })
108
+
109
+ await waitFor(() => {
110
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
111
+ })
112
+ })
113
+
114
+ it('should close dialog with DialogClose component', async () => {
115
+ function TestDialog() {
116
+ const [open, setOpen] = React.useState(false)
117
+ return (
118
+ <Dialog open={open} onOpenChange={setOpen}>
119
+ <DialogTrigger asChild>
120
+ <Button>Open Dialog</Button>
121
+ </DialogTrigger>
122
+ <DialogContent>
123
+ <DialogHeader>
124
+ <DialogTitle>Dialog Title</DialogTitle>
125
+ </DialogHeader>
126
+ <DialogFooter>
127
+ <DialogClose asChild>
128
+ <Button variant="outline">Cancel</Button>
129
+ </DialogClose>
130
+ </DialogFooter>
131
+ </DialogContent>
132
+ </Dialog>
133
+ )
134
+ }
135
+
136
+ await act(async () => {
137
+ render(<TestDialog />)
138
+ })
139
+
140
+ const trigger = screen.getByRole('button', { name: 'Open Dialog' })
141
+
142
+ await act(async () => {
143
+ await userEvent.click(trigger)
144
+ })
145
+
146
+ await waitFor(() => {
147
+ expect(screen.getByRole('dialog')).toBeInTheDocument()
148
+ })
149
+
150
+ const cancelButton = screen.getByRole('button', { name: 'Cancel' })
151
+
152
+ await act(async () => {
153
+ await userEvent.click(cancelButton)
154
+ })
155
+
156
+ await waitFor(() => {
157
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
158
+ })
159
+ })
160
+
161
+ it('should close dialog when escape key is pressed', async () => {
162
+ function TestDialog() {
163
+ const [open, setOpen] = React.useState(false)
164
+ return (
165
+ <Dialog open={open} onOpenChange={setOpen}>
166
+ <DialogTrigger asChild>
167
+ <Button>Open Dialog</Button>
168
+ </DialogTrigger>
169
+ <DialogContent>
170
+ <DialogHeader>
171
+ <DialogTitle>Dialog Title</DialogTitle>
172
+ </DialogHeader>
173
+ </DialogContent>
174
+ </Dialog>
175
+ )
176
+ }
177
+
178
+ await act(async () => {
179
+ render(<TestDialog />)
180
+ })
181
+
182
+ const trigger = screen.getByRole('button', { name: 'Open Dialog' })
183
+
184
+ await act(async () => {
185
+ await userEvent.click(trigger)
186
+ })
187
+
188
+ await waitFor(() => {
189
+ expect(screen.getByRole('dialog')).toBeInTheDocument()
190
+ })
191
+
192
+ await act(async () => {
193
+ await userEvent.keyboard('{Escape}')
194
+ })
195
+
196
+ await waitFor(() => {
197
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
198
+ })
199
+ })
200
+
201
+ it('should render overlay when dialog is open', async () => {
202
+ function TestDialog() {
203
+ const [open, setOpen] = React.useState(false)
204
+ return (
205
+ <Dialog open={open} onOpenChange={setOpen}>
206
+ <DialogTrigger asChild>
207
+ <Button>Open Dialog</Button>
208
+ </DialogTrigger>
209
+ <DialogContent>
210
+ <DialogHeader>
211
+ <DialogTitle>Dialog Title</DialogTitle>
212
+ </DialogHeader>
213
+ </DialogContent>
214
+ </Dialog>
215
+ )
216
+ }
217
+
218
+ await act(async () => {
219
+ render(<TestDialog />)
220
+ })
221
+
222
+ const trigger = screen.getByRole('button', { name: 'Open Dialog' })
223
+
224
+ await act(async () => {
225
+ await userEvent.click(trigger)
226
+ })
227
+
228
+ await waitFor(() => {
229
+ const overlay = document.querySelector('[data-state="open"]')
230
+ expect(overlay).toBeInTheDocument()
231
+ })
232
+ })
233
+
234
+ it('should trap focus within dialog when open', async () => {
235
+ function TestDialog() {
236
+ const [open, setOpen] = React.useState(false)
237
+ return (
238
+ <Dialog open={open} onOpenChange={setOpen}>
239
+ <DialogTrigger asChild>
240
+ <Button>Open Dialog</Button>
241
+ </DialogTrigger>
242
+ <DialogContent>
243
+ <DialogHeader>
244
+ <DialogTitle>Dialog Title</DialogTitle>
245
+ </DialogHeader>
246
+ <DialogFooter>
247
+ <Button>First Button</Button>
248
+ <Button>Second Button</Button>
249
+ </DialogFooter>
250
+ </DialogContent>
251
+ </Dialog>
252
+ )
253
+ }
254
+
255
+ await act(async () => {
256
+ render(<TestDialog />)
257
+ })
258
+
259
+ const trigger = screen.getByRole('button', { name: 'Open Dialog' })
260
+
261
+ await act(async () => {
262
+ await userEvent.click(trigger)
263
+ })
264
+
265
+ await waitFor(() => {
266
+ expect(screen.getByRole('dialog')).toBeInTheDocument()
267
+ })
268
+
269
+ // Tab through focusable elements
270
+ await act(async () => {
271
+ await userEvent.keyboard('{Tab}')
272
+ })
273
+
274
+ // Focus should be within the dialog
275
+ const dialog = screen.getByRole('dialog')
276
+ const activeElement = document.activeElement
277
+ expect(dialog.contains(activeElement)).toBe(true)
278
+ })
279
+ })
@@ -0,0 +1,130 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { render, screen } from '@testing-library/react'
3
+ import userEvent from '@testing-library/user-event'
4
+ import { CheckboxField } from '../../src/components/fields/CheckboxField.js'
5
+
6
+ describe('CheckboxField', () => {
7
+ describe('edit mode', () => {
8
+ it('should render checkbox with label', () => {
9
+ render(<CheckboxField name="active" value={false} onChange={vi.fn()} label="Is Active" />)
10
+
11
+ expect(screen.getByLabelText('Is Active')).toBeInTheDocument()
12
+ expect(screen.getByRole('checkbox')).toBeInTheDocument()
13
+ })
14
+
15
+ it('should display checked state when value is true', () => {
16
+ render(<CheckboxField name="active" value={true} onChange={vi.fn()} label="Is Active" />)
17
+
18
+ const checkbox = screen.getByRole('checkbox')
19
+ expect(checkbox).toBeChecked()
20
+ })
21
+
22
+ it('should display unchecked state when value is false', () => {
23
+ render(<CheckboxField name="active" value={false} onChange={vi.fn()} label="Is Active" />)
24
+
25
+ const checkbox = screen.getByRole('checkbox')
26
+ expect(checkbox).not.toBeChecked()
27
+ })
28
+
29
+ it('should call onChange with true when clicked', async () => {
30
+ const onChange = vi.fn()
31
+ const user = userEvent.setup()
32
+
33
+ render(<CheckboxField name="active" value={false} onChange={onChange} label="Is Active" />)
34
+
35
+ const checkbox = screen.getByRole('checkbox')
36
+ await user.click(checkbox)
37
+
38
+ expect(onChange).toHaveBeenCalledWith(true)
39
+ })
40
+
41
+ it('should call onChange with false when unchecked', async () => {
42
+ const onChange = vi.fn()
43
+ const user = userEvent.setup()
44
+
45
+ render(<CheckboxField name="active" value={true} onChange={onChange} label="Is Active" />)
46
+
47
+ const checkbox = screen.getByRole('checkbox')
48
+ await user.click(checkbox)
49
+
50
+ expect(onChange).toHaveBeenCalledWith(false)
51
+ })
52
+
53
+ it('should display error message', () => {
54
+ render(
55
+ <CheckboxField
56
+ name="terms"
57
+ value={false}
58
+ onChange={vi.fn()}
59
+ label="Accept Terms"
60
+ error="You must accept the terms"
61
+ />,
62
+ )
63
+
64
+ expect(screen.getByText('You must accept the terms')).toBeInTheDocument()
65
+ })
66
+
67
+ it('should be disabled when disabled prop is true', () => {
68
+ render(
69
+ <CheckboxField name="active" value={false} onChange={vi.fn()} label="Is Active" disabled />,
70
+ )
71
+
72
+ const checkbox = screen.getByRole('checkbox')
73
+ expect(checkbox).toBeDisabled()
74
+ })
75
+
76
+ it('should not be clickable when disabled', async () => {
77
+ const onChange = vi.fn()
78
+ const user = userEvent.setup()
79
+
80
+ render(
81
+ <CheckboxField
82
+ name="active"
83
+ value={false}
84
+ onChange={onChange}
85
+ label="Is Active"
86
+ disabled
87
+ />,
88
+ )
89
+
90
+ const checkbox = screen.getByRole('checkbox')
91
+ await user.click(checkbox)
92
+
93
+ expect(onChange).not.toHaveBeenCalled()
94
+ })
95
+ })
96
+
97
+ describe('read mode', () => {
98
+ it('should render "Yes" when value is true', () => {
99
+ render(
100
+ <CheckboxField
101
+ name="active"
102
+ value={true}
103
+ onChange={vi.fn()}
104
+ label="Is Active"
105
+ mode="read"
106
+ />,
107
+ )
108
+
109
+ expect(screen.getByText('Is Active')).toBeInTheDocument()
110
+ expect(screen.getByText('Yes')).toBeInTheDocument()
111
+ expect(screen.queryByRole('checkbox')).not.toBeInTheDocument()
112
+ })
113
+
114
+ it('should render "No" when value is false', () => {
115
+ render(
116
+ <CheckboxField
117
+ name="active"
118
+ value={false}
119
+ onChange={vi.fn()}
120
+ label="Is Active"
121
+ mode="read"
122
+ />,
123
+ )
124
+
125
+ expect(screen.getByText('Is Active')).toBeInTheDocument()
126
+ expect(screen.getByText('No')).toBeInTheDocument()
127
+ expect(screen.queryByRole('checkbox')).not.toBeInTheDocument()
128
+ })
129
+ })
130
+ })