Web Application Testing
Comprehensive testing toolkit for the LucidData personal data bank MVP, integrating Playwright for end-to-end testing and Vitest for unit/integration testing.
Overview
This skill enables test-driven development (TDD) workflows for the LucidData Next.js application. It provides patterns for writing, running, and debugging tests across all layers: unit tests for utilities and services, integration tests for React components and API routes, and end-to-end tests for complete user workflows.
When to Use This Skill
Activate this skill when you need to:
- Run test suites: Execute unit, integration, or E2E tests
- Debug test failures: Investigate failing tests, review screenshots, check console logs
- Write new tests: Create test files following LucidData patterns
- Verify functionality: Test user flows like signup → vault creation → consent management
- Check test coverage: Generate and analyze coverage reports (80% target)
- Test responsive behavior: Validate UI across different viewports
- Validate accessibility: Ensure WCAG compliance through automated tests
LucidData Testing Philosophy
The project follows a test pyramid approach:
- Many unit tests: Fast, isolated tests for utilities, services, and pure functions
- Some integration tests: Tests for React components with hooks, API route handlers
- Few E2E tests: Critical user journeys (auth, core workflows)
Coverage targets: 80% for lines, functions, branches, and statements.
Test naming conventions:
- Unit/Integration:
*.test.tsor*.test.tsx - E2E:
*.spec.ts
Test Infrastructure
Vitest (Unit & Integration Testing)
Configuration: vitest.config.ts
Key settings:
- Environment:
jsdom(simulates browser DOM) - Setup file: test/setup.ts
- Global test functions:
describe,it,expectavailable everywhere - Excludes E2E tests:
__tests__/e2e/**directory excluded - Coverage: v8 provider with 80% thresholds
- Timeouts: 10000ms for tests and hooks
Test utilities:
- Mocks: test/mocks/prisma.ts, test/mocks/supabase.ts
- Fixtures: test/fixtures/ (sample data factories)
- Helpers: test/utils/ and test/helpers/
Libraries:
@testing-library/react: Component testing with queries and user events@testing-library/jest-dom: Custom matchers (e.g.,toBeInTheDocument())@testing-library/user-event: Simulate user interactionsvitest-mock-extended: Advanced mocking withDeepMockProxymsw: Mock Service Worker for API request interception
Playwright (E2E Testing)
Configuration: playwright.config.ts
Key settings:
- Test directory:
__tests__/e2e - Global setup: tests/e2e/global-setup.ts
- Global teardown: tests/e2e/global-teardown.ts
- Base URL:
http://localhost:3000 - Parallel execution: Enabled
- Browsers: Chromium, Firefox, WebKit, Mobile Chrome (Pixel 5), Mobile Safari (iPhone 12)
- Web server: Auto-starts Next.js dev server before tests
- Screenshots: On failure
- Videos: On first retry
- Traces: On first retry
Helpers:
- Auth helper: tests/e2e/helpers/auth.ts
login(),signup(),logout(),isAuthenticated(),getUniqueEmail(),clearSession()
- Data generators: tests/e2e/helpers/data-generators.ts
generateVaultEntry(),generateConsent(),generateVaultEntries(count)
Common Testing Patterns
Unit Testing Pattern (Vitest)
import { describe, it, expect, vi } from 'vitest';
describe('UtilityFunction', () => {
it('should perform expected operation', () => {
const result = utilityFunction('input');
expect(result).toBe('expected output');
});
it('should handle edge cases', () => {
const result = utilityFunction('');
expect(result).toBe('default value');
});
});
Example from LucidData (lib/crypto/tests/hashing.test.ts):
describe('hash', () => {
it('should produce SHA-256 hex string', () => {
const hashed = hash('test data');
expect(typeof hashed).toBe('string');
expect(hashed).toHaveLength(64);
expect(hashed).toMatch(/^[a-f0-9]{64}$/);
});
it('should be deterministic', () => {
const hash1 = hash('data');
const hash2 = hash('data');
expect(hash1).toBe(hash2);
});
});
Integration Testing Pattern (React Components)
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { vi } from 'vitest';
vi.mock('@/lib/db/prisma', () => ({
prisma: prismaMock,
}));
describe('ComponentName', () => {
it('should render correctly', () => {
render(<ComponentName />);
expect(screen.getByText('Expected Text')).toBeInTheDocument();
});
it('should handle user interaction', async () => {
const user = userEvent.setup();
render(<ComponentName />);
await user.click(screen.getByRole('button', { name: 'Submit' }));
expect(mockFunction).toHaveBeenCalled();
});
});
Example from LucidData (repository tests):
import { prismaMock } from '@/test/mocks/prisma';
describe('VaultRepository', () => {
beforeEach(() => {
repository = new VaultRepository();
vi.clearAllMocks();
});
it('should return vault entries for a user', async () => {
prismaMock.vaultData.findMany.mockResolvedValue(userEntries);
const result = await repository.findByUserId(userId);
expect(result).toEqual(userEntries);
expect(prismaMock.vaultData.findMany).toHaveBeenCalledWith({
where: { userId },
orderBy: { createdAt: 'desc' },
});
});
});
E2E Testing Pattern (Playwright)
import { test, expect } from '@playwright/test';
test.describe('Feature Flow', () => {
test.beforeEach(async ({ page }) => {
await clearSession(page);
});
test('should complete user workflow', async ({ page }) => {
await page.goto('/feature-page');
// Fill form
await page.fill('input[name="field"]', 'value');
await page.click('button[type="submit"]');
// Verify result
await expect(page.locator('text=Success')).toBeVisible();
});
});
Example from LucidData (tests/e2e/auth/signup.spec.ts):
test.describe('Signup Flow', () => {
test('should successfully create account', async ({ page }) => {
await page.goto('/signup');
const email = getUniqueEmail();
await page.fill('input[name="email"]', email);
await page.fill('input[name="password"]', TEST_USER.password);
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard', { timeout: 10000 });
await expect(page.locator(`text=${email}`)).toBeVisible();
});
});
Test Commands
Unit & Integration Tests (Vitest)
# Watch mode (re-runs on file changes)
npm run test
# Single run (for CI/CD)
npm run test:run
# With coverage report
npm run test:coverage
# UI mode (visual test runner)
npm run test:ui
# Run specific test file
npx vitest run path/to/test.test.ts
# Run tests matching pattern
npx vitest run --grep "VaultRepository"
E2E Tests (Playwright)
# Headless mode (no browser UI)
npm run test:e2e
# Headed mode (see browser)
npm run test:e2e:headed
# UI mode (interactive)
npm run test:e2e:ui
# Debug mode (step through tests)
npm run test:e2e:debug
# Run specific test file
npx playwright test __tests__/e2e/auth/signup.spec.ts
# Run specific browser
npx playwright test --project=chromium
# Run specific test by name
npx playwright test --grep "should successfully create account"
All Tests
# Run all test suites (unit, integration, E2E)
npm run test:all
Debugging Test Failures
Step-by-Step Debugging Process
- Read test output: Identify the failing test and assertion error
- Check screenshots: E2E failures auto-save screenshots in
test-results/ - Review browser logs: Playwright captures
console.log,console.errormessages - Use interactive debugging:
- Vitest: Add
vi.debug()to inspect values - Playwright: Add
await page.pause()for interactive debugging
- Vitest: Add
- Verify test fixtures/mocks: Ensure mock data matches expected structure
- Check environment variables: Verify
.env.testhas correct configuration - Isolate the issue: Run only the failing test with
--grepflag
Common Issues & Solutions
Issue: "Element not found" in E2E test
- Cause: Selector changed or element not rendered yet
- Solution: Use
await expect(element).toBeVisible()or increase timeout
Issue: "React Query mutation not called" in integration test
- Cause: Mock setup timing issue
- Solution: Use
await waitFor()from @testing-library/react
Issue: "Prisma method not mocked"
- Cause: Missing mock setup for database call
- Solution: Add
prismaMock.model.method.mockResolvedValue()in test setup
Issue: Test timeout
- Cause: Async operation taking too long
- Solution: Increase timeout in test config or use
{ timeout: 15000 }in specific test
LucidData-Specific Test Scenarios
Authentication Tests
- Signup flow: Create account → Redirect to dashboard
- Login flow: Enter credentials → Successful login
- Logout flow: Click logout → Redirect to landing page
- Protected routes: Access dashboard without auth → Redirect to login
Location: __tests__/e2e/auth/
Vault CRUD Tests
- Create vault entry: Fill form → Submit → Entry appears in list
- Read vault entry: Click entry → View dialog shows data
- Update vault entry: Edit form → Save → Changes reflected
- Delete vault entry: Click delete → Confirm → Entry removed
Location: __tests__/e2e/vault/
Consent Management Tests
- Grant consent: Create consent → Set permissions → Save
- View consent: Open consent list → See active/revoked consents
- Revoke consent: Click revoke → Confirm → Status updated
- Filter consents: Apply filters → List updates
Location: __tests__/e2e/consent/ (when implemented)
Audit Log Tests
- View audit log: Navigate to audit page → See log entries
- Filter by event type: Select filter → Entries update
- Verify hash chain: Check hash integrity across entries
Location: __tests__/e2e/audit/ (when implemented)
Responsive Design Tests
Test UI across viewports:
- Mobile: 375px, 390px (iPhone SE, iPhone 12)
- Tablet: 768px, 1024px (iPad, iPad Pro)
- Desktop: 1920px, 2560px (HD, QHD)
test('should be responsive on mobile', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/dashboard');
// Verify mobile layout
});
Accessibility Tests
- Keyboard navigation: Tab through forms, activate buttons with Enter
- Screen reader labels: Check ARIA attributes, alt text
- Color contrast: Automated checks with axe-core (future integration)
Test Data Management
Using Fixtures
Import pre-built data from test/fixtures/:
import { mockVaultEntry, createMockVaultEntries } from '@/test/fixtures/vault-data';
// Use single entry
const entry = mockVaultEntry;
// Generate multiple entries
const entries = createMockVaultEntries(5, 'user-id-123');
E2E Data Generators
For E2E tests, use helper functions to create unique test data:
import { generateVaultEntry } from '@/__tests__/e2e/helpers/data-generators';
const entry = generateVaultEntry({
label: 'Test Entry',
category: 'Identity',
});
Cleanup After E2E Tests
Global teardown (tests/e2e/global-teardown.ts) handles cleanup. For manual cleanup:
test.afterEach(async ({ page }) => {
// Delete test data created during test
await clearTestData(page);
});
Writing New Tests
1. Decide Test Type
- Unit test: Testing a single function/utility in isolation
- Integration test: Testing components with hooks, API routes with database calls
- E2E test: Testing complete user workflows across multiple pages
2. Choose Location
- Unit:
lib/**/__tests__/utility.test.ts - Integration:
components/**/__tests__/component.test.tsxorlib/**/__tests__/service.test.ts - E2E:
__tests__/e2e/feature/flow.spec.ts
3. Follow Existing Patterns
Look at similar existing tests for structure, mock setup, and assertion patterns.
4. Use Descriptive Test Names
// Good
it('should encrypt data with AES-256-GCM')
it('should redirect to login when not authenticated')
// Bad
it('works')
it('test encryption')
5. Arrange-Act-Assert (AAA) Pattern
it('should create vault entry', async () => {
// Arrange: Set up test data and mocks
const input = { label: 'Test', category: 'Identity' };
prismaMock.vaultData.create.mockResolvedValue(mockEntry);
// Act: Execute the function under test
const result = await vaultService.create(input);
// Assert: Verify the outcome
expect(result).toEqual(mockEntry);
expect(prismaMock.vaultData.create).toHaveBeenCalledWith({
data: expect.objectContaining(input),
});
});
Best Practices
- Isolate tests: Each test should be independent, not relying on other tests' state
- Use realistic data: Fixtures should resemble production data (but never use real PII)
- Mock external dependencies: Mock Prisma, Supabase, API calls for unit/integration tests
- Test error paths: Don't just test happy paths; verify error handling
- Keep tests fast: Unit tests should run in milliseconds, integration tests in seconds
- Avoid test pollution: Clean up mocks with
vi.clearAllMocks()inbeforeEach() - Use semantic queries: Prefer
getByRole,getByLabelTextovergetByTestId - Wait for async updates: Use
await waitFor()orawait expect().toBeVisible()for async operations
Coverage Analysis
Generate coverage report:
npm run test:coverage
Coverage report location: coverage/index.html (open in browser)
Interpreting coverage:
- Green: Above 80% threshold
- Yellow: 50-80% coverage
- Red: Below 50% coverage
Focus on:
- Lines: Percentage of code lines executed
- Functions: Percentage of functions called
- Branches: Percentage of if/else paths tested
- Statements: Percentage of statements executed
Note: 100% coverage doesn't guarantee bug-free code. Focus on testing critical paths and edge cases.
Environment Configuration
Test environment uses .env.test file:
NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
SUPABASE_SERVICE_ROLE_KEY=<test-key>
DATABASE_URL=postgresql://postgres:postgres@localhost:54322/postgres
ENCRYPTION_KEY=eugsb9sWIyEOcEg5AzHazv0k7CMTjSsGfL1lbLp8duU=
NEXT_PUBLIC_APP_URL=http://localhost:3000
NODE_ENV=test
Important: Never commit real keys or sensitive data to version control.
References
For more detailed information, see:
- Playwright Patterns - Page objects, selectors, authentication helpers
- Vitest Patterns - Component testing, mocking strategies, async patterns
- Test Data - Fixture structure, sample data, test credentials
Unified Test Runner Script
For convenience, use the run-tests.sh script:
# Run unit tests
./scripts/run-tests.sh unit
# Run E2E tests
./scripts/run-tests.sh e2e
# Run all tests
./scripts/run-tests.sh all
# With coverage
./scripts/run-tests.sh unit --coverage
Quick Reference
| Task | Command | Notes |
|---|---|---|
| Run unit tests (watch) | npm run test |
Re-runs on file changes |
| Run unit tests (once) | npm run test:run |
For CI/CD |
| Coverage report | npm run test:coverage |
80% threshold |
| Vitest UI | npm run test:ui |
Visual test runner |
| Run E2E tests | npm run test:e2e |
Headless mode |
| E2E headed mode | npm run test:e2e:headed |
See browser |
| E2E UI mode | npm run test:e2e:ui |
Interactive debugging |
| E2E debug mode | npm run test:e2e:debug |
Step through tests |
| Run all tests | npm run test:all |
Unit + E2E |
| Specific test file | npx vitest run path/to/test.test.ts |
Single file |
| Specific E2E test | npx playwright test path/to/test.spec.ts |
Single E2E file |
Version: 1.0 Last Updated: 2026-01-13 Maintained by: LucidData Team