Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal 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
3
README.md
Normal 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
77
plugin.lock.json
Normal 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": []
|
||||
}
|
||||
}
|
||||
333
skills/testing-next-stack/SKILL.md
Normal file
333
skills/testing-next-stack/SKILL.md
Normal 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
|
||||
128
skills/testing-next-stack/assets/examples/component-test.tsx
Normal file
128
skills/testing-next-stack/assets/examples/component-test.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
196
skills/testing-next-stack/assets/examples/e2e-test.ts
Normal file
196
skills/testing-next-stack/assets/examples/e2e-test.ts
Normal 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([])
|
||||
})
|
||||
})
|
||||
86
skills/testing-next-stack/assets/examples/unit-test.ts
Normal file
86
skills/testing-next-stack/assets/examples/unit-test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
50
skills/testing-next-stack/assets/playwright.config.ts
Normal file
50
skills/testing-next-stack/assets/playwright.config.ts
Normal 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
|
||||
}
|
||||
})
|
||||
58
skills/testing-next-stack/assets/test-setup.ts
Normal file
58
skills/testing-next-stack/assets/test-setup.ts
Normal 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
|
||||
45
skills/testing-next-stack/assets/vitest.config.ts
Normal file
45
skills/testing-next-stack/assets/vitest.config.ts
Normal 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')
|
||||
}
|
||||
}
|
||||
})
|
||||
315
skills/testing-next-stack/references/a11y-testing.md
Normal file
315
skills/testing-next-stack/references/a11y-testing.md
Normal 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/)
|
||||
93
skills/testing-next-stack/scripts/generate_test_deps.py
Normal file
93
skills/testing-next-stack/scripts/generate_test_deps.py
Normal 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()
|
||||
Reference in New Issue
Block a user