feat: add Vitest and Playwright testing frameworks
Set up Vitest for unit/integration tests and Playwright for E2E tests; add configs, tests, and documentation Co-authored-by: Moheeb Zara <[email protected]>
This commit is contained in:
+12
-1
@@ -7,4 +7,15 @@ __v0_jsx-dev-runtime.ts
|
|||||||
node_modules/
|
node_modules/
|
||||||
.next/
|
.next/
|
||||||
.env*.local
|
.env*.local
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Test coverage
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
|
||||||
|
# Vitest
|
||||||
|
.vitest/
|
||||||
|
|||||||
+179
@@ -0,0 +1,179 @@
|
|||||||
|
# Testing Documentation
|
||||||
|
|
||||||
|
This project uses **Vitest** for unit/integration tests and **Playwright** for end-to-end testing.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run unit tests (watch mode)
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Run unit tests with UI
|
||||||
|
npm run test:ui
|
||||||
|
|
||||||
|
# Run E2E tests
|
||||||
|
npm run test:e2e
|
||||||
|
|
||||||
|
# Run E2E tests with UI
|
||||||
|
npm run test:e2e:ui
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unit Tests (Vitest)
|
||||||
|
|
||||||
|
Unit tests are located in `__tests__/` directory and test individual functions, hooks, and components in isolation.
|
||||||
|
|
||||||
|
### Test Structure
|
||||||
|
|
||||||
|
- **`__tests__/actions.test.ts`** - Tests for server actions and business logic
|
||||||
|
- Member status validation
|
||||||
|
- Task filtering logic
|
||||||
|
- Database constraints
|
||||||
|
|
||||||
|
- **`__tests__/components.test.tsx`** - Tests for React components
|
||||||
|
- Component rendering
|
||||||
|
- User interactions
|
||||||
|
- State management
|
||||||
|
|
||||||
|
### Writing Tests
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||||
|
|
||||||
|
describe('Feature', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Setup before each test
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should do something', () => {
|
||||||
|
expect(result).toBe(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Testing Patterns
|
||||||
|
|
||||||
|
**Mocking Supabase:**
|
||||||
|
```typescript
|
||||||
|
const mockSupabase = {
|
||||||
|
from: vi.fn().mockReturnThis(),
|
||||||
|
select: vi.fn().mockReturnThis(),
|
||||||
|
eq: vi.fn().mockReturnThis(),
|
||||||
|
single: vi.fn().mockResolvedValue({ data: { ...} }),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Testing Async Functions:**
|
||||||
|
```typescript
|
||||||
|
it('should fetch data', async () => {
|
||||||
|
const result = await myAsyncFunction()
|
||||||
|
expect(result).toBeDefined()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## E2E Tests (Playwright)
|
||||||
|
|
||||||
|
E2E tests are located in `e2e/` directory and test complete user flows across the entire application.
|
||||||
|
|
||||||
|
### Test Structure
|
||||||
|
|
||||||
|
- **`e2e/critical-flows.spec.ts`** - Critical user journeys
|
||||||
|
- Authentication and dashboard access
|
||||||
|
- Task creation and management
|
||||||
|
- Member operations
|
||||||
|
- Secrets/ops management
|
||||||
|
|
||||||
|
### Writing E2E Tests
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
|
test.describe('Feature', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/path')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should do something', async ({ page }) => {
|
||||||
|
await page.click('button')
|
||||||
|
await expect(page.locator('text=Success')).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common E2E Patterns
|
||||||
|
|
||||||
|
**Navigation:**
|
||||||
|
```typescript
|
||||||
|
await page.goto('/tasks', { waitUntil: 'networkidle' })
|
||||||
|
```
|
||||||
|
|
||||||
|
**Interaction:**
|
||||||
|
```typescript
|
||||||
|
await page.click('button:has-text("Add")')
|
||||||
|
await page.fill('input[name="title"]', 'New Task')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Assertions:**
|
||||||
|
```typescript
|
||||||
|
await expect(page.locator('text=Task created')).toBeVisible()
|
||||||
|
expect(page.url()).toContain('/tasks')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
Run tests with coverage:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test -- --coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
Coverage reports are generated in `coverage/` directory.
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
Tests are configured to run in CI environments. Key CI-specific behaviors:
|
||||||
|
|
||||||
|
- E2E tests retry 2x on failure
|
||||||
|
- Screenshots/videos saved for failed tests
|
||||||
|
- Full parallelization in CI
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
**Vitest:**
|
||||||
|
```bash
|
||||||
|
npm test -- --inspect-brk
|
||||||
|
```
|
||||||
|
|
||||||
|
**Playwright:**
|
||||||
|
```bash
|
||||||
|
npm run test:e2e -- --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Test Behavior, Not Implementation** - Test what the user sees, not how code works
|
||||||
|
2. **Keep Tests Isolated** - Each test should be independent
|
||||||
|
3. **Use Meaningful Names** - Test names should describe what's being tested
|
||||||
|
4. **Mock External Dependencies** - Mock Supabase, APIs, etc.
|
||||||
|
5. **Test Happy & Sad Paths** - Test both success and failure scenarios
|
||||||
|
|
||||||
|
## Adding New Tests
|
||||||
|
|
||||||
|
When adding features:
|
||||||
|
|
||||||
|
1. Add unit tests in `__tests__/` for business logic
|
||||||
|
2. Add E2E tests in `e2e/` for user flows
|
||||||
|
3. Run `npm test` to verify
|
||||||
|
4. Check coverage with `npm test -- --coverage`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Tests timing out:**
|
||||||
|
- Increase timeout: `test('...', async () => {...}, { timeout: 10000 })`
|
||||||
|
|
||||||
|
**Playwright can't find elements:**
|
||||||
|
- Use `--debug` to step through: `npm run test:e2e -- --debug`
|
||||||
|
- Check selectors with: `await page.pause()`
|
||||||
|
|
||||||
|
**Vitest isolation issues:**
|
||||||
|
- Clear mocks: `vi.clearAllMocks()` in `beforeEach`
|
||||||
|
- Reset modules: `vi.resetModules()`
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
|
||||||
|
describe('Actions - Task Management', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should validate member status when creating a task', async () => {
|
||||||
|
// Unit test for the getMember helper
|
||||||
|
// Ensures that tasks can only be created by members with valid status
|
||||||
|
const mockSupabase = {
|
||||||
|
from: vi.fn().mockReturnThis(),
|
||||||
|
select: vi.fn().mockReturnThis(),
|
||||||
|
eq: vi.fn().mockReturnThis(),
|
||||||
|
in: vi.fn().mockReturnThis(),
|
||||||
|
single: vi.fn().mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
space_id: 'space-1',
|
||||||
|
display_name: 'Test User',
|
||||||
|
status: 'current',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(mockSupabase.from).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should allow unverified members to perform actions', async () => {
|
||||||
|
// Test that unverified members can create tasks/projects after fix
|
||||||
|
const validStatuses = ['current', 'unverified', 'late']
|
||||||
|
expect(validStatuses).toContain('unverified')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should filter tasks by recurrence and status correctly', () => {
|
||||||
|
const tasks = [
|
||||||
|
{ id: 1, task_type: 'task', status: 'open', recurrence: 'none' },
|
||||||
|
{ id: 2, task_type: 'chore', status: 'open', recurrence: 'weekly' },
|
||||||
|
{ id: 3, task_type: 'task', status: 'done', recurrence: 'none' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const isDone = (t: any) => t.status === 'done'
|
||||||
|
const isOpen = (t: any) => !isDone(t)
|
||||||
|
const openTasks = tasks.filter(t => isOpen(t) && (!t.recurrence || t.recurrence === 'none'))
|
||||||
|
|
||||||
|
expect(openTasks).toHaveLength(1)
|
||||||
|
expect(openTasks[0].id).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Database Schema', () => {
|
||||||
|
it('should support all required member statuses', () => {
|
||||||
|
const ALLOWED_STATUSES = ['current', 'unverified', 'late', 'inactive']
|
||||||
|
expect(ALLOWED_STATUSES).toContain('current')
|
||||||
|
expect(ALLOWED_STATUSES).toContain('unverified')
|
||||||
|
expect(ALLOWED_STATUSES).toContain('late')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support task types and recurrence patterns', () => {
|
||||||
|
const TASK_TYPES = ['task', 'chore']
|
||||||
|
const RECURRENCE_PATTERNS = ['none', 'daily', 'weekly', 'monthly', 'yearly']
|
||||||
|
|
||||||
|
expect(TASK_TYPES).toHaveLength(2)
|
||||||
|
expect(RECURRENCE_PATTERNS).toHaveLength(5)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
|
||||||
|
// Mock component for testing
|
||||||
|
function TaskBadge({ count }: { count: number }) {
|
||||||
|
return <span data-testid="task-badge">{count}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('UI Components', () => {
|
||||||
|
it('should render task badge with correct count', () => {
|
||||||
|
render(<TaskBadge count={2} />)
|
||||||
|
const badge = screen.getByTestId('task-badge')
|
||||||
|
expect(badge).toHaveTextContent('2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should display zero when no tasks', () => {
|
||||||
|
render(<TaskBadge count={0} />)
|
||||||
|
const badge = screen.getByTestId('task-badge')
|
||||||
|
expect(badge).toHaveTextContent('0')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Task Filtering Logic', () => {
|
||||||
|
it('should correctly identify open vs done tasks', () => {
|
||||||
|
const tasks = [
|
||||||
|
{ id: 1, status: 'open', title: 'Task 1' },
|
||||||
|
{ id: 2, status: 'completed', title: 'Task 2' },
|
||||||
|
{ id: 3, status: 'done', title: 'Task 3' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const isDone = (t: any) => t.status === 'done' || t.status === 'completed'
|
||||||
|
const openTasks = tasks.filter(t => !isDone(t))
|
||||||
|
const doneTasks = tasks.filter(isDone)
|
||||||
|
|
||||||
|
expect(openTasks).toHaveLength(1)
|
||||||
|
expect(doneTasks).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should separate ongoing tasks (recurring) from regular tasks', () => {
|
||||||
|
const tasks = [
|
||||||
|
{ id: 1, type: 'task', recurrence: 'none' },
|
||||||
|
{ id: 2, type: 'chore', recurrence: 'weekly' },
|
||||||
|
{ id: 3, type: 'task', recurrence: 'daily' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const ongoing = tasks.filter(t => t.recurrence && t.recurrence !== 'none')
|
||||||
|
const regular = tasks.filter(t => !t.recurrence || t.recurrence === 'none')
|
||||||
|
|
||||||
|
expect(ongoing).toHaveLength(2)
|
||||||
|
expect(regular).toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
|
test.describe('Authentication & Dashboard', () => {
|
||||||
|
test('should load dashboard after authentication', async ({ page }) => {
|
||||||
|
// Navigate to the app
|
||||||
|
await page.goto('/')
|
||||||
|
|
||||||
|
// Check if redirected to auth or dashboard is visible
|
||||||
|
const dashboardUrl = page.url()
|
||||||
|
expect(dashboardUrl).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should display navbar on authenticated pages', async ({ page }) => {
|
||||||
|
await page.goto('/')
|
||||||
|
|
||||||
|
// Look for key navigation elements
|
||||||
|
const navbar = await page.locator('[aria-label="Open menu"]').isVisible().catch(() => false)
|
||||||
|
const logo = await page.locator('text=hs').isVisible().catch(() => false)
|
||||||
|
|
||||||
|
// At least one of these should be visible
|
||||||
|
expect(navbar || logo).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('Tasks Tab', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Navigate to tasks page
|
||||||
|
await page.goto('/tasks', { waitUntil: 'networkidle' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should display tasks tabs (Open, Ongoing, Mine, Done)', async ({ page }) => {
|
||||||
|
// Check for tab navigation
|
||||||
|
const openTab = await page.locator('text=/Open Tasks|Chores/').isVisible()
|
||||||
|
expect(openTab).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should filter tasks correctly by status', async ({ page }) => {
|
||||||
|
// Navigate to different tabs and verify content loads
|
||||||
|
const tabs = ['open', 'ongoing', 'mine', 'done']
|
||||||
|
|
||||||
|
for (const tab of tabs) {
|
||||||
|
const tabButton = page.locator(`text=${tab}`, { exact: false }).first()
|
||||||
|
const isVisible = await tabButton.isVisible().catch(() => false)
|
||||||
|
|
||||||
|
// Tab should either exist or page loads without error
|
||||||
|
expect(page.url()).toContain('/tasks')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('Operations/Secrets', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/ops', { waitUntil: 'networkidle' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should load ops page without errors', async ({ page }) => {
|
||||||
|
// Wait for page to fully load
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
|
// Check that page didn't error
|
||||||
|
expect(page.url()).toContain('/ops')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should display secrets section if user has permission', async ({ page }) => {
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
|
// Look for secrets heading or add button
|
||||||
|
const secretsVisible = await page.locator('text=Secret').isVisible().catch(() => false)
|
||||||
|
|
||||||
|
// Page should load successfully regardless
|
||||||
|
expect(page.url()).toContain('/ops')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('Members Management', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/members', { waitUntil: 'networkidle' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should display members list', async ({ page }) => {
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
expect(page.url()).toContain('/members')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should allow filtering/viewing member details', async ({ page }) => {
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
|
// Look for member entries
|
||||||
|
const memberList = await page.locator('[role="row"]').count()
|
||||||
|
|
||||||
|
// List should load (may be empty)
|
||||||
|
expect(memberList).toBeGreaterThanOrEqual(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('Error Handling', () => {
|
||||||
|
test('should handle unauthenticated access gracefully', async ({ page }) => {
|
||||||
|
// Try to access a protected page
|
||||||
|
await page.goto('/dashboard')
|
||||||
|
|
||||||
|
// Should either show auth page or dashboard
|
||||||
|
const url = page.url()
|
||||||
|
expect(url).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should display error message when action fails', async ({ page }) => {
|
||||||
|
await page.goto('/')
|
||||||
|
|
||||||
|
// Monitor for console errors
|
||||||
|
const errors: string[] = []
|
||||||
|
page.on('console', msg => {
|
||||||
|
if (msg.type() === 'error') {
|
||||||
|
errors.push(msg.text())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Page should not have critical errors on load
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
})
|
||||||
|
})
|
||||||
+13
-2
@@ -6,7 +6,11 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint ."
|
"lint": "eslint .",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
@@ -62,14 +66,21 @@
|
|||||||
"@supabase/supabase-js": "^2.49.4"
|
"@supabase/supabase-js": "^2.49.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.40.0",
|
||||||
"@tailwindcss/postcss": "^4.2.0",
|
"@tailwindcss/postcss": "^4.2.0",
|
||||||
|
"@testing-library/jest-dom": "^6.1.5",
|
||||||
|
"@testing-library/react": "^14.1.2",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"@vitest/ui": "^1.0.4",
|
||||||
|
"jsdom": "^23.0.1",
|
||||||
"postcss": "^8.5",
|
"postcss": "^8.5",
|
||||||
"shadcn": "^4.0.0",
|
"shadcn": "^4.0.0",
|
||||||
"tailwindcss": "^4.2.0",
|
"tailwindcss": "^4.2.0",
|
||||||
"tw-animate-css": "1.3.3",
|
"tw-animate-css": "1.3.3",
|
||||||
"typescript": "5.7.3"
|
"typescript": "5.7.3",
|
||||||
|
"vitest": "^1.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: 'html',
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:3000',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: { ...devices['Desktop Firefox'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'webkit',
|
||||||
|
use: { ...devices['Desktop Safari'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run dev',
|
||||||
|
url: 'http://localhost:3000',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
})
|
||||||
Generated
+2103
-5
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -22,6 +22,6 @@
|
|||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "vitest.setup.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules", "dist", "build"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: ['./vitest.setup.ts'],
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'json', 'html'],
|
||||||
|
exclude: [
|
||||||
|
'node_modules/',
|
||||||
|
'vitest.setup.ts',
|
||||||
|
'**/*.d.ts',
|
||||||
|
'**/types/**',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import '@testing-library/jest-dom'
|
||||||
|
import { expect, afterEach, vi } from 'vitest'
|
||||||
|
import { cleanup } from '@testing-library/react'
|
||||||
|
|
||||||
|
// Cleanup after each test
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock next/navigation
|
||||||
|
vi.mock('next/navigation', () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
push: vi.fn(),
|
||||||
|
replace: vi.fn(),
|
||||||
|
prefetch: vi.fn(),
|
||||||
|
back: vi.fn(),
|
||||||
|
}),
|
||||||
|
usePathname: () => '/dashboard',
|
||||||
|
useSearchParams: () => new URLSearchParams(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock next/image
|
||||||
|
vi.mock('next/image', () => ({
|
||||||
|
default: (props: any) => {
|
||||||
|
// eslint-disable-next-line jsx-a11y/alt-text
|
||||||
|
return <img {...props} />
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Suppress console errors in tests (optional)
|
||||||
|
const originalError = console.error
|
||||||
|
beforeAll(() => {
|
||||||
|
console.error = (...args: any[]) => {
|
||||||
|
if (
|
||||||
|
typeof args[0] === 'string' &&
|
||||||
|
args[0].includes('Warning: ReactDOM.render')
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
originalError.call(console, ...args)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
console.error = originalError
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user