mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-13 08:15:21 +02:00
Add Open AI Comatible API option in model configuration
Add unit tests for various UI components and context hooks - Created tests for Progress, RadioGroup, Select, Separator, Sheet, Sidebar, Skeleton, Sonner, Switch, Table, Tabs, Textarea, Tooltip components. - Implemented tests for UserConfigContext and other context hooks. - Added utility tests for filters and utils functions. - Set up MSW for API mocking in tests. - Configured Vitest for testing environment with necessary setup.
This commit is contained in:
parent
bbb4f91a27
commit
20617db37a
44 changed files with 8426 additions and 5661 deletions
11565
ui/package-lock.json
generated
11565
ui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ui",
|
||||
"version": "1.31.0",
|
||||
"version": "1.31.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_OPTIONS=--enable-source-maps next dev --turbopack",
|
||||
|
|
@ -59,18 +59,33 @@
|
|||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@hey-api/openapi-ts": "^0.95.0",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"@vitest/coverage-v8": "^4.1.7",
|
||||
"@vitest/ui": "^4.1.7",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "^15.3.3",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"jsdom": "^29.1.1",
|
||||
"msw": "^2.14.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
"typescript": "^5",
|
||||
"vitest": "^4.1.7"
|
||||
},
|
||||
"overrides": {
|
||||
"lucide-react": "^0.505.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
56
ui/src/components/ui/alert-dialog.test.tsx
Normal file
56
ui/src/components/ui/alert-dialog.test.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@/test/test-utils';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
} from './alert-dialog';
|
||||
|
||||
describe('AlertDialog', () => {
|
||||
it('renders trigger', () => {
|
||||
render(
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger>Open</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Title</AlertDialogTitle>
|
||||
<AlertDialogDescription>Desc</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction>Confirm</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
expect(screen.getByText('Open')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders content when open', () => {
|
||||
render(
|
||||
<AlertDialog open>
|
||||
<AlertDialogTrigger>Open</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Alert Title</AlertDialogTitle>
|
||||
<AlertDialogDescription>Alert description</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction>Confirm</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
expect(screen.getByText('Alert Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Alert description')).toBeInTheDocument();
|
||||
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
||||
expect(screen.getByText('Confirm')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
35
ui/src/components/ui/badge.test.tsx
Normal file
35
ui/src/components/ui/badge.test.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@/test/test-utils';
|
||||
import { Badge } from './badge';
|
||||
|
||||
describe('Badge', () => {
|
||||
it('renders children text', () => {
|
||||
render(<Badge>New</Badge>);
|
||||
expect(screen.getByText('New')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies default variant classes', () => {
|
||||
const { container } = render(<Badge>Default</Badge>);
|
||||
expect(container.firstChild).toHaveClass('bg-primary');
|
||||
});
|
||||
|
||||
it('applies secondary variant classes', () => {
|
||||
const { container } = render(<Badge variant="secondary">Secondary</Badge>);
|
||||
expect(container.firstChild).toHaveClass('bg-secondary');
|
||||
});
|
||||
|
||||
it('applies destructive variant classes', () => {
|
||||
const { container } = render(<Badge variant="destructive">Error</Badge>);
|
||||
expect(container.firstChild).toHaveClass('bg-destructive');
|
||||
});
|
||||
|
||||
it('applies outline variant classes', () => {
|
||||
const { container } = render(<Badge variant="outline">Outline</Badge>);
|
||||
expect(container.firstChild).toHaveClass('text-foreground');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Badge className="custom-badge">Custom</Badge>);
|
||||
expect(container.firstChild).toHaveClass('custom-badge');
|
||||
});
|
||||
});
|
||||
51
ui/src/components/ui/button.test.tsx
Normal file
51
ui/src/components/ui/button.test.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen } from '@/test/test-utils';
|
||||
import { Button } from './button';
|
||||
|
||||
describe('Button', () => {
|
||||
it('renders children', () => {
|
||||
render(<Button>Click me</Button>);
|
||||
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles click events', () => {
|
||||
const handleClick = vi.fn();
|
||||
render(<Button onClick={handleClick}>Click</Button>);
|
||||
screen.getByRole('button').click();
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('is disabled when disabled prop is true', () => {
|
||||
render(<Button disabled>Disabled</Button>);
|
||||
expect(screen.getByRole('button')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('applies default variant classes', () => {
|
||||
const { container } = render(<Button>Default</Button>);
|
||||
expect(container.firstChild).toHaveClass('bg-primary');
|
||||
});
|
||||
|
||||
it('applies destructive variant classes', () => {
|
||||
const { container } = render(<Button variant="destructive">Delete</Button>);
|
||||
expect(container.firstChild).toHaveClass('bg-destructive');
|
||||
});
|
||||
|
||||
it('applies outline variant classes', () => {
|
||||
const { container } = render(<Button variant="outline">Outline</Button>);
|
||||
expect(container.firstChild).toHaveClass('border');
|
||||
});
|
||||
|
||||
it('applies size classes', () => {
|
||||
const { container } = render(<Button size="sm">Small</Button>);
|
||||
expect(container.firstChild).toHaveClass('h-8');
|
||||
});
|
||||
|
||||
it('renders as child when asChild is true', () => {
|
||||
render(
|
||||
<Button asChild>
|
||||
<a href="/">Link Button</a>
|
||||
</Button>
|
||||
);
|
||||
expect(screen.getByRole('link', { name: 'Link Button' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
56
ui/src/components/ui/calendar.test.tsx
Normal file
56
ui/src/components/ui/calendar.test.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { render } from '@/test/test-utils';
|
||||
import { Calendar } from './calendar';
|
||||
|
||||
describe('Calendar', () => {
|
||||
it('renders with default props', () => {
|
||||
const { container } = render(<Calendar />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts custom className', () => {
|
||||
const { container } = render(<Calendar className="custom-calendar" />);
|
||||
expect(container.firstChild).toHaveClass('custom-calendar');
|
||||
});
|
||||
|
||||
it('accepts showOutsideDays prop', () => {
|
||||
const { container } = render(<Calendar showOutsideDays={false} />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts captionLayout prop', () => {
|
||||
const { container } = render(<Calendar captionLayout="dropdown" />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts buttonVariant prop', () => {
|
||||
const { container } = render(<Calendar buttonVariant="outline" />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts fromDate and toDate constraints', () => {
|
||||
const fromDate = new Date(2024, 0, 1);
|
||||
const toDate = new Date(2024, 11, 31);
|
||||
const { container } = render(<Calendar fromDate={fromDate} toDate={toDate} />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts classNames prop', () => {
|
||||
const { container } = render(
|
||||
<Calendar classNames={{ root: 'custom-root' }} />
|
||||
);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts formatters prop', () => {
|
||||
const { container } = render(
|
||||
<Calendar formatters={{ formatMonthDropdown: (d) => d.toLocaleString('default', { month: 'long' }) }} />
|
||||
);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts components prop', () => {
|
||||
const { container } = render(<Calendar components={{}} />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
40
ui/src/components/ui/card.test.tsx
Normal file
40
ui/src/components/ui/card.test.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@/test/test-utils';
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './card';
|
||||
|
||||
describe('Card', () => {
|
||||
it('renders children', () => {
|
||||
render(<Card>Card content</Card>);
|
||||
expect(screen.getByText('Card content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Card className="custom-card">Content</Card>);
|
||||
expect(container.firstChild).toHaveClass('custom-card');
|
||||
});
|
||||
|
||||
it('renders CardHeader with padding', () => {
|
||||
const { container } = render(<CardHeader>Header</CardHeader>);
|
||||
expect(container.firstChild).toHaveClass('p-6');
|
||||
});
|
||||
|
||||
it('renders CardTitle as h3', () => {
|
||||
render(<CardTitle>Title</CardTitle>);
|
||||
expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent('Title');
|
||||
});
|
||||
|
||||
it('renders CardDescription', () => {
|
||||
render(<CardDescription>Desc</CardDescription>);
|
||||
expect(screen.getByText('Desc')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders CardContent', () => {
|
||||
render(<CardContent>Body</CardContent>);
|
||||
expect(screen.getByText('Body')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders CardFooter', () => {
|
||||
render(<CardFooter>Footer</CardFooter>);
|
||||
expect(screen.getByText('Footer')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
28
ui/src/components/ui/checkbox.test.tsx
Normal file
28
ui/src/components/ui/checkbox.test.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, waitFor } from '@/test/test-utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Checkbox } from './checkbox';
|
||||
|
||||
describe('Checkbox', () => {
|
||||
it('renders unchecked by default', () => {
|
||||
render(<Checkbox aria-label="Accept terms" />);
|
||||
expect(screen.getByRole('checkbox')).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('renders checked when checked prop is true', () => {
|
||||
render(<Checkbox checked aria-label="Accept terms" />);
|
||||
expect(screen.getByRole('checkbox')).toBeChecked();
|
||||
});
|
||||
|
||||
it('handles onCheckedChange events', async () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Checkbox onCheckedChange={handleChange} aria-label="Accept terms" />);
|
||||
await userEvent.click(screen.getByRole('checkbox'));
|
||||
await waitFor(() => expect(handleChange).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it('is disabled when disabled prop is true', () => {
|
||||
render(<Checkbox disabled aria-label="Accept terms" />);
|
||||
expect(screen.getByRole('checkbox')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
69
ui/src/components/ui/choice-chips.test.tsx
Normal file
69
ui/src/components/ui/choice-chips.test.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@/test/test-utils';
|
||||
import { ChoiceChips } from './choice-chips';
|
||||
|
||||
describe('ChoiceChips', () => {
|
||||
const defaultProps = {
|
||||
options: [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
{ value: 'option3', label: 'Option 3' },
|
||||
],
|
||||
value: 'option1',
|
||||
onChange: vi.fn(),
|
||||
};
|
||||
|
||||
it('renders all options', () => {
|
||||
render(<ChoiceChips {...defaultProps} />);
|
||||
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option 3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders selected option with primary styles', () => {
|
||||
const { container } = render(<ChoiceChips {...defaultProps} />);
|
||||
const selectedButton = screen.getByText('Option 1');
|
||||
expect(selectedButton).toHaveClass('bg-primary');
|
||||
expect(selectedButton).toHaveClass('text-primary-foreground');
|
||||
});
|
||||
|
||||
it('renders unselected options with secondary styles', () => {
|
||||
const { container } = render(<ChoiceChips {...defaultProps} />);
|
||||
const unselectedButton = screen.getByText('Option 2');
|
||||
expect(unselectedButton).toHaveClass('bg-secondary');
|
||||
expect(unselectedButton).toHaveClass('text-secondary-foreground');
|
||||
});
|
||||
|
||||
it('calls onChange when clicking an option', () => {
|
||||
const onChange = vi.fn();
|
||||
render(<ChoiceChips {...defaultProps} onChange={onChange} />);
|
||||
|
||||
fireEvent.click(screen.getByText('Option 2'));
|
||||
expect(onChange).toHaveBeenCalledWith('option2');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<ChoiceChips {...defaultProps} className="custom-class" />
|
||||
);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('handles empty options array', () => {
|
||||
const { container } = render(
|
||||
<ChoiceChips options={[]} value="" onChange={vi.fn()} />
|
||||
);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles single option', () => {
|
||||
render(
|
||||
<ChoiceChips
|
||||
options={[{ value: 'only', label: 'Only Option' }]}
|
||||
value="only"
|
||||
onChange={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('Only Option')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
25
ui/src/components/ui/collapsible.test.tsx
Normal file
25
ui/src/components/ui/collapsible.test.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@/test/test-utils';
|
||||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from './collapsible';
|
||||
|
||||
describe('Collapsible', () => {
|
||||
it('renders trigger and content', () => {
|
||||
render(
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger>Toggle</CollapsibleTrigger>
|
||||
<CollapsibleContent>Hidden content</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
expect(screen.getByText('Toggle')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows content when open', () => {
|
||||
render(
|
||||
<Collapsible open>
|
||||
<CollapsibleTrigger>Toggle</CollapsibleTrigger>
|
||||
<CollapsibleContent>Visible content</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
expect(screen.getByText('Visible content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
38
ui/src/components/ui/dialog.test.tsx
Normal file
38
ui/src/components/ui/dialog.test.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@/test/test-utils';
|
||||
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from './dialog';
|
||||
|
||||
describe('Dialog', () => {
|
||||
it('renders dialog trigger', () => {
|
||||
render(
|
||||
<Dialog>
|
||||
<DialogTrigger>Open</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Title</DialogTitle>
|
||||
<DialogDescription>Desc</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
expect(screen.getByText('Open')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders dialog content when open', () => {
|
||||
render(
|
||||
<Dialog open>
|
||||
<DialogTrigger>Open</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Dialog Title</DialogTitle>
|
||||
<DialogDescription>Dialog description</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>Footer</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
expect(screen.getByText('Dialog Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Dialog description')).toBeInTheDocument();
|
||||
expect(screen.getByText('Footer')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
71
ui/src/components/ui/dropdown-menu.test.tsx
Normal file
71
ui/src/components/ui/dropdown-menu.test.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@/test/test-utils';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
} from './dropdown-menu';
|
||||
|
||||
describe('DropdownMenu', () => {
|
||||
it('renders trigger', () => {
|
||||
render(
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Item</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByText('Open')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders content when open', () => {
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>Label</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>Item</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByText('Label')).toBeInTheDocument();
|
||||
expect(screen.getByText('Item')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders checkbox item', () => {
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuCheckboxItem checked>Check</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByText('Check')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders radio group', () => {
|
||||
render(
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuRadioGroup value="a">
|
||||
<DropdownMenuRadioItem value="a">A</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
expect(screen.getByText('A')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
41
ui/src/components/ui/input.test.tsx
Normal file
41
ui/src/components/ui/input.test.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen } from '@/test/test-utils';
|
||||
import { Input } from './input';
|
||||
|
||||
describe('Input', () => {
|
||||
it('renders with placeholder', () => {
|
||||
render(<Input placeholder="Enter text" />);
|
||||
expect(screen.getByPlaceholderText('Enter text')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts and displays value', () => {
|
||||
render(<Input value="test value" readOnly />);
|
||||
expect(screen.getByDisplayValue('test value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles onChange events', () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Input onChange={handleChange} />);
|
||||
const input = screen.getByRole('textbox');
|
||||
input.click();
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('is disabled when disabled prop is true', () => {
|
||||
render(<Input disabled />);
|
||||
expect(screen.getByRole('textbox')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('renders different input types', () => {
|
||||
const { rerender } = render(<Input type="text" />);
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
|
||||
rerender(<Input type="password" aria-label="Password" />);
|
||||
expect(screen.getByLabelText('Password')).toHaveAttribute('type', 'password');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Input className="custom-class" />);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
143
ui/src/components/ui/json-editor.test.tsx
Normal file
143
ui/src/components/ui/json-editor.test.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@/test/test-utils';
|
||||
import { JsonEditor, validateJson } from './json-editor';
|
||||
|
||||
describe('JsonEditor', () => {
|
||||
const defaultProps = {
|
||||
value: '{"key": "value"}',
|
||||
onChange: vi.fn(),
|
||||
};
|
||||
|
||||
it('renders with value', () => {
|
||||
render(<JsonEditor {...defaultProps} />);
|
||||
expect(screen.getByDisplayValue('{"key": "value"}')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with label', () => {
|
||||
render(<JsonEditor {...defaultProps} label="JSON Config" />);
|
||||
expect(screen.getByText('JSON Config')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with description', () => {
|
||||
render(<JsonEditor {...defaultProps} description="Enter JSON configuration" />);
|
||||
expect(screen.getByText('Enter JSON configuration')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onChange when value changes', () => {
|
||||
const onChange = vi.fn();
|
||||
render(<JsonEditor {...defaultProps} onChange={onChange} />);
|
||||
|
||||
const textarea = screen.getByDisplayValue('{"key": "value"}');
|
||||
fireEvent.change(textarea, { target: { value: '{"new": "value"}' } });
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('{"new": "value"}');
|
||||
});
|
||||
|
||||
it('shows error message for invalid JSON', () => {
|
||||
render(<JsonEditor {...defaultProps} value="invalid json" error="Invalid JSON" />);
|
||||
expect(screen.getByText('Invalid JSON')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders copy button when showCopyButton is true', () => {
|
||||
render(<JsonEditor {...defaultProps} showCopyButton />);
|
||||
expect(screen.getByRole('button', { name: /copy/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<JsonEditor {...defaultProps} className="custom-class" />
|
||||
);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('applies custom minHeight', () => {
|
||||
render(<JsonEditor {...defaultProps} minHeight="200px" />);
|
||||
const textarea = screen.getByDisplayValue('{"key": "value"}');
|
||||
expect(textarea).toHaveStyle({ minHeight: '200px' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateJson', () => {
|
||||
it('returns valid for empty string', () => {
|
||||
const result = validateJson('');
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('returns valid for empty object', () => {
|
||||
const result = validateJson('{}');
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.parsed).toEqual({});
|
||||
});
|
||||
|
||||
it('returns valid for empty array', () => {
|
||||
const result = validateJson('[]');
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.parsed).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns valid for valid JSON object', () => {
|
||||
const result = validateJson('{"key": "value"}');
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.parsed).toEqual({ key: 'value' });
|
||||
});
|
||||
|
||||
it('returns valid for valid JSON array', () => {
|
||||
const result = validateJson('["a", "b", "c"]');
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.parsed).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('returns invalid for invalid JSON', () => {
|
||||
const result = validateJson('{invalid}');
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('detects unquoted template variables', () => {
|
||||
const result = validateJson('{"key": {{variable}}}');
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('Template variables must be quoted');
|
||||
});
|
||||
|
||||
it('detects trailing comma', () => {
|
||||
const result = validateJson('{"key": "value",}');
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('Trailing comma');
|
||||
});
|
||||
|
||||
it('detects single quotes', () => {
|
||||
const result = validateJson("{'key': 'value'}");
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('double quotes');
|
||||
});
|
||||
|
||||
it('detects unquoted string values', () => {
|
||||
const result = validateJson('{key: value}');
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('quoted');
|
||||
});
|
||||
|
||||
it('handles numbers', () => {
|
||||
const result = validateJson('{"count": 42}');
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.parsed).toEqual({ count: 42 });
|
||||
});
|
||||
|
||||
it('handles booleans', () => {
|
||||
const result = validateJson('{"enabled": true}');
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.parsed).toEqual({ enabled: true });
|
||||
});
|
||||
|
||||
it('handles null', () => {
|
||||
const result = validateJson('{"value": null}');
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.parsed).toEqual({ value: null });
|
||||
});
|
||||
|
||||
it('handles nested objects', () => {
|
||||
const result = validateJson('{"outer": {"inner": "value"}}');
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.parsed).toEqual({ outer: { inner: 'value' } });
|
||||
});
|
||||
});
|
||||
20
ui/src/components/ui/label.test.tsx
Normal file
20
ui/src/components/ui/label.test.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@/test/test-utils';
|
||||
import { Label } from './label';
|
||||
|
||||
describe('Label', () => {
|
||||
it('renders children text', () => {
|
||||
render(<Label>Email</Label>);
|
||||
expect(screen.getByText('Email')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('associates with input via htmlFor', () => {
|
||||
render(<Label htmlFor="email-input">Email</Label>);
|
||||
expect(screen.getByText('Email')).toHaveAttribute('for', 'email-input');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Label className="custom-label">Text</Label>);
|
||||
expect(container.firstChild).toHaveClass('custom-label');
|
||||
});
|
||||
});
|
||||
25
ui/src/components/ui/popover.test.tsx
Normal file
25
ui/src/components/ui/popover.test.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@/test/test-utils';
|
||||
import { Popover, PopoverTrigger, PopoverContent } from './popover';
|
||||
|
||||
describe('Popover', () => {
|
||||
it('renders trigger', () => {
|
||||
render(
|
||||
<Popover>
|
||||
<PopoverTrigger>Click me</PopoverTrigger>
|
||||
<PopoverContent>Popover content</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
expect(screen.getByText('Click me')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders content when open', () => {
|
||||
render(
|
||||
<Popover open>
|
||||
<PopoverTrigger>Click</PopoverTrigger>
|
||||
<PopoverContent>Content</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
expect(screen.getByText('Content')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
28
ui/src/components/ui/progress.test.tsx
Normal file
28
ui/src/components/ui/progress.test.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { render } from '@/test/test-utils';
|
||||
import { Progress } from './progress';
|
||||
|
||||
describe('Progress', () => {
|
||||
it('renders with default value', () => {
|
||||
const { container } = render(<Progress value={50} />);
|
||||
const indicator = container.querySelector('[data-slot="progress-indicator"]');
|
||||
expect(indicator).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Progress value={30} className="custom-progress" />);
|
||||
expect(container.firstChild).toHaveClass('custom-progress');
|
||||
});
|
||||
|
||||
it('renders with zero value', () => {
|
||||
const { container } = render(<Progress value={0} />);
|
||||
const indicator = container.querySelector('[data-slot="progress-indicator"]');
|
||||
expect(indicator).toHaveStyle('transform: translateX(-100%)');
|
||||
});
|
||||
|
||||
it('renders with full value', () => {
|
||||
const { container } = render(<Progress value={100} />);
|
||||
const indicator = container.querySelector('[data-slot="progress-indicator"]');
|
||||
expect(indicator).toHaveStyle('transform: translateX(-0%)');
|
||||
});
|
||||
});
|
||||
36
ui/src/components/ui/radio-group.test.tsx
Normal file
36
ui/src/components/ui/radio-group.test.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, waitFor } from '@/test/test-utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { RadioGroup, RadioGroupItem } from './radio-group';
|
||||
|
||||
describe('RadioGroup', () => {
|
||||
it('renders radio items', () => {
|
||||
render(
|
||||
<RadioGroup>
|
||||
<RadioGroupItem value="a" aria-label="Option A" />
|
||||
<RadioGroupItem value="b" aria-label="Option B" />
|
||||
</RadioGroup>
|
||||
);
|
||||
expect(screen.getAllByRole('radio')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('checks item when clicked', async () => {
|
||||
const handleChange = vi.fn();
|
||||
render(
|
||||
<RadioGroup onValueChange={handleChange}>
|
||||
<RadioGroupItem value="a" aria-label="Option A" />
|
||||
</RadioGroup>
|
||||
);
|
||||
await userEvent.click(screen.getByRole('radio'));
|
||||
await waitFor(() => expect(handleChange).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<RadioGroup className="custom-group">
|
||||
<RadioGroupItem value="a" aria-label="A" />
|
||||
</RadioGroup>
|
||||
);
|
||||
expect(container.firstChild).toHaveClass('custom-group');
|
||||
});
|
||||
});
|
||||
48
ui/src/components/ui/select.test.tsx
Normal file
48
ui/src/components/ui/select.test.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@/test/test-utils';
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectLabel } from './select';
|
||||
|
||||
describe('Select', () => {
|
||||
it('renders select trigger with placeholder', () => {
|
||||
render(
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Choose an option" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="a">Option A</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
expect(screen.getByText('Choose an option')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders select with value', () => {
|
||||
render(
|
||||
<Select value="a">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="a">Option A</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
expect(screen.getByText('Option A')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders select group with label when open', () => {
|
||||
render(
|
||||
<Select open>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Group</SelectLabel>
|
||||
<SelectItem value="a">A</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
expect(screen.getByText('Group')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
25
ui/src/components/ui/separator.test.tsx
Normal file
25
ui/src/components/ui/separator.test.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { render } from '@/test/test-utils';
|
||||
import { Separator } from './separator';
|
||||
|
||||
describe('Separator', () => {
|
||||
it('renders horizontal separator by default', () => {
|
||||
const { container } = render(<Separator />);
|
||||
expect(container.firstChild).toHaveAttribute('data-orientation', 'horizontal');
|
||||
});
|
||||
|
||||
it('renders vertical separator', () => {
|
||||
const { container } = render(<Separator orientation="vertical" />);
|
||||
expect(container.firstChild).toHaveAttribute('data-orientation', 'vertical');
|
||||
});
|
||||
|
||||
it('has data-orientation attribute', () => {
|
||||
const { container } = render(<Separator />);
|
||||
expect(container.firstChild).toHaveAttribute('data-orientation', 'horizontal');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Separator className="custom-sep" />);
|
||||
expect(container.firstChild).toHaveClass('custom-sep');
|
||||
});
|
||||
});
|
||||
37
ui/src/components/ui/sheet.test.tsx
Normal file
37
ui/src/components/ui/sheet.test.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@/test/test-utils';
|
||||
import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter } from './sheet';
|
||||
|
||||
describe('Sheet', () => {
|
||||
it('renders trigger', () => {
|
||||
render(
|
||||
<Sheet>
|
||||
<SheetTrigger>Open</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Title</SheetTitle>
|
||||
<SheetDescription>Desc</SheetDescription>
|
||||
</SheetHeader>
|
||||
<SheetFooter>Footer</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
expect(screen.getByText('Open')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders content when open', () => {
|
||||
render(
|
||||
<Sheet open>
|
||||
<SheetTrigger>Open</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Sheet Title</SheetTitle>
|
||||
<SheetDescription>Sheet description</SheetDescription>
|
||||
</SheetHeader>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
expect(screen.getByText('Sheet Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Sheet description')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
134
ui/src/components/ui/sidebar.test.tsx
Normal file
134
ui/src/components/ui/sidebar.test.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
SidebarInset
|
||||
} from './sidebar';
|
||||
|
||||
describe('Sidebar', () => {
|
||||
it('exports all expected components', () => {
|
||||
expect(Sidebar).toBeDefined();
|
||||
expect(SidebarContent).toBeDefined();
|
||||
expect(SidebarFooter).toBeDefined();
|
||||
expect(SidebarGroup).toBeDefined();
|
||||
expect(SidebarGroupContent).toBeDefined();
|
||||
expect(SidebarGroupLabel).toBeDefined();
|
||||
expect(SidebarHeader).toBeDefined();
|
||||
expect(SidebarInput).toBeDefined();
|
||||
expect(SidebarMenu).toBeDefined();
|
||||
expect(SidebarMenuAction).toBeDefined();
|
||||
expect(SidebarMenuButton).toBeDefined();
|
||||
expect(SidebarMenuItem).toBeDefined();
|
||||
expect(SidebarMenuSkeleton).toBeDefined();
|
||||
expect(SidebarMenuSub).toBeDefined();
|
||||
expect(SidebarMenuSubButton).toBeDefined();
|
||||
expect(SidebarMenuSubItem).toBeDefined();
|
||||
expect(SidebarProvider).toBeDefined();
|
||||
expect(SidebarSeparator).toBeDefined();
|
||||
expect(SidebarTrigger).toBeDefined();
|
||||
expect(SidebarInset).toBeDefined();
|
||||
});
|
||||
|
||||
it('exports useSidebar hook', () => {
|
||||
expect(useSidebar).toBeDefined();
|
||||
expect(typeof useSidebar).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarProvider is a function component', () => {
|
||||
expect(typeof SidebarProvider).toBe('function');
|
||||
});
|
||||
|
||||
it('Sidebar is a function component', () => {
|
||||
expect(typeof Sidebar).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarContent is a function component', () => {
|
||||
expect(typeof SidebarContent).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarHeader is a function component', () => {
|
||||
expect(typeof SidebarHeader).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarFooter is a function component', () => {
|
||||
expect(typeof SidebarFooter).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarGroup is a function component', () => {
|
||||
expect(typeof SidebarGroup).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarGroupLabel is a function component', () => {
|
||||
expect(typeof SidebarGroupLabel).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarGroupContent is a function component', () => {
|
||||
expect(typeof SidebarGroupContent).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarMenu is a function component', () => {
|
||||
expect(typeof SidebarMenu).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarMenuItem is a function component', () => {
|
||||
expect(typeof SidebarMenuItem).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarMenuButton is a function component', () => {
|
||||
expect(typeof SidebarMenuButton).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarMenuSkeleton is a function component', () => {
|
||||
expect(typeof SidebarMenuSkeleton).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarSeparator is a function component', () => {
|
||||
expect(typeof SidebarSeparator).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarTrigger is a function component', () => {
|
||||
expect(typeof SidebarTrigger).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarInput is a function component', () => {
|
||||
expect(typeof SidebarInput).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarInset is a function component', () => {
|
||||
expect(typeof SidebarInset).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarMenuAction is a function component', () => {
|
||||
expect(typeof SidebarMenuAction).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarMenuSub is a function component', () => {
|
||||
expect(typeof SidebarMenuSub).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarMenuSubButton is a function component', () => {
|
||||
expect(typeof SidebarMenuSubButton).toBe('function');
|
||||
});
|
||||
|
||||
it('SidebarMenuSubItem is a function component', () => {
|
||||
expect(typeof SidebarMenuSubItem).toBe('function');
|
||||
});
|
||||
});
|
||||
20
ui/src/components/ui/skeleton.test.tsx
Normal file
20
ui/src/components/ui/skeleton.test.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { render } from '@/test/test-utils';
|
||||
import { Skeleton } from './skeleton';
|
||||
|
||||
describe('Skeleton', () => {
|
||||
it('renders a div with animate-pulse', () => {
|
||||
const { container } = render(<Skeleton />);
|
||||
expect(container.firstChild).toHaveClass('animate-pulse');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Skeleton className="custom-skeleton" />);
|
||||
expect(container.firstChild).toHaveClass('custom-skeleton');
|
||||
});
|
||||
|
||||
it('renders children', () => {
|
||||
render(<Skeleton>Loading</Skeleton>);
|
||||
expect(document.querySelector('[data-slot="skeleton"]')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
16
ui/src/components/ui/sonner.test.tsx
Normal file
16
ui/src/components/ui/sonner.test.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { Toaster } from './sonner';
|
||||
|
||||
// Note: sonner uses next-themes which requires a ThemeProvider wrapper
|
||||
// These tests verify the component exports correctly
|
||||
|
||||
describe('Toaster', () => {
|
||||
it('exports Toaster component', () => {
|
||||
// Verify the named export exists
|
||||
expect(Toaster).toBeDefined();
|
||||
});
|
||||
|
||||
it('Toaster is a function component', () => {
|
||||
expect(typeof Toaster).toBe('function');
|
||||
});
|
||||
});
|
||||
28
ui/src/components/ui/switch.test.tsx
Normal file
28
ui/src/components/ui/switch.test.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, waitFor } from '@/test/test-utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Switch } from './switch';
|
||||
|
||||
describe('Switch', () => {
|
||||
it('renders unchecked by default', () => {
|
||||
render(<Switch aria-label="Toggle" />);
|
||||
expect(screen.getByRole('switch')).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('renders checked when checked prop is true', () => {
|
||||
render(<Switch checked aria-label="Toggle" />);
|
||||
expect(screen.getByRole('switch')).toBeChecked();
|
||||
});
|
||||
|
||||
it('handles onCheckedChange events', async () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Switch onCheckedChange={handleChange} aria-label="Toggle" />);
|
||||
await userEvent.click(screen.getByRole('switch'));
|
||||
await waitFor(() => expect(handleChange).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it('is disabled when disabled prop is true', () => {
|
||||
render(<Switch disabled aria-label="Toggle" />);
|
||||
expect(screen.getByRole('switch')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
53
ui/src/components/ui/table.test.tsx
Normal file
53
ui/src/components/ui/table.test.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@/test/test-utils';
|
||||
import { Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption } from './table';
|
||||
|
||||
describe('Table', () => {
|
||||
it('renders table with header, body, and rows', () => {
|
||||
render(
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Age</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Alice</TableCell>
|
||||
<TableCell>30</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
expect(screen.getByText('Name')).toBeInTheDocument();
|
||||
expect(screen.getByText('Alice')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders table caption', () => {
|
||||
render(
|
||||
<Table>
|
||||
<TableCaption>User list</TableCaption>
|
||||
<TableBody><TableRow><TableCell>Data</TableCell></TableRow></TableBody>
|
||||
</Table>
|
||||
);
|
||||
expect(screen.getByText('User list')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders table footer', () => {
|
||||
render(
|
||||
<Table>
|
||||
<TableFooter>
|
||||
<TableRow><TableCell>Total</TableCell></TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
);
|
||||
expect(screen.getByText('Total')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Table className="custom-table" />);
|
||||
expect(container.querySelector('table')).toHaveClass('custom-table');
|
||||
});
|
||||
});
|
||||
43
ui/src/components/ui/tabs.test.tsx
Normal file
43
ui/src/components/ui/tabs.test.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@/test/test-utils';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs';
|
||||
|
||||
describe('Tabs', () => {
|
||||
it('renders tabs with list and content', () => {
|
||||
render(
|
||||
<Tabs defaultValue="tab1">
|
||||
<TabsList>
|
||||
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
||||
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="tab1">Content 1</TabsContent>
|
||||
<TabsContent value="tab2">Content 2</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
expect(screen.getByRole('tablist')).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Tab 1' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Tab 2' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows default tab content', () => {
|
||||
render(
|
||||
<Tabs defaultValue="tab1">
|
||||
<TabsList>
|
||||
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="tab1">Content 1</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
expect(screen.getByText('Content 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies custom className to Tabs', () => {
|
||||
const { container } = render(
|
||||
<Tabs className="custom-tabs" defaultValue="tab1">
|
||||
<TabsList><TabsTrigger value="tab1">Tab</TabsTrigger></TabsList>
|
||||
<TabsContent value="tab1">Content</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
expect(container.firstChild).toHaveClass('custom-tabs');
|
||||
});
|
||||
});
|
||||
33
ui/src/components/ui/textarea.test.tsx
Normal file
33
ui/src/components/ui/textarea.test.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen } from '@/test/test-utils';
|
||||
import { Textarea } from './textarea';
|
||||
|
||||
describe('Textarea', () => {
|
||||
it('renders with placeholder', () => {
|
||||
render(<Textarea placeholder="Enter description" />);
|
||||
expect(screen.getByPlaceholderText('Enter description')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts and displays value', () => {
|
||||
render(<Textarea value="test content" readOnly />);
|
||||
expect(screen.getByDisplayValue('test content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles onChange events', () => {
|
||||
const handleChange = vi.fn();
|
||||
render(<Textarea onChange={handleChange} />);
|
||||
const textarea = screen.getByRole('textbox');
|
||||
textarea.click();
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('is disabled when disabled prop is true', () => {
|
||||
render(<Textarea disabled />);
|
||||
expect(screen.getByRole('textbox')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Textarea className="custom-textarea" />);
|
||||
expect(container.firstChild).toHaveClass('custom-textarea');
|
||||
});
|
||||
});
|
||||
25
ui/src/components/ui/tooltip.test.tsx
Normal file
25
ui/src/components/ui/tooltip.test.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen } from '@/test/test-utils';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from './tooltip';
|
||||
|
||||
describe('Tooltip', () => {
|
||||
it('renders tooltip trigger', () => {
|
||||
render(
|
||||
<Tooltip>
|
||||
<TooltipTrigger>Hover me</TooltipTrigger>
|
||||
<TooltipContent>Tooltip text</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
expect(screen.getByText('Hover me')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders tooltip content when open', () => {
|
||||
render(
|
||||
<Tooltip open>
|
||||
<TooltipTrigger>Trigger</TooltipTrigger>
|
||||
<TooltipContent>Tooltip text</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
expect(screen.getByRole('tooltip')).toHaveTextContent('Tooltip text');
|
||||
});
|
||||
});
|
||||
40
ui/src/context/UserConfigContext.test.tsx
Normal file
40
ui/src/context/UserConfigContext.test.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { UserConfigProvider, useUserConfig } from './UserConfigContext';
|
||||
|
||||
describe('UserConfigContext', () => {
|
||||
it('exports UserConfigProvider component', () => {
|
||||
expect(UserConfigProvider).toBeDefined();
|
||||
expect(typeof UserConfigProvider).toBe('function');
|
||||
});
|
||||
|
||||
it('exports useUserConfig hook', () => {
|
||||
expect(useUserConfig).toBeDefined();
|
||||
expect(typeof useUserConfig).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Other Contexts', () => {
|
||||
it('AppConfigContext exports provider and hook', async () => {
|
||||
const module = await import('./AppConfigContext');
|
||||
expect(module.AppConfigProvider).toBeDefined();
|
||||
expect(module.useAppConfig).toBeDefined();
|
||||
});
|
||||
|
||||
it('OnboardingContext exports provider and hook', async () => {
|
||||
const module = await import('./OnboardingContext');
|
||||
expect(module.OnboardingProvider).toBeDefined();
|
||||
expect(module.useOnboarding).toBeDefined();
|
||||
});
|
||||
|
||||
it('UnsavedChangesContext exports provider and hook', async () => {
|
||||
const module = await import('./UnsavedChangesContext');
|
||||
expect(module.UnsavedChangesProvider).toBeDefined();
|
||||
expect(module.useUnsavedChanges).toBeDefined();
|
||||
});
|
||||
|
||||
it('TelephonyConfigWarningsContext exports provider and hook', async () => {
|
||||
const module = await import('./TelephonyConfigWarningsContext');
|
||||
expect(module.TelephonyConfigWarningsProvider).toBeDefined();
|
||||
expect(module.useTelephonyConfigWarnings).toBeDefined();
|
||||
});
|
||||
});
|
||||
58
ui/src/hooks/use-mobile.test.ts
Normal file
58
ui/src/hooks/use-mobile.test.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useIsMobile } from './use-mobile';
|
||||
|
||||
describe('useIsMobile', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns false when viewport is larger than breakpoint', () => {
|
||||
// Mock matchMedia to return false (desktop)
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
// Mock innerWidth
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
writable: true,
|
||||
value: 1024,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsMobile());
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when viewport is smaller than breakpoint', () => {
|
||||
// Mock matchMedia to return true (mobile)
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation(query => ({
|
||||
matches: true,
|
||||
media: query,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
// Mock innerWidth
|
||||
Object.defineProperty(window, 'innerWidth', {
|
||||
writable: true,
|
||||
value: 375,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIsMobile());
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('is exported as a function', () => {
|
||||
expect(useIsMobile).toBeDefined();
|
||||
expect(typeof useIsMobile).toBe('function');
|
||||
});
|
||||
});
|
||||
291
ui/src/lib/filters.test.ts
Normal file
291
ui/src/lib/filters.test.ts
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
getDefaultValue,
|
||||
validateFilter,
|
||||
encodeFiltersToURL,
|
||||
decodeFiltersFromURL,
|
||||
formatDateRange,
|
||||
formatNumberRange,
|
||||
getDatePresetValue
|
||||
} from './filters';
|
||||
import {
|
||||
ActiveFilter,
|
||||
DateRangeValue,
|
||||
MultiSelectValue,
|
||||
NumberRangeValue,
|
||||
FilterAttribute
|
||||
} from '@/types/filters';
|
||||
|
||||
describe('filters', () => {
|
||||
describe('getDefaultValue', () => {
|
||||
it('returns { from: null, to: null } for dateRange', () => {
|
||||
expect(getDefaultValue('dateRange')).toEqual({ from: null, to: null });
|
||||
});
|
||||
|
||||
it('returns { codes: [] } for multiSelect', () => {
|
||||
expect(getDefaultValue('multiSelect')).toEqual({ codes: [] });
|
||||
});
|
||||
|
||||
it('returns { value: null } for number', () => {
|
||||
expect(getDefaultValue('number')).toEqual({ value: null });
|
||||
});
|
||||
|
||||
it('returns { min: null, max: null } for numberRange', () => {
|
||||
expect(getDefaultValue('numberRange')).toEqual({ min: null, max: null });
|
||||
});
|
||||
|
||||
it('returns { status: "all" } for radio', () => {
|
||||
expect(getDefaultValue('radio')).toEqual({ status: 'all' });
|
||||
});
|
||||
|
||||
it('returns { codes: [] } for tags', () => {
|
||||
expect(getDefaultValue('tags')).toEqual({ codes: [] });
|
||||
});
|
||||
|
||||
it('returns { value: "" } for text', () => {
|
||||
expect(getDefaultValue('text')).toEqual({ value: '' });
|
||||
});
|
||||
|
||||
it('throws error for unknown type', () => {
|
||||
expect(() => getDefaultValue('unknown' as any)).toThrow('Unknown filter type: unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateFilter', () => {
|
||||
const createFilter = (type: FilterAttribute['type'], value: any, config: any = {}): ActiveFilter => ({
|
||||
attribute: {
|
||||
id: 'test',
|
||||
type,
|
||||
label: 'Test',
|
||||
config,
|
||||
},
|
||||
value,
|
||||
isValid: true,
|
||||
});
|
||||
|
||||
describe('dateRange', () => {
|
||||
it('returns error when from is missing', () => {
|
||||
const filter = createFilter('dateRange', { from: null, to: new Date() });
|
||||
expect(validateFilter(filter)).toBe('Both dates are required');
|
||||
});
|
||||
|
||||
it('returns error when to is missing', () => {
|
||||
const filter = createFilter('dateRange', { from: new Date(), to: null });
|
||||
expect(validateFilter(filter)).toBe('Both dates are required');
|
||||
});
|
||||
|
||||
it('returns error when to is before from', () => {
|
||||
const from = new Date('2024-01-15');
|
||||
const to = new Date('2024-01-10');
|
||||
const filter = createFilter('dateRange', { from, to });
|
||||
expect(validateFilter(filter)).toBe('End date must be after start date');
|
||||
});
|
||||
|
||||
it('returns error when range exceeds maxRangeDays', () => {
|
||||
const from = new Date('2024-01-01');
|
||||
const to = new Date('2024-02-01');
|
||||
const filter = createFilter('dateRange', { from, to }, { maxRangeDays: 30 });
|
||||
expect(validateFilter(filter)).toBe('Date range cannot exceed 30 days');
|
||||
});
|
||||
|
||||
it('returns null for valid date range', () => {
|
||||
const from = new Date('2024-01-01');
|
||||
const to = new Date('2024-01-15');
|
||||
const filter = createFilter('dateRange', { from, to });
|
||||
expect(validateFilter(filter)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiSelect', () => {
|
||||
it('returns error when no codes selected', () => {
|
||||
const filter = createFilter('multiSelect', { codes: [] });
|
||||
expect(validateFilter(filter)).toBe('At least one option must be selected');
|
||||
});
|
||||
|
||||
it('returns error when exceeds maxSelections', () => {
|
||||
const filter = createFilter('multiSelect', { codes: ['a', 'b', 'c'] }, { maxSelections: 2 });
|
||||
expect(validateFilter(filter)).toBe('Cannot select more than 2 options');
|
||||
});
|
||||
|
||||
it('returns null for valid multiSelect', () => {
|
||||
const filter = createFilter('multiSelect', { codes: ['a', 'b'] });
|
||||
expect(validateFilter(filter)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('numberRange', () => {
|
||||
it('returns error when min is greater than max', () => {
|
||||
const filter = createFilter('numberRange', { min: 100, max: 50 });
|
||||
expect(validateFilter(filter)).toBe('Minimum must be less than maximum');
|
||||
});
|
||||
|
||||
it('returns null for valid numberRange', () => {
|
||||
const filter = createFilter('numberRange', { min: 10, max: 100 });
|
||||
expect(validateFilter(filter)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns error when min and max are null', () => {
|
||||
const filter = createFilter('numberRange', { min: null, max: null });
|
||||
expect(validateFilter(filter)).toBe('Both values are required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('number', () => {
|
||||
it('returns error when value is below min', () => {
|
||||
const filter = createFilter('number', { value: 5 }, { min: 10, max: 100 });
|
||||
expect(validateFilter(filter)).toBe('Value cannot be less than 10');
|
||||
});
|
||||
|
||||
it('returns error when value is above max', () => {
|
||||
const filter = createFilter('number', { value: 150 }, { min: 10, max: 100 });
|
||||
expect(validateFilter(filter)).toBe('Value cannot be greater than 100');
|
||||
});
|
||||
|
||||
it('returns null for valid number', () => {
|
||||
const filter = createFilter('number', { value: 50 }, { min: 10, max: 100 });
|
||||
expect(validateFilter(filter)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns error when value is null', () => {
|
||||
const filter = createFilter('number', { value: null });
|
||||
expect(validateFilter(filter)).toBe('A value is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('radio', () => {
|
||||
it('returns null for valid radio', () => {
|
||||
const filter = createFilter('radio', { status: 'completed' });
|
||||
expect(validateFilter(filter)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('text', () => {
|
||||
it('returns error when text is empty', () => {
|
||||
const filter = createFilter('text', { value: '' });
|
||||
expect(validateFilter(filter)).toBe('Text value is required');
|
||||
});
|
||||
|
||||
it('returns error when text is whitespace only', () => {
|
||||
const filter = createFilter('text', { value: ' ' });
|
||||
expect(validateFilter(filter)).toBe('Text value is required');
|
||||
});
|
||||
|
||||
it('returns null for valid text', () => {
|
||||
const filter = createFilter('text', { value: 'hello' });
|
||||
expect(validateFilter(filter)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('encodeFiltersToURL', () => {
|
||||
it('returns empty string for empty filters', () => {
|
||||
expect(encodeFiltersToURL([])).toBe('');
|
||||
});
|
||||
|
||||
it('encodes filters to URL params', () => {
|
||||
const filters: ActiveFilter[] = [
|
||||
{
|
||||
attribute: { id: 'date', type: 'dateRange', label: 'Date', config: {} },
|
||||
value: { from: new Date('2024-01-01'), to: new Date('2024-01-31') },
|
||||
isValid: true,
|
||||
},
|
||||
];
|
||||
const result = encodeFiltersToURL(filters);
|
||||
expect(result).toContain('filters=');
|
||||
});
|
||||
|
||||
it('encodes multiple filters', () => {
|
||||
const filters: ActiveFilter[] = [
|
||||
{
|
||||
attribute: { id: 'status', type: 'multiSelect', label: 'Status', config: { options: [] } },
|
||||
value: { codes: ['completed', 'failed'] },
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
attribute: { id: 'text', type: 'text', label: 'Text', config: {} },
|
||||
value: { value: 'hello' },
|
||||
isValid: true,
|
||||
},
|
||||
];
|
||||
const result = encodeFiltersToURL(filters);
|
||||
expect(result).toContain('filters=');
|
||||
// Should contain encoded JSON
|
||||
expect(decodeURIComponent(result)).toContain('status');
|
||||
expect(decodeURIComponent(result)).toContain('hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('decodeFiltersFromURL', () => {
|
||||
it('returns empty array for empty params', () => {
|
||||
const params = new URLSearchParams();
|
||||
const availableAttributes: FilterAttribute[] = [];
|
||||
expect(decodeFiltersFromURL(params, availableAttributes)).toEqual([]);
|
||||
});
|
||||
|
||||
it('decodes filters from URL', () => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('filters', JSON.stringify([{ id: 'test', value: { value: 'hello' } }]));
|
||||
const availableAttributes: FilterAttribute[] = [
|
||||
{ id: 'test', type: 'text', label: 'Test', config: {} }
|
||||
];
|
||||
const result = decodeFiltersFromURL(params, availableAttributes);
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].attribute.id).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDateRange', () => {
|
||||
it('returns "No date range selected" for null values', () => {
|
||||
expect(formatDateRange({ from: null, to: null })).toBe('No date range selected');
|
||||
});
|
||||
|
||||
it('formats date range correctly', () => {
|
||||
const value: DateRangeValue = {
|
||||
from: new Date('2024-01-15'),
|
||||
to: new Date('2024-01-20')
|
||||
};
|
||||
const result = formatDateRange(value);
|
||||
expect(result).toContain('2024');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatNumberRange', () => {
|
||||
it('returns "No range selected" for null values', () => {
|
||||
expect(formatNumberRange({ min: null, max: null })).toBe('No range selected');
|
||||
});
|
||||
|
||||
it('formats number range with unit', () => {
|
||||
const value: NumberRangeValue = { min: 10, max: 100 };
|
||||
const result = formatNumberRange(value, 'seconds');
|
||||
expect(result).toContain('10');
|
||||
expect(result).toContain('100');
|
||||
expect(result).toContain('seconds');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDatePresetValue', () => {
|
||||
it('returns valid date range for "today"', () => {
|
||||
const result = getDatePresetValue('today');
|
||||
expect(result.from).toBeInstanceOf(Date);
|
||||
expect(result.to).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('returns valid date range for "yesterday"', () => {
|
||||
const result = getDatePresetValue('yesterday');
|
||||
expect(result.from).toBeInstanceOf(Date);
|
||||
expect(result.to).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('returns valid date range for "last7days"', () => {
|
||||
const result = getDatePresetValue('last7days');
|
||||
expect(result.from).toBeInstanceOf(Date);
|
||||
expect(result.to).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('returns valid date range for "last30days"', () => {
|
||||
const result = getDatePresetValue('last30days');
|
||||
expect(result.from).toBeInstanceOf(Date);
|
||||
expect(result.to).toBeInstanceOf(Date);
|
||||
});
|
||||
});
|
||||
});
|
||||
81
ui/src/lib/utils.test.ts
Normal file
81
ui/src/lib/utils.test.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { cn, getRandomId, getNextNodeId, debounce } from './utils';
|
||||
|
||||
describe('cn', () => {
|
||||
it('merges class names with tailwind', () => {
|
||||
expect(cn('px-2', 'py-1')).toBe('px-2 py-1');
|
||||
});
|
||||
|
||||
it('handles conditional classes', () => {
|
||||
expect(cn('base', false && 'hidden', true && 'visible')).toBe('base visible');
|
||||
});
|
||||
|
||||
it('merges conflicting tailwind classes', () => {
|
||||
expect(cn('px-2', 'px-4')).toBe('px-4');
|
||||
});
|
||||
|
||||
it('handles empty inputs', () => {
|
||||
expect(cn()).toBe('');
|
||||
});
|
||||
|
||||
it('handles undefined and null values', () => {
|
||||
expect(cn('base', undefined, null, 'extra')).toBe('base extra');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRandomId', () => {
|
||||
it('returns a number between 0 and 9999', () => {
|
||||
const id = getRandomId();
|
||||
expect(id).toBeGreaterThanOrEqual(0);
|
||||
expect(id).toBeLessThan(10000);
|
||||
});
|
||||
|
||||
it('returns different values on multiple calls', () => {
|
||||
const ids = new Set(Array.from({ length: 20 }, getRandomId));
|
||||
expect(ids.size).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextNodeId', () => {
|
||||
it('returns "1" for empty array', () => {
|
||||
expect(getNextNodeId([])).toBe('1');
|
||||
});
|
||||
|
||||
it('returns next id after max existing', () => {
|
||||
expect(getNextNodeId([{ id: '1' }, { id: '3' }])).toBe('4');
|
||||
});
|
||||
|
||||
it('ignores non-numeric ids', () => {
|
||||
expect(getNextNodeId([{ id: 'abc' }, { id: '5' }])).toBe('6');
|
||||
});
|
||||
});
|
||||
|
||||
describe('debounce', () => {
|
||||
it('delays function execution', () => {
|
||||
vi.useFakeTimers();
|
||||
const fn = vi.fn();
|
||||
const debounced = debounce(fn, 100);
|
||||
|
||||
debounced();
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('cancels previous call on rapid invocations', () => {
|
||||
vi.useFakeTimers();
|
||||
const fn = vi.fn();
|
||||
const debounced = debounce(fn, 100);
|
||||
|
||||
debounced();
|
||||
debounced();
|
||||
debounced();
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
60
ui/src/test/mocks/handlers.ts
Normal file
60
ui/src/test/mocks/handlers.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
export const handlers = [
|
||||
// Auth
|
||||
http.get('/api/auth/session', () => {
|
||||
return HttpResponse.json({ user: { id: '1', email: 'test@example.com' } });
|
||||
}),
|
||||
|
||||
// App config
|
||||
http.get('/api/config/auth', () => {
|
||||
return HttpResponse.json({ provider: 'local' });
|
||||
}),
|
||||
|
||||
http.get('/api/config/version', () => {
|
||||
return HttpResponse.json({ version: '1.31.0' });
|
||||
}),
|
||||
|
||||
// User config
|
||||
http.get('/api/v1/user/configurations/user', () => {
|
||||
return HttpResponse.json({
|
||||
llm: { provider: 'openai', model: 'gpt-4.1', base_url: 'https://api.openai.com/v1' },
|
||||
tts: { provider: 'openai', model: 'gpt-4o-mini-tts' },
|
||||
stt: { provider: 'deepgram', model: 'nova-2' },
|
||||
embeddings: { provider: 'openai', model: 'text-embedding-3-small' },
|
||||
});
|
||||
}),
|
||||
|
||||
http.put('/api/v1/user/configurations/user', async () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
// Default configurations
|
||||
http.get('/api/v1/user/configurations/defaults', () => {
|
||||
return HttpResponse.json({
|
||||
llm: {
|
||||
openai: {
|
||||
properties: {
|
||||
provider: { const: 'openai', type: 'string' },
|
||||
api_key: { type: 'string' },
|
||||
model: { type: 'string', default: 'gpt-4.1' },
|
||||
base_url: { type: 'string', default: 'https://api.openai.com/v1' },
|
||||
},
|
||||
},
|
||||
},
|
||||
tts: {},
|
||||
stt: {},
|
||||
embeddings: {},
|
||||
default_providers: { llm: 'openai', tts: 'openai', stt: 'deepgram', embeddings: 'openai' },
|
||||
});
|
||||
}),
|
||||
|
||||
// Workflows
|
||||
http.get('/api/v1/workflows', () => {
|
||||
return HttpResponse.json({ items: [], total: 0 });
|
||||
}),
|
||||
|
||||
http.get('/api/v1/workflow/count', () => {
|
||||
return HttpResponse.json({ count: 0 });
|
||||
}),
|
||||
];
|
||||
4
ui/src/test/mocks/server.ts
Normal file
4
ui/src/test/mocks/server.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { setupServer } from 'msw/node';
|
||||
import { handlers } from './handlers';
|
||||
|
||||
export const server = setupServer(...handlers);
|
||||
21
ui/src/test/setup.ts
Normal file
21
ui/src/test/setup.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import '@testing-library/jest-dom/vitest';
|
||||
import { vi } from 'vitest';
|
||||
import { server } from './mocks/server';
|
||||
|
||||
// Mock ResizeObserver for Radix UI components in jsdom
|
||||
global.ResizeObserver = vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// Start MSW before all tests
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
|
||||
|
||||
// Reset handlers after each test
|
||||
afterEach(() => server.resetHandlers());
|
||||
|
||||
// Close MSW after all tests
|
||||
afterAll(() => server.close());
|
||||
14
ui/src/test/test-utils.tsx
Normal file
14
ui/src/test/test-utils.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { render as rtlRender, RenderOptions } from '@testing-library/react';
|
||||
import { ReactElement, ReactNode } from 'react';
|
||||
|
||||
function AllProviders({ children }: { children: ReactNode }) {
|
||||
// Minimal wrapper — tests that need full providers will wrap explicitly
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export function render(ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) {
|
||||
return rtlRender(ui, { wrapper: AllProviders, ...options });
|
||||
}
|
||||
|
||||
export * from '@testing-library/react';
|
||||
export { default as userEvent } from '@testing-library/user-event';
|
||||
28
ui/vitest.config.ts
Normal file
28
ui/vitest.config.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
include: ['src/**/*.test.{ts,tsx}'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'html', 'lcov'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'src/test/',
|
||||
'src/client/',
|
||||
'**/*.d.ts',
|
||||
],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue