@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,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
+ })