Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:47:01 +08:00
commit 192f93f4d5
12 changed files with 1396 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "testing-next-stack",
"description": "Scaffolds comprehensive testing setup for Next.js applications including Vitest unit tests, React Testing Library component tests, and Playwright E2E flows with accessibility testing via axe-core. This skill should be used when setting up test infrastructure, generating test files, creating test utilities, adding accessibility checks, or configuring testing frameworks for Next.js projects. Trigger terms include setup testing, scaffold tests, vitest, RTL, playwright, e2e tests, component tests, u",
"version": "1.0.0",
"author": {
"name": "Hope Overture",
"email": "support@worldbuilding-app-skills.dev"
},
"skills": [
"./skills"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# testing-next-stack
Scaffolds comprehensive testing setup for Next.js applications including Vitest unit tests, React Testing Library component tests, and Playwright E2E flows with accessibility testing via axe-core. This skill should be used when setting up test infrastructure, generating test files, creating test utilities, adding accessibility checks, or configuring testing frameworks for Next.js projects. Trigger terms include setup testing, scaffold tests, vitest, RTL, playwright, e2e tests, component tests, u

77
plugin.lock.json Normal file
View File

@@ -0,0 +1,77 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:hopeoverture/worldbuilding-app-skills:plugins/testing-next-stack",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "2af0e096a9e5da13131e0add608cb7fb0947a93c",
"treeHash": "9811669e497f0789c1ebf725c0f022e1da5b0c7f6cf9e4f994498224963a0966",
"generatedAt": "2025-11-28T10:17:33.745681Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "testing-next-stack",
"description": "Scaffolds comprehensive testing setup for Next.js applications including Vitest unit tests, React Testing Library component tests, and Playwright E2E flows with accessibility testing via axe-core. This skill should be used when setting up test infrastructure, generating test files, creating test utilities, adding accessibility checks, or configuring testing frameworks for Next.js projects. Trigger terms include setup testing, scaffold tests, vitest, RTL, playwright, e2e tests, component tests, u",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "d870d6d6cf7dba4c8ab0fdb55d0a6f2b4338215551e822cbfdaf78073eac8698"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "da615a00400347d8195da728d177e47145283b24fc2e3745115341ba39bca36f"
},
{
"path": "skills/testing-next-stack/SKILL.md",
"sha256": "89e3d54d4e70c02bfdce1533f3d44f97c370ebf26840c4dfecabeb3ee0d8d126"
},
{
"path": "skills/testing-next-stack/references/a11y-testing.md",
"sha256": "e67cd41061a79e90fd82818a474c2fde022cd9229976f09e68a5e87b6ecf4d54"
},
{
"path": "skills/testing-next-stack/scripts/generate_test_deps.py",
"sha256": "cd9f6a1e4d200bb79861ce32732ed0063eb8dc15d63b355095c5593d02ba4142"
},
{
"path": "skills/testing-next-stack/assets/test-setup.ts",
"sha256": "b9ced2a3417994a63b30af909aab670cac42f76253095b8dce76042ded722fec"
},
{
"path": "skills/testing-next-stack/assets/playwright.config.ts",
"sha256": "3dd19caf9f337e66bb5f43bcbc180584298a04a717cd1ce49b50d2b984db1ed1"
},
{
"path": "skills/testing-next-stack/assets/vitest.config.ts",
"sha256": "efa8eb65cc7045c2338b0d9de83e8f5f4abc61ba7ebbed187cb9b508e78ceb41"
},
{
"path": "skills/testing-next-stack/assets/examples/component-test.tsx",
"sha256": "2764490aa6087fbe78770cbfcc7a4ad7f7008be9d38318ea96e11e0065442c11"
},
{
"path": "skills/testing-next-stack/assets/examples/e2e-test.ts",
"sha256": "59d30e2465786f12dca26d9376e4c62c6f6153be03759754de4193969a15c676"
},
{
"path": "skills/testing-next-stack/assets/examples/unit-test.ts",
"sha256": "09b795ec10b2c8890b9ca5a00d02393047784e7d517145632b31fa6c09c95b5a"
}
],
"dirSha256": "9811669e497f0789c1ebf725c0f022e1da5b0c7f6cf9e4f994498224963a0966"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,333 @@
---
name: testing-next-stack
description: Scaffolds comprehensive testing setup for Next.js applications including Vitest unit tests, React Testing Library component tests, and Playwright E2E flows with accessibility testing via axe-core. This skill should be used when setting up test infrastructure, generating test files, creating test utilities, adding accessibility checks, or configuring testing frameworks for Next.js projects. Trigger terms include setup testing, scaffold tests, vitest, RTL, playwright, e2e tests, component tests, unit tests, accessibility testing, a11y tests, axe-core, test configuration.
---
# Testing Next Stack
Scaffold complete testing infrastructure for Next.js applications with modern testing tools.
## Overview
To create a comprehensive testing setup for Next.js applications, use this skill to generate:
- Vitest configuration for fast unit tests
- React Testing Library setup for component testing
- Playwright configuration for E2E testing
- Accessibility testing with axe-core
- Test utilities and helpers
- Example test files demonstrating best practices
## When to Use
Use this skill when:
- Starting a new Next.js project requiring test infrastructure
- Migrating from Jest to Vitest
- Adding E2E testing with Playwright
- Implementing accessibility testing requirements
- Creating test utilities for worldbuilding app features (entities, relationships, timelines)
- Standardizing testing patterns across projects
## Setup Process
### 1. Analyze Project Structure
To understand the project layout, examine:
- Package.json for existing dependencies
- Next.js version and configuration
- TypeScript or JavaScript setup
- Existing testing infrastructure
- Component architecture
### 2. Install Dependencies
Generate package.json additions using `scripts/generate_test_deps.py`:
```bash
python scripts/generate_test_deps.py --nextjs-version <version> --typescript
```
Install required packages:
- `vitest` - Fast unit test runner
- `@testing-library/react` - Component testing utilities
- `@testing-library/jest-dom` - Custom matchers
- `@testing-library/user-event` - User interaction simulation
- `@playwright/test` - E2E testing framework
- `@axe-core/playwright` - Accessibility testing
- `@vitejs/plugin-react` - Vite React plugin
- `jsdom` - DOM implementation for Vitest
### 3. Generate Configuration Files
Create configuration files using templates from `assets/`:
**Vitest Configuration** (`vitest.config.ts`):
- Use template from `assets/vitest.config.ts`
- Configure path aliases matching Next.js
- Set up test environment (jsdom)
- Configure coverage reporting
**Playwright Configuration** (`playwright.config.ts`):
- Use template from `assets/playwright.config.ts`
- Configure browsers (chromium, firefox, webkit)
- Set baseURL for development server
- Configure screenshot and video capture
- Set up test artifacts directory
**Test Setup** (`test/setup.ts`):
- Use template from `assets/test-setup.ts`
- Import @testing-library/jest-dom
- Configure global test utilities
- Set up mock implementations
### 4. Create Test Utilities
Generate utility functions in `test/utils/`:
**Render Utilities** (`test/utils/render.tsx`):
- Custom render function wrapping providers
- Context providers (auth, theme, data)
- Router mocking for Next.js
- Query client setup for React Query
**Mock Factories** (`test/utils/factories.ts`):
- Entity mock data generators
- Relationship mock data
- User mock data
- API response mocks
**Test Helpers** (`test/utils/helpers.ts`):
- Async test utilities
- DOM query shortcuts
- Accessibility test helpers
- Custom matchers
### 5. Generate Example Tests
Create example test files demonstrating patterns:
**Unit Test Example** (`test/unit/example.test.ts`):
- Use template from `assets/examples/unit-test.ts`
- Demonstrate pure function testing
- Show async function testing
- Include edge case coverage
**Component Test Example** (`test/component/example.test.tsx`):
- Use template from `assets/examples/component-test.tsx`
- Demonstrate rendering and assertions
- Show user interaction testing
- Include accessibility checks with axe
**E2E Test Example** (`test/e2e/example.spec.ts`):
- Use template from `assets/examples/e2e-test.ts`
- Demonstrate user flow testing
- Show authentication flows
- Include accessibility scanning
### 6. Update Package Scripts
Add test scripts to package.json:
```json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug"
}
}
```
## Test Patterns
### Unit Testing with Vitest
To test utility functions and business logic:
```typescript
import { describe, it, expect } from 'vitest'
import { validateEntityRelationship } from '@/lib/validation'
describe('validateEntityRelationship', () => {
it('validates valid relationship', () => {
const result = validateEntityRelationship({
sourceId: '1',
targetId: '2',
type: 'BELONGS_TO'
})
expect(result.isValid).toBe(true)
})
it('rejects self-referential relationship', () => {
const result = validateEntityRelationship({
sourceId: '1',
targetId: '1',
type: 'BELONGS_TO'
})
expect(result.isValid).toBe(false)
})
})
```
### Component Testing with RTL
To test React components:
```typescript
import { render, screen } from '@/test/utils/render'
import { userEvent } from '@testing-library/user-event'
import { axe } from '@axe-core/playwright'
import EntityCard from '@/components/EntityCard'
describe('EntityCard', () => {
it('renders entity information', () => {
render(<EntityCard entity={mockEntity} />)
expect(screen.getByText(mockEntity.name)).toBeInTheDocument()
})
it('handles edit action', async () => {
const onEdit = vi.fn()
render(<EntityCard entity={mockEntity} onEdit={onEdit} />)
await userEvent.click(screen.getByRole('button', { name: /edit/i }))
expect(onEdit).toHaveBeenCalledWith(mockEntity.id)
})
it('has no accessibility violations', async () => {
const { container } = render(<EntityCard entity={mockEntity} />)
const results = await axe(container)
expect(results.violations).toHaveLength(0)
})
})
```
### E2E Testing with Playwright
To test complete user flows:
```typescript
import { test, expect } from '@playwright/test'
import { injectAxe, checkA11y } from '@axe-core/playwright'
test('user creates new entity', async ({ page }) => {
await page.goto('/entities')
// Inject axe for accessibility testing
await injectAxe(page)
// Navigate to create form
await page.getByRole('button', { name: /create entity/i }).click()
// Fill form
await page.getByLabel(/name/i).fill('New Character')
await page.getByLabel(/type/i).selectOption('character')
await page.getByLabel(/description/i).fill('A mysterious traveler')
// Submit
await page.getByRole('button', { name: /save/i }).click()
// Verify success
await expect(page.getByText('New Character')).toBeVisible()
// Check accessibility
await checkA11y(page)
})
```
## Accessibility Testing
### Component-Level A11y
To add accessibility assertions in component tests:
```typescript
import { render } from '@/test/utils/render'
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
it('meets accessibility standards', async () => {
const { container } = render(<MyComponent />)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
```
### E2E A11y Scanning
To scan entire pages for accessibility issues:
```typescript
import { test } from '@playwright/test'
import { injectAxe, checkA11y, getViolations } from '@axe-core/playwright'
test('homepage accessibility', async ({ page }) => {
await page.goto('/')
await injectAxe(page)
// Check entire page
await checkA11y(page)
// Or check specific element
await checkA11y(page, '#main-content')
// Or get violations for custom reporting
const violations = await getViolations(page)
expect(violations).toHaveLength(0)
})
```
## Coverage Configuration
To generate code coverage reports, configure Vitest coverage in `vitest.config.ts`:
```typescript
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'test/',
'**/*.config.{ts,js}',
'**/*.d.ts'
],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80
}
}
}
})
```
## Resources
Consult the following resources for detailed information:
- `references/vitest-setup.md` - Vitest configuration details
- `references/rtl-patterns.md` - React Testing Library best practices
- `references/playwright-setup.md` - Playwright configuration guide
- `references/a11y-testing.md` - Accessibility testing guidelines
- `assets/vitest.config.ts` - Vitest configuration template
- `assets/playwright.config.ts` - Playwright configuration template
- `assets/test-setup.ts` - Test setup template
- `assets/examples/` - Example test files
## Next Steps
After scaffolding the testing infrastructure:
1. Run `npm install` to install dependencies
2. Execute `npm test` to verify Vitest setup
3. Execute `npm run test:e2e` to verify Playwright setup
4. Review and customize configuration files
5. Add tests for existing components and features
6. Configure CI/CD pipeline with test execution
7. Set up coverage reporting in CI
8. Document testing guidelines for team

