@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
@@ -1,24 +1,27 @@
1
1
  import Link from 'next/link.js'
2
2
  import { formatListName } from '../lib/utils.js'
3
- import { AccessContext, getUrlKey, OpenSaasConfig } from '@opensaas/stack-core'
3
+ import { type AccessContext, getUrlKey, OpenSaasConfig } from '@opensaas/stack-core'
4
+ import { UserMenu } from './UserMenu.js'
4
5
 
5
- export interface NavigationProps<TPrisma> {
6
- context: AccessContext<TPrisma>
6
+ export interface NavigationProps {
7
+ context: AccessContext<unknown>
7
8
  config: OpenSaasConfig
8
9
  basePath?: string
9
10
  currentPath?: string
11
+ onSignOut?: () => Promise<void>
10
12
  }
11
13
 
12
14
  /**
13
15
  * Navigation sidebar showing all lists
14
16
  * Server Component
15
17
  */
16
- export function Navigation<TPrisma>({
18
+ export function Navigation({
17
19
  context,
18
20
  config,
19
21
  basePath = '/admin',
20
22
  currentPath = '',
21
- }: NavigationProps<TPrisma>) {
23
+ onSignOut,
24
+ }: NavigationProps) {
22
25
  const lists = Object.keys(config.lists || {})
23
26
 
24
27
  return (
@@ -90,27 +93,13 @@ export function Navigation<TPrisma>({
90
93
  </div>
91
94
  </div>
92
95
 
93
- {/* Footer */}
96
+ {/* Footer - User Menu */}
94
97
  {context.session && (
95
- <div className="p-4 border-t border-border bg-gradient-to-br from-primary/5 to-accent/5">
96
- <div className="flex items-center space-x-3">
97
- <div className="h-9 w-9 rounded-full bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-lg shadow-primary/25">
98
- <span className="text-sm font-bold text-primary-foreground">
99
- {String(
100
- (context.session.data as Record<string, unknown>)?.name,
101
- )?.[0]?.toUpperCase() || '?'}
102
- </span>
103
- </div>
104
- <div className="flex-1 min-w-0">
105
- <p className="text-sm font-medium truncate">
106
- {String((context.session.data as Record<string, unknown>)?.name) || 'User'}
107
- </p>
108
- <p className="text-xs text-muted-foreground truncate">
109
- {String((context.session.data as Record<string, unknown>)?.email) || ''}
110
- </p>
111
- </div>
112
- </div>
113
- </div>
98
+ <UserMenu
99
+ userName={String((context.session.data as Record<string, unknown>)?.name) || 'User'}
100
+ userEmail={String((context.session.data as Record<string, unknown>)?.email) || ''}
101
+ onSignOut={onSignOut}
102
+ />
114
103
  )}
115
104
  </nav>
116
105
  )
@@ -0,0 +1,44 @@
1
+ 'use client'
2
+
3
+ import { useRouter } from 'next/navigation.js'
4
+ import { Button } from '../primitives/button.js'
5
+
6
+ export interface UserMenuProps {
7
+ userName?: string
8
+ userEmail?: string
9
+ onSignOut?: () => Promise<void>
10
+ }
11
+
12
+ /**
13
+ * User menu component with sign-out button
14
+ * Client Component
15
+ */
16
+ export function UserMenu({ userName, userEmail, onSignOut }: UserMenuProps) {
17
+ const router = useRouter()
18
+
19
+ const handleSignOut = async () => {
20
+ if (onSignOut) {
21
+ await onSignOut()
22
+ }
23
+ router.push('/sign-in')
24
+ }
25
+
26
+ return (
27
+ <div className="p-4 border-t border-border bg-gradient-to-br from-primary/5 to-accent/5">
28
+ <div className="flex items-center space-x-3 mb-3">
29
+ <div className="h-9 w-9 rounded-full bg-gradient-to-br from-primary to-accent flex items-center justify-center shadow-lg shadow-primary/25">
30
+ <span className="text-sm font-bold text-primary-foreground">
31
+ {userName?.[0]?.toUpperCase() || '?'}
32
+ </span>
33
+ </div>
34
+ <div className="flex-1 min-w-0">
35
+ <p className="text-sm font-medium truncate">{userName || 'User'}</p>
36
+ <p className="text-xs text-muted-foreground truncate">{userEmail || ''}</p>
37
+ </div>
38
+ </div>
39
+ <Button onClick={handleSignOut} variant="outline" size="sm" className="w-full text-sm">
40
+ Sign Out
41
+ </Button>
42
+ </div>
43
+ )
44
+ }
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { Input } from '../../primitives/input.js'
4
+ import { Textarea } from '../../primitives/textarea.js'
4
5
  import { Label } from '../../primitives/label.js'
5
6
  import { cn } from '../../lib/utils.js'
6
7
 
@@ -14,6 +15,7 @@ export interface TextFieldProps {
14
15
  disabled?: boolean
15
16
  required?: boolean
16
17
  mode?: 'read' | 'edit'
18
+ displayMode?: 'input' | 'textarea'
17
19
  }
18
20
 
19
21
  export function TextField({
@@ -26,6 +28,7 @@ export function TextField({
26
28
  disabled,
27
29
  required,
28
30
  mode = 'edit',
31
+ displayMode = 'input',
29
32
  }: TextFieldProps) {
30
33
  if (mode === 'read') {
31
34
  return (
@@ -36,16 +39,18 @@ export function TextField({
36
39
  )
37
40
  }
38
41
 
42
+ const InputComponent = displayMode === 'textarea' ? Textarea : Input
43
+
39
44
  return (
40
45
  <div className="space-y-2">
41
46
  <Label htmlFor={name}>
42
47
  {label}
43
48
  {required && <span className="text-destructive ml-1">*</span>}
44
49
  </Label>
45
- <Input
50
+ <InputComponent
46
51
  id={name}
47
52
  name={name}
48
- type="text"
53
+ type={displayMode === 'input' ? 'text' : undefined}
49
54
  value={value || ''}
50
55
  onChange={(e) => onChange(e.target.value)}
51
56
  placeholder={placeholder}
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  export { AdminUI } from './components/AdminUI.js'
3
3
  export { Dashboard } from './components/Dashboard.js'
4
4
  export { Navigation } from './components/Navigation.js'
5
+ export { UserMenu } from './components/UserMenu.js'
5
6
  export { ListView } from './components/ListView.js'
6
7
  export { ListViewClient } from './components/ListViewClient.js'
7
8
  export { ItemForm } from './components/ItemForm.js'
@@ -29,6 +30,7 @@ export {
29
30
  export type { AdminUIProps } from './components/AdminUI.js'
30
31
  export type { DashboardProps } from './components/Dashboard.js'
31
32
  export type { NavigationProps } from './components/Navigation.js'
33
+ export type { UserMenuProps } from './components/UserMenu.js'
32
34
  export type { ListViewProps } from './components/ListView.js'
33
35
  export type { ListViewClientProps } from './components/ListViewClient.js'
34
36
  export type { ItemFormProps } from './components/ItemForm.js'
@@ -31,8 +31,7 @@ const buttonVariants = cva(
31
31
  )
32
32
 
33
33
  export interface ButtonProps
34
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
35
- VariantProps<typeof buttonVariants> {
34
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
36
35
  asChild?: boolean
37
36
  }
38
37
 
@@ -0,0 +1,154 @@
1
+ # Browser Tests
2
+
3
+ This directory contains UI tests that run in a real browser environment using Vitest Browser Mode with Playwright.
4
+
5
+ ## Overview
6
+
7
+ Browser tests provide more realistic testing by running tests in actual browser environments (Chromium, Firefox, or WebKit). These tests can verify:
8
+
9
+ - Real browser interactions (clicks, keyboard input, focus management)
10
+ - Visual rendering and CSS behavior
11
+ - Accessibility features
12
+ - Browser-specific bugs
13
+
14
+ ## Running Browser Tests
15
+
16
+ ### Prerequisites
17
+
18
+ 1. Install Playwright browsers:
19
+
20
+ ```bash
21
+ npx playwright install chromium
22
+ ```
23
+
24
+ 2. Ensure you have a display server available (for headless: false mode)
25
+
26
+ ### Commands
27
+
28
+ ```bash
29
+ # Run browser tests in headless mode
30
+ pnpm test:browser
31
+
32
+ # Run browser tests with UI
33
+ pnpm test:browser:ui
34
+ ```
35
+
36
+ ### Configuration
37
+
38
+ Browser tests are configured in `vitest.config.ts`:
39
+
40
+ - **Enabled**: Only when `BROWSER_TEST=true` environment variable is set
41
+ - **Browser**: Chromium (via Playwright)
42
+ - **Headless**: true (can be set to false for debugging)
43
+ - **Screenshot on failure**: Enabled
44
+
45
+ ## Test Structure
46
+
47
+ Browser tests follow this structure:
48
+
49
+ ```typescript
50
+ import { describe, it, expect } from 'vitest'
51
+ import { render, screen } from '@testing-library/react'
52
+ import { userEvent } from 'vitest/browser'
53
+ import { MyComponent } from '../../../src/components/MyComponent.js'
54
+
55
+ describe('MyComponent (Browser)', () => {
56
+ it('should handle real browser interactions', async () => {
57
+ render(<MyComponent />)
58
+
59
+ const button = screen.getByRole('button')
60
+ await userEvent.click(button)
61
+
62
+ expect(button).toHaveAttribute('aria-pressed', 'true')
63
+ })
64
+ })
65
+ ```
66
+
67
+ ## Test Categories
68
+
69
+ ### Primitives
70
+
71
+ - **Button.browser.test.tsx**: Tests button variants, keyboard navigation, focus states
72
+ - **Dialog.browser.test.tsx**: Tests modal dialogs, focus trapping, keyboard shortcuts (Escape)
73
+
74
+ ### Fields
75
+
76
+ - **TextField.browser.test.tsx**: Tests text input, paste, special characters, focus/blur
77
+ - **CheckboxField.browser.test.tsx**: Tests checkbox toggling, keyboard (Space), label clicks
78
+ - **SelectField.browser.test.tsx**: Tests dropdown behavior, keyboard navigation (arrows)
79
+
80
+ ## Browser vs Regular Tests
81
+
82
+ | Aspect | Regular Tests (Happy DOM) | Browser Tests |
83
+ | --------------------- | ------------------------- | -------------- |
84
+ | **Speed** | Fast (~500ms) | Slower (~5s) |
85
+ | **Environment** | Simulated DOM | Real browser |
86
+ | **User Interactions** | Simulated | Real events |
87
+ | **Visual Testing** | No | Yes |
88
+ | **CI/CD** | Easy | Requires setup |
89
+
90
+ ## When to Use Browser Tests
91
+
92
+ Use browser tests for:
93
+
94
+ - Complex user interactions (drag-drop, keyboard navigation)
95
+ - Focus management and accessibility
96
+ - Browser-specific features
97
+ - Visual regression testing
98
+ - Issues that only reproduce in real browsers
99
+
100
+ Use regular tests (Happy DOM) for:
101
+
102
+ - Unit testing components
103
+ - Testing props and state
104
+ - Fast feedback during development
105
+ - Most UI logic
106
+
107
+ ## CI/CD Setup
108
+
109
+ For CI/CD environments, ensure:
110
+
111
+ 1. **Display Server**: Use `xvfb` or run in headless mode
112
+ 2. **Playwright Installation**: Include `npx playwright install --with-deps` in CI setup
113
+ 3. **Browser Binaries**: Ensure chromium/firefox/webkit are available
114
+
115
+ Example GitHub Actions:
116
+
117
+ ```yaml
118
+ - name: Install Playwright
119
+ run: npx playwright install --with-deps chromium
120
+
121
+ - name: Run browser tests
122
+ run: pnpm test:browser
123
+ ```
124
+
125
+ ## Troubleshooting
126
+
127
+ ### "Browser connection was closed"
128
+
129
+ - **Cause**: No display server available
130
+ - **Solution**: Run in headless mode or use xvfb
131
+
132
+ ### "vitest/browser can be imported only inside Browser Mode"
133
+
134
+ - **Cause**: Browser tests running in regular test mode
135
+ - **Solution**: Ensure tests are in `tests/browser/` directory and use `pnpm test:browser`
136
+
137
+ ### Playwright installation errors
138
+
139
+ - **Cause**: Missing system dependencies
140
+ - **Solution**: Run `npx playwright install --with-deps chromium`
141
+
142
+ ## Development Tips
143
+
144
+ 1. **Debugging**: Set `headless: false` in vitest.config.ts to see the browser
145
+ 2. **Screenshots**: Failed tests automatically capture screenshots
146
+ 3. **Slow Tests**: Use `{ timeout: 10000 }` for tests that need more time
147
+ 4. **Browser DevTools**: Set `slowMo: 100` to slow down interactions for debugging
148
+
149
+ ## Future Enhancements
150
+
151
+ - [ ] Add visual regression testing with screenshots
152
+ - [ ] Test in multiple browsers (Firefox, WebKit)
153
+ - [ ] Add E2E tests for complete user flows
154
+ - [ ] Integrate with Percy or similar visual testing service
@@ -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
+ })