@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
|
@@ -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
|
|
6
|
-
context: AccessContext<
|
|
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
|
|
18
|
+
export function Navigation({
|
|
17
19
|
context,
|
|
18
20
|
config,
|
|
19
21
|
basePath = '/admin',
|
|
20
22
|
currentPath = '',
|
|
21
|
-
|
|
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
|
-
<
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
<
|
|
50
|
+
<InputComponent
|
|
46
51
|
id={name}
|
|
47
52
|
name={name}
|
|
48
|
-
type=
|
|
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
|
+
})
|