View File

@@ -0,0 +1,128 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen, within } from '@testing-library/react'
import { userEvent } from '@testing-library/user-event'
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
/**
* Example component test demonstrating RTL best practices
* with accessibility testing via axe-core
*/
// Example component
interface EntityCardProps {
entity: {
id: string
name: string
type: string
description?: string
}
onEdit?: (id: string) => void
onDelete?: (id: string) => void
}
function EntityCard({ entity, onEdit, onDelete }: EntityCardProps) {
return (
<article aria-label={`Entity: ${entity.name}`}>
<header>
<h2>{entity.name}</h2>
<span className="badge">{entity.type}</span>
</header>
{entity.description && <p>{entity.description}</p>}
<footer>
{onEdit && (
<button onClick={() => onEdit(entity.id)} aria-label={`Edit ${entity.name}`}>
Edit
</button>
)}
{onDelete && (
<button onClick={() => onDelete(entity.id)} aria-label={`Delete ${entity.name}`}>
Delete
</button>
)}
</footer>
</article>
)
}
describe('EntityCard', () => {
const mockEntity = {
id: '1',
name: 'Test Character',
type: 'character',
description: 'A brave adventurer'
}
it('renders entity information', () => {
render(<EntityCard entity={mockEntity} />)
expect(screen.getByRole('heading', { name: 'Test Character' })).toBeInTheDocument()
expect(screen.getByText('character')).toBeInTheDocument()
expect(screen.getByText('A brave adventurer')).toBeInTheDocument()
})
it('does not render description when not provided', () => {
const entityWithoutDesc = { ...mockEntity, description: undefined }
render(<EntityCard entity={entityWithoutDesc} />)
expect(screen.queryByText('A brave adventurer')).not.toBeInTheDocument()
})
it('calls onEdit when edit button is clicked', async () => {
const user = userEvent.setup()
const onEdit = vi.fn()
render(<EntityCard entity={mockEntity} onEdit={onEdit} />)
await user.click(screen.getByRole('button', { name: /edit test character/i }))
expect(onEdit).toHaveBeenCalledTimes(1)
expect(onEdit).toHaveBeenCalledWith('1')
})
it('calls onDelete when delete button is clicked', async () => {
const user = userEvent.setup()
const onDelete = vi.fn()
render(<EntityCard entity={mockEntity} onDelete={onDelete} />)
await user.click(screen.getByRole('button', { name: /delete test character/i }))
expect(onDelete).toHaveBeenCalledTimes(1)
expect(onDelete).toHaveBeenCalledWith('1')
})
it('does not render action buttons when handlers not provided', () => {
render(<EntityCard entity={mockEntity} />)
expect(screen.queryByRole('button', { name: /edit/i })).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument()
})
it('has accessible structure', () => {
render(<EntityCard entity={mockEntity} onEdit={vi.fn()} onDelete={vi.fn()} />)
const article = screen.getByRole('article', { name: /entity: test character/i })
expect(article).toBeInTheDocument()
// Check heading hierarchy
const heading = within(article).getByRole('heading', { level: 2 })
expect(heading).toHaveTextContent('Test Character')
// Check buttons have accessible names
const editButton = within(article).getByRole('button', { name: /edit test character/i })
const deleteButton = within(article).getByRole('button', { name: /delete test character/i })
expect(editButton).toBeInTheDocument()
expect(deleteButton).toBeInTheDocument()
})
it('has no accessibility violations', async () => {
const { container } = render(
<EntityCard entity={mockEntity} onEdit={vi.fn()} onDelete={vi.fn()} />
)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
})

View File

@@ -0,0 +1,196 @@
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'
/**
* Example E2E test demonstrating Playwright best practices
* with accessibility testing via axe-core
*/
test.describe('Entity Management', () => {
test.beforeEach(async ({ page }) => {
// Navigate to entities page
await page.goto('/entities')
})
test('displays entity list', async ({ page }) => {
// Wait for content to load
await page.waitForSelector('[role="list"]')
// Verify entities are displayed
const entities = page.getByRole('listitem')
await expect(entities).not.toHaveCount(0)
// Verify entity cards have proper structure
const firstEntity = entities.first()
await expect(firstEntity.getByRole('heading')).toBeVisible()
})
test('creates new entity', async ({ page }) => {
// Click create button
await page.getByRole('button', { name: /create entity/i }).click()
// Verify form is displayed
await expect(page.getByRole('heading', { name: /new entity/i })).toBeVisible()
// Fill in form
await page.getByLabel(/name/i).fill('Mysterious Stranger')
await page.getByLabel(/type/i).selectOption('character')
await page.getByLabel(/description/i).fill('A traveler from distant lands')
// Submit form
await page.getByRole('button', { name: /save|create/i }).click()
// Verify success message or redirect
await expect(
page.getByText(/entity created|success/i)
).toBeVisible({ timeout: 5000 })
// Verify new entity appears in list
await page.goto('/entities')
await expect(page.getByText('Mysterious Stranger')).toBeVisible()
})
test('edits existing entity', async ({ page }) => {
// Find and click edit button for first entity
const firstEntity = page.getByRole('listitem').first()
const entityName = await firstEntity.getByRole('heading').textContent()
await firstEntity.getByRole('button', { name: /edit/i }).click()
// Update name
const nameInput = page.getByLabel(/name/i)
await nameInput.clear()
await nameInput.fill(`${entityName} (Updated)`)
// Save changes
await page.getByRole('button', { name: /save|update/i }).click()
// Verify update
await expect(page.getByText(/updated|success/i)).toBeVisible()
await page.goto('/entities')
await expect(page.getByText(`${entityName} (Updated)`)).toBeVisible()
})
test('deletes entity with confirmation', async ({ page }) => {
// Click delete button
const firstEntity = page.getByRole('listitem').first()
const entityName = await firstEntity.getByRole('heading').textContent()
await firstEntity.getByRole('button', { name: /delete/i }).click()
// Confirm deletion in dialog
const dialog = page.getByRole('dialog')
await expect(dialog.getByText(/confirm|sure/i)).toBeVisible()
await dialog.getByRole('button', { name: /delete|confirm/i }).click()
// Verify entity is removed
await expect(page.getByText(entityName!)).not.toBeVisible()
})
test('searches entities', async ({ page }) => {
// Enter search query
const searchInput = page.getByRole('searchbox', { name: /search/i })
await searchInput.fill('character')
// Wait for filtered results
await page.waitForTimeout(500) // Debounce
// Verify filtered results
const results = page.getByRole('listitem')
const count = await results.count()
// All visible results should match search
for (let i = 0; i < count; i++) {
const item = results.nth(i)
await expect(item.getByText(/character/i)).toBeVisible()
}
})
test('filters entities by type', async ({ page }) => {
// Select filter
await page.getByLabel(/filter by type/i).selectOption('location')
// Wait for filtered results
await page.waitForSelector('[role="listitem"]')
// Verify all results are locations
const badges = page.locator('.badge')
const count = await badges.count()
for (let i = 0; i < count; i++) {
await expect(badges.nth(i)).toHaveText('location')
}
})
test('keyboard navigation works', async ({ page }) => {
// Focus first interactive element
await page.keyboard.press('Tab')
// Navigate through entities with arrow keys
await page.keyboard.press('ArrowDown')
await page.keyboard.press('ArrowDown')
// Activate focused element with Enter
await page.keyboard.press('Enter')
// Verify navigation worked
await expect(page.getByRole('heading', { name: /entity/i })).toBeVisible()
})
test('meets accessibility standards', async ({ page }) => {
// Run axe accessibility scan
const accessibilityScanResults = await new AxeBuilder({ page }).analyze()
// Expect no violations
expect(accessibilityScanResults.violations).toEqual([])
})
test('is responsive on mobile', async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 })
// Verify mobile layout
await expect(page.getByRole('button', { name: /menu/i })).toBeVisible()
// Test mobile navigation
await page.getByRole('button', { name: /menu/i }).click()
await expect(page.getByRole('navigation')).toBeVisible()
})
})
test.describe('Entity Relationships', () => {
test('creates relationship between entities', async ({ page }) => {
// Navigate to first entity detail page
await page.goto('/entities')
await page.getByRole('listitem').first().click()
// Open relationship creation
await page.getByRole('button', { name: /add relationship/i }).click()
// Select related entity
await page.getByLabel(/related entity/i).fill('Location')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('Enter')
// Select relationship type
await page.getByLabel(/relationship type/i).selectOption('lives_in')
// Save relationship
await page.getByRole('button', { name: /create|save/i }).click()
// Verify relationship appears
await expect(page.getByText(/lives_in/i)).toBeVisible()
})
test('relationship section is accessible', async ({ page }) => {
await page.goto('/entities')
await page.getByRole('listitem').first().click()
// Scan relationships section
const accessibilityScanResults = await new AxeBuilder({ page })
.include('#relationships')
.analyze()
expect(accessibilityScanResults.violations).toEqual([])
})
})

View File

@@ -0,0 +1,86 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
/**
* Example unit test demonstrating best practices
* for testing pure functions and business logic
*/
// Example function to test
function validateEntityName(name: string): { valid: boolean; error?: string } {
if (!name || name.trim().length === 0) {
return { valid: false, error: 'Name is required' }
}
if (name.length > 100) {
return { valid: false, error: 'Name must be 100 characters or less' }
}
if (!/^[a-zA-Z0-9\s-_']+$/.test(name)) {
return { valid: false, error: 'Name contains invalid characters' }
}
return { valid: true }
}
describe('validateEntityName', () => {
it('accepts valid entity names', () => {
expect(validateEntityName('John Doe').valid).toBe(true)
expect(validateEntityName("O'Brien").valid).toBe(true)
expect(validateEntityName('Location-123').valid).toBe(true)
})
it('rejects empty names', () => {
const result = validateEntityName('')
expect(result.valid).toBe(false)
expect(result.error).toBe('Name is required')
})
it('rejects names with only whitespace', () => {
const result = validateEntityName(' ')
expect(result.valid).toBe(false)
expect(result.error).toBe('Name is required')
})
it('rejects names exceeding max length', () => {
const longName = 'a'.repeat(101)
const result = validateEntityName(longName)
expect(result.valid).toBe(false)
expect(result.error).toContain('100 characters')
})
it('rejects names with invalid characters', () => {
const result = validateEntityName('Name@#$')
expect(result.valid).toBe(false)
expect(result.error).toContain('invalid characters')
})
})
// Example async function test
async function fetchEntityData(id: string): Promise<any> {
const response = await fetch(`/api/entities/${id}`)
if (!response.ok) throw new Error('Failed to fetch')
return response.json()
}
describe('fetchEntityData', () => {
beforeEach(() => {
global.fetch = vi.fn()
})
it('fetches entity data successfully', async () => {
const mockData = { id: '1', name: 'Test Entity' }
;(global.fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => mockData
})
const result = await fetchEntityData('1')
expect(result).toEqual(mockData)
expect(global.fetch).toHaveBeenCalledWith('/api/entities/1')
})
it('throws error on failed fetch', async () => {
;(global.fetch as any).mockResolvedValueOnce({
ok: false
})
await expect(fetchEntityData('1')).rejects.toThrow('Failed to fetch')
})
})

View File

@@ -0,0 +1,50 @@
import { defineConfig, devices } from '@playwright/test'
const baseURL = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000'
export default defineConfig({
testDir: './test/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['json', { outputFile: 'test-results/results.json' }],
['list']
],
use: {
baseURL,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] }
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] }
}
],
webServer: {
command: 'npm run dev',
url: baseURL,
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000
}
})

View File

@@ -0,0 +1,58 @@
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.js router
vi.mock('next/navigation', () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
prefetch: vi.fn(),
back: vi.fn(),
pathname: '/',
query: {},
asPath: '/'
}),
usePathname: () => '/',
useSearchParams: () => new URLSearchParams(),
useParams: () => ({})
}))
// Mock Next.js image component
vi.mock('next/image', () => ({
default: ({ src, alt, ...props }: any) => {
// eslint-disable-next-line @next/next/no-img-element
return <img src={src} alt={alt} {...props} />
}
}))
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn()
}))
})
// Mock IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
constructor() {}
disconnect() {}
observe() {}
takeRecords() {
return []
}
unobserve() {}
} as any

View File

@@ -0,0 +1,45 @@
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: ['./test/setup.ts'],
include: ['**/*.{test,spec}.{ts,tsx}'],
exclude: ['node_modules', 'dist', '.next', 'test/e2e/**'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
exclude: [
'node_modules/',
'test/',
'**/*.config.{ts,js}',
'**/*.d.ts',
'.next/',
'dist/',
'public/',
'**/__mocks__/**',
'**/types/**'
],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80
}
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@/components': path.resolve(__dirname, './src/components'),
'@/lib': path.resolve(__dirname, './src/lib'),
'@/hooks': path.resolve(__dirname, './src/hooks'),
'@/types': path.resolve(__dirname, './src/types'),
'@/test': path.resolve(__dirname, './test')
}
}
})

View File

@@ -0,0 +1,315 @@
# Accessibility Testing Guide
Comprehensive guide for implementing accessibility testing in Next.js applications.
## Overview
Accessibility testing ensures applications are usable by people with disabilities and comply with WCAG standards.
## Tools
### axe-core
Industry-standard accessibility testing engine that detects WCAG violations.
**Installation:**
```bash
npm install -D @axe-core/playwright jest-axe
```
### @axe-core/playwright
Playwright integration for axe-core enabling E2E accessibility testing.
### jest-axe
Jest/Vitest matcher for accessibility assertions in component tests.
## Component-Level Testing
### Setup
```typescript
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
```
### Basic Usage
```typescript
it('has no accessibility violations', async () => {
const { container } = render(<MyComponent />)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
```
### Testing Specific Elements
```typescript
it('form has no violations', async () => {
const { container } = render(<SignupForm />)
const form = container.querySelector('form')
const results = await axe(form)
expect(results).toHaveNoViolations()
})
```
### Custom Rules
```typescript
const results = await axe(container, {
rules: {
'color-contrast': { enabled: true },
'valid-aria-role': { enabled: true }
}
})
```
## E2E Accessibility Testing
### Setup
```typescript
import AxeBuilder from '@axe-core/playwright'
```
### Page-Level Scanning
```typescript
test('homepage meets a11y standards', async ({ page }) => {
await page.goto('/')
const accessibilityScanResults = await new AxeBuilder({ page }).analyze()
expect(accessibilityScanResults.violations).toEqual([])
})
```
### Scanning Specific Regions
```typescript
test('navigation is accessible', async ({ page }) => {
await page.goto('/')
const results = await new AxeBuilder({ page })
.include('#navigation')
.analyze()
expect(results.violations).toEqual([])
})
```
### Excluding Elements
```typescript
const results = await new AxeBuilder({ page })
.exclude('#third-party-widget')
.analyze()
```
### Custom Tags
Test specific WCAG levels:
```typescript
// WCAG 2.1 Level AA
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze()
```
## Common Violations and Fixes
### Missing Alt Text
**Violation:** Images without alt attributes
**Fix:**
```tsx
// Bad
<img src="/avatar.jpg" />
// Good
<img src="/avatar.jpg" alt="User avatar" />
// Decorative images
<img src="/divider.png" alt="" />
```
### Form Labels
**Violation:** Form inputs without labels
**Fix:**
```tsx
// Bad
<input type="text" placeholder="Name" />
// Good
<label htmlFor="name">Name</label>
<input id="name" type="text" />
// Or use aria-label
<input type="text" aria-label="Name" />
```
### Color Contrast
**Violation:** Insufficient contrast ratio
**Fix:**
- Use contrast ratio of at least 4.5:1 for normal text
- Use contrast ratio of at least 3:1 for large text
- Test with tools like WebAIM Contrast Checker
### Heading Hierarchy
**Violation:** Skipped heading levels
**Fix:**
```tsx
// Bad
<h1>Page Title</h1>
<h3>Section</h3>
// Good
<h1>Page Title</h1>
<h2>Section</h2>
```
### Keyboard Navigation
**Violation:** Interactive elements not keyboard accessible
**Fix:**
```tsx
// Bad
<div onClick={handleClick}>Click me</div>
// Good
<button onClick={handleClick}>Click me</button>
// Or add keyboard handlers
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick()
}
}}
>
Click me
</div>
```
### Focus Indicators
**Violation:** Invisible focus indicators
**Fix:**
```css
/* Ensure visible focus */
:focus-visible {
outline: 2px solid blue;
outline-offset: 2px;
}
```
## ARIA Best Practices
### Landmarks
```tsx
<header role="banner">
<nav role="navigation">
<main role="main">
<aside role="complementary">
<footer role="contentinfo">
```
### Live Regions
```tsx
<div role="status" aria-live="polite">
Form submitted successfully
</div>
<div role="alert" aria-live="assertive">
Error: Please correct the following fields
</div>
```
### Dynamic Content
```tsx
<button
aria-expanded={isOpen}
aria-controls="dropdown-menu"
>
Menu
</button>
<div id="dropdown-menu" aria-hidden={!isOpen}>
{/* Menu items */}
</div>
```
## Testing Checklist
- [ ] All images have alt text
- [ ] Form inputs have labels
- [ ] Heading hierarchy is logical
- [ ] Color contrast meets WCAG AA
- [ ] Keyboard navigation works
- [ ] Focus indicators are visible
- [ ] ARIA attributes are correct
- [ ] Dynamic content announces properly
- [ ] No violations in axe scans
- [ ] Screen reader tested (optional but recommended)
## CI Integration
### GitHub Actions
```yaml
- name: Run accessibility tests
run: npm run test:e2e -- --grep @a11y
- name: Upload a11y results
uses: actions/upload-artifact@v3
with:
name: accessibility-results
path: test-results/
```
### Failed Test Reporting
```typescript
test('check accessibility', async ({ page }) => {
await page.goto('/')
const results = await new AxeBuilder({ page }).analyze()
if (results.violations.length > 0) {
console.log('Accessibility violations:')
results.violations.forEach(violation => {
console.log(`- ${violation.id}: ${violation.description}`)
console.log(` Impact: ${violation.impact}`)
console.log(` Elements: ${violation.nodes.length}`)
})
}
expect(results.violations).toEqual([])
})
```
## Resources
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
- [axe-core Documentation](https://github.com/dequelabs/axe-core)
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
- [ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""Generate testing dependencies for Next.js projects."""
import argparse
import json
import sys
def generate_dependencies(nextjs_version: str, typescript: bool) -> dict:
"""Generate package.json dependencies for testing setup."""
deps = {
"devDependencies": {
"vitest": "^2.0.0",
"@vitejs/plugin-react": "^4.3.0",
"@testing-library/react": "^16.0.0",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/user-event": "^14.5.0",
"jsdom": "^25.0.0",
"@playwright/test": "^1.48.0",
"@axe-core/playwright": "^4.10.0",
"happy-dom": "^15.0.0"
}
}
if typescript:
deps["devDependencies"]["@types/node"] = "^22.0.0"
# Add coverage tools
deps["devDependencies"]["@vitest/ui"] = "^2.0.0"
deps["devDependencies"]["@vitest/coverage-v8"] = "^2.0.0"
return deps
def generate_scripts() -> dict:
"""Generate package.json scripts for testing."""
return {
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:watch": "vitest --watch",
"test:coverage": "vitest --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug",
"test:e2e:report": "playwright show-report"
}
}
def main():
parser = argparse.ArgumentParser(
description="Generate testing dependencies for Next.js projects"
)
parser.add_argument(
"--nextjs-version",
default="14",
help="Next.js version (default: 14)"
)
parser.add_argument(
"--typescript",
action="store_true",
help="Include TypeScript types"
)
parser.add_argument(
"--output",
choices=["json", "install"],
default="install",
help="Output format (json or install command)"
)
args = parser.parse_args()
deps = generate_dependencies(args.nextjs_version, args.typescript)
scripts = generate_scripts()
if args.output == "json":
output = {**deps, **scripts}
print(json.dumps(output, indent=2))
else:
# Generate install command
packages = " ".join(
f"{pkg}@{version}"
for pkg, version in deps["devDependencies"].items()
)
print(f"npm install -D {packages}")
print("\nAdd these scripts to package.json:")
print(json.dumps(scripts, indent=2))
if __name__ == "__main__":
main()