Name: Project Delivery Toolkit (project-delivery-toolkit) Type: Single-Page Application (SPA) Purpose: Interactive data visualization tool for exploring barrier themes and resources in the Project Delivery Toolkit
Core Stack:
- React 19.1.1 + React DOM 19.1.1
- JavaScript (JSX) - No TypeScript
- Vite 7.1.7 (build tool & dev server)
UI & Visualization:
- TailwindCSS 3.4.17 (styling)
- Recharts 3.2.1 (donut chart visualization)
Data Processing:
- PapaParse 5.5.3 (client-side CSV parsing)
- csv-parse 6.1.0 (build-time CSV processing)
- node-fetch 3.3.2 (data fetching)
Code Quality:
- ESLint 9.36.0 with React plugins configured
❌ CRITICAL: ZERO TEST COVERAGE
- No test files exist
- No test framework installed
- No test scripts in package.json
- No test directories
- No coverage reporting tools
- No CI/CD testing pipeline
Source Files:
- Main Application:
src/App.jsx(694 lines) - MONOLITHIC COMPONENT - Entry Point:
src/main.jsx - Build Script:
scripts/build-data.mjs(53 lines) - Data Files:
src/data/*.json(generated at build time)
Key Concerns:
- All business logic concentrated in single 694-line component
- Mixed concerns: UI, state management, data transformation, URL handling
- Build-time data fetching from external Google Sheets
- No separation between pure functions and stateful logic
/\
/E2E\ 10% - End-to-End (Critical user flows)
/------\
/Integr-\ 20% - Integration (Component interactions)
/----------\
/----Unit----\ 70% - Unit (Pure functions, utilities)
/--------------\
Test pure functions and isolated utilities:
- Color manipulation (
lightenfunction in App.jsx:21-31) - Data normalization (
toArray,normalizeResourcein App.jsx:35-42) - CSV parsing and transformation (build-data.mjs)
- Data filtering logic
- URL parameter parsing/serialization
Test React components in isolation:
- App component rendering
- Interactive donut chart segments
- Search input behavior
- Filter checkboxes (personas)
- Theme/barrier selection logic
- Responsive header behavior
Test component interactions and data flow:
- Search filtering + persona filtering combined
- Theme selection → barrier clearing behavior
- URL state synchronization
- Data loading and parsing
- Chart visualization with real data structures
Test critical user workflows:
- Landing page → select theme → view filtered resources
- Search for resource → apply persona filter
- Share URL with filters → page loads with correct state
- Mobile responsive navigation
- Build process and data fetching
Why Vitest?
- Native Vite integration (already using Vite)
- Jest-compatible API (familiar syntax)
- Fast, modern, ESM-first
- Built-in coverage with c8
- Excellent React testing support
Alternative: Jest (more mature, larger ecosystem)
Why RTL?
- Tests components from user perspective
- Encouraged by React team
- Prevents implementation detail testing
- Excellent accessibility testing features
- Works seamlessly with Vitest
Alternative: Enzyme (older, less maintained)
Why Playwright?
- Modern, fast, reliable
- Cross-browser testing (Chromium, Firefox, WebKit)
- Auto-waiting and retry mechanisms
- Built-in screenshot/video recording
- Excellent debugging tools
Alternatives: Cypress, Puppeteer
Priority: CRITICAL
-
Setup Testing Infrastructure
- Install Vitest + React Testing Library
- Configure test environment
- Add test scripts to package.json
- Setup coverage reporting
-
Extract & Test Pure Functions
- Extract
lightencolor function → test - Extract
toArray,normalizeResource→ test - Extract URL parsing logic → test
- Target: 20+ unit tests, 90%+ coverage on utilities
- Extract
-
Test Build Script
- Test CSV parsing in build-data.mjs
- Mock fetch calls
- Test data normalization
- Test error handling (missing env vars)
Priority: HIGH
-
Test Core UI Components
- Render App with default state
- Test search input updates
- Test persona checkbox toggling
- Test clear filters button
-
Test Interactive Chart
- Test donut chart rendering
- Test theme segment clicks
- Test barrier segment clicks
- Test single-selection behavior
-
Test Resource Display
- Test resource filtering by search
- Test filtering by personas
- Test filtering by theme/barrier
- Test combined filters
Target: 40+ component tests, 70%+ coverage on App.jsx
Priority: MEDIUM
-
Test Data Flow
- Load real JSON data fixtures
- Test end-to-end filtering pipeline
- Test URL state persistence
- Test dynamic header height calculations
-
Test Edge Cases
- Empty search results
- No matching filters
- Malformed URL parameters
- Missing data fields
- Pipe-delimited parsing variations
Target: 20+ integration tests
Priority: MEDIUM
-
Setup Playwright
- Install and configure
- Create test fixtures
-
Test Critical User Flows
- Load page → verify chart renders
- Click theme → verify resources filtered
- Search + filter → verify combined results
- Copy URL → open in new tab → verify state restored
- Mobile viewport testing
Target: 8-12 E2E tests covering happy paths
Priority: ENHANCEMENT
- Enable Safe Refactoring
- Use tests to split App.jsx into smaller components
- Extract custom hooks (useURLState, useResourceFilter)
- Extract chart logic into separate module
- Maintain 100% passing tests during refactor
-
Data Normalization (App.jsx:35-42, build-data.mjs:32-44)
- Pipe-delimited field parsing
- CSV → JSON transformation
- Fallback handling for missing fields
-
Single-Selection Logic (App.jsx:86-95)
- Theme selection clears barrier
- Barrier selection clears theme
- Toggle deselection behavior
-
Resource Filtering (App.jsx:101-150)
- Search text matching
- Persona filtering
- Theme/barrier filtering
- Combined filter logic
-
URL State Management (App.jsx:64-83)
- Reading URL params on load
- Writing state to URL
- Handling malformed params
-
Color Utilities (App.jsx:20-31)
- Lighten function correctness
- Hex parsing variations (3-char vs 6-char)
-
Chart Rendering
- Correct data structure for Recharts
- Segment click handlers
- Hover states
-
Build Process
- Environment variable validation
- Network error handling
- File write operations
-
Responsive Behavior
- Dynamic header height
- Mobile layout
- Font size adjustments
-
Accessibility
- Keyboard navigation
- ARIA labels
- Focus management
npm install -D vitest @vitest/ui @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdomFile: vitest.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.js',
coverage: {
provider: 'c8',
reporter: ['text', 'json', 'html', 'lcov'],
exclude: [
'node_modules/',
'src/test/',
'*.config.js',
'scripts/',
'dist/'
],
thresholds: {
lines: 75,
functions: 75,
branches: 70,
statements: 75
}
}
}
});File: src/test/setup.js
import '@testing-library/jest-dom';
import { expect, afterEach, vi } from 'vitest';
import { cleanup } from '@testing-library/react';
// Cleanup after each test
afterEach(() => {
cleanup();
});
// 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(),
})),
});{
"scripts": {
"dev": "vite",
"lint": "eslint .",
"preview": "vite preview",
"prebuild": "node scripts/build-data.mjs",
"build": "node scripts/build-data.mjs && vite build",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"test:run": "vitest run"
}
}src/
├── test/
│ ├── setup.js # Global test setup
│ ├── fixtures/
│ │ ├── mockResources.json # Test data fixtures
│ │ ├── mockBarriers.json
│ │ └── mockThemes.json
│ ├── utils/
│ │ └── testHelpers.js # Custom render functions
│ └── __tests__/
│ ├── unit/
│ │ ├── utils.test.js # Pure function tests
│ │ └── dataTransform.test.js
│ ├── component/
│ │ ├── App.test.jsx # Component tests
│ │ └── Chart.test.jsx
│ └── integration/
│ ├── filtering.test.jsx # Integration tests
│ └── urlState.test.jsx
scripts/
└── __tests__/
└── build-data.test.mjs # Build script tests
e2e/
├── playwright.config.js
└── tests/
├── userFlows.spec.js
└── responsive.spec.js
File: src/test/__tests__/unit/utils.test.js
import { describe, it, expect } from 'vitest';
// Extract these functions from App.jsx first
function lighten(hex, amt = 0.3) {
let c = hex?.replace("#", "") || "64748b";
if (c.length === 3) c = c.split("").map(ch => ch + ch).join("");
const n = parseInt(c, 16);
let r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
r = Math.min(255, Math.round(r + (255 - r) * amt));
g = Math.min(255, Math.round(g + (255 - g) * amt));
b = Math.min(255, Math.round(b + (255 - b) * amt));
const h = (v) => v.toString(16).padStart(2, "0");
return `#${h(r)}${h(g)}${h(b)}`;
}
const toArray = (v) =>
Array.isArray(v)
? v
: (typeof v === "string" ? v.split("|").map(s => s.trim()).filter(Boolean) : []);
describe('lighten', () => {
it('should lighten a 6-character hex color', () => {
const result = lighten('#2563eb', 0.3);
expect(result).toMatch(/^#[0-9a-f]{6}$/i);
expect(result).not.toBe('#2563eb');
});
it('should handle 3-character hex colors', () => {
const result = lighten('#abc', 0.3);
expect(result).toMatch(/^#[0-9a-f]{6}$/i);
});
it('should handle missing # prefix', () => {
const result = lighten('2563eb', 0.3);
expect(result).toMatch(/^#[0-9a-f]{6}$/i);
});
it('should return default color for null input', () => {
const result = lighten(null, 0.3);
expect(result).toMatch(/^#[0-9a-f]{6}$/i);
});
});
describe('toArray', () => {
it('should split pipe-delimited string', () => {
expect(toArray('Project|Programme|Business')).toEqual(['Project', 'Programme', 'Business']);
});
it('should trim whitespace', () => {
expect(toArray(' Project | Programme ')).toEqual(['Project', 'Programme']);
});
it('should return array unchanged', () => {
expect(toArray(['Project', 'Programme'])).toEqual(['Project', 'Programme']);
});
it('should return empty array for empty string', () => {
expect(toArray('')).toEqual([]);
});
it('should filter empty values', () => {
expect(toArray('Project||Business')).toEqual(['Project', 'Business']);
});
});File: src/test/__tests__/component/App.test.jsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from '../../App';
describe('App Component', () => {
it('should render without crashing', () => {
render(<App />);
expect(screen.getByText(/Project Delivery Toolkit/i)).toBeInTheDocument();
});
it('should update search input', async () => {
const user = userEvent.setup();
render(<App />);
const searchInput = screen.getByPlaceholderText(/search/i);
await user.type(searchInput, 'test query');
expect(searchInput).toHaveValue('test query');
});
it('should toggle persona filter', async () => {
const user = userEvent.setup();
render(<App />);
const projectCheckbox = screen.getByLabelText(/project/i);
await user.click(projectCheckbox);
expect(projectCheckbox).toBeChecked();
});
it('should clear all filters', async () => {
const user = userEvent.setup();
render(<App />);
const searchInput = screen.getByPlaceholderText(/search/i);
await user.type(searchInput, 'test');
const clearButton = screen.getByText(/clear/i);
await user.click(clearButton);
expect(searchInput).toHaveValue('');
});
});File: scripts/__tests__/build-data.test.mjs
import { describe, it, expect, vi, beforeEach } from 'vitest';
import fetch from 'node-fetch';
vi.mock('node-fetch');
describe('build-data script', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should parse CSV correctly', async () => {
const mockCsv = 'id,title,url\n1,Test,http://test.com';
fetch.mockResolvedValueOnce({
text: async () => mockCsv
});
// Test CSV parsing logic
const { parse } = await import('csv-parse/sync');
const result = parse(mockCsv, { columns: true, skip_empty_lines: true });
expect(result).toHaveLength(1);
expect(result[0]).toEqual({ id: '1', title: 'Test', url: 'http://test.com' });
});
it('should split pipe-delimited values', () => {
const splitPipes = (s) =>
s ? s.split('|').map(v => v.trim()).filter(Boolean) : [];
expect(splitPipes('Project|Programme')).toEqual(['Project', 'Programme']);
expect(splitPipes('')).toEqual([]);
expect(splitPipes(null)).toEqual([]);
});
});npm install -D @playwright/test
npx playwright installFile: playwright.config.js
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
});File: e2e/tests/userFlows.spec.js
import { test, expect } from '@playwright/test';
test('should load and display donut chart', async ({ page }) => {
await page.goto('/');
await expect(page.locator('svg')).toBeVisible();
});
test('should filter resources by search', async ({ page }) => {
await page.goto('/');
await page.fill('input[type="search"]', 'data');
await expect(page.locator('.resource-item')).toHaveCount(expect.any(Number));
});
test('should persist filters in URL', async ({ page }) => {
await page.goto('/');
await page.fill('input[type="search"]', 'test');
const url = page.url();
expect(url).toContain('q=test');
});
test('should select theme and update resources', async ({ page }) => {
await page.goto('/');
// Click on a theme segment
await page.locator('svg path').first().click();
// Verify URL updated
await expect(page).toHaveURL(/theme=/);
});Overall Project: 75%+ coverage
Breakdown by module:
| Module | Target Coverage | Priority | Rationale |
|---|---|---|---|
| Pure utility functions | 95%+ | Critical | Easy to test, zero dependencies |
| Data transformation | 90%+ | Critical | Core business logic |
| Build script | 80%+ | High | Affects all builds |
| App.jsx business logic | 75%+ | High | Core application |
| React rendering | 60%+ | Medium | Some boilerplate OK to skip |
| Chart visualization | 60%+ | Medium | Library handles much |
| CSS/Styles | 0% | N/A | Not measured |
# Run tests with coverage
npm run test:coverage
# Generate HTML report
npm run test:coverage -- --reporter=html
# Open coverage report
open coverage/index.html # macOS
start coverage/index.html # WindowsCoverage thresholds are configured in vitest.config.js:
coverage: {
thresholds: {
lines: 75,
functions: 75,
branches: 70,
statements: 75
}
}Tests will fail if coverage drops below these thresholds.
-
Baseline Measurement
- Run initial coverage after Phase 1
- Document starting point (likely 0%)
- Set incremental goals (20%, 40%, 60%, 75%)
-
CI/CD Integration
- Coverage reports generated on every PR
- Block PRs that decrease coverage
- Track trends over time
-
Coverage Badges
- Add coverage badge to README
- Update automatically via CI/CD
- Visible indicator of project health
Add to GitHub Actions workflow (.github/workflows/test.yml):
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run lint
- run: npm run test:coverage
- uses: codecov/codecov-action@v4
with:
files: ./coverage/lcov.info
- name: Coverage Comment
uses: romeovs/lcov-reporter-action@v0.3.1
with:
lcov-file: ./coverage/lcov.info
github-token: ${{ secrets.GITHUB_TOKEN }}-
Test Behavior, Not Implementation
- Focus on what users see and do
- Avoid testing internal state directly
- Use accessible queries (getByRole, getByLabelText)
-
Arrange-Act-Assert Pattern
it('should filter resources by persona', () => { // Arrange: Setup test data const resources = [/* ... */]; // Act: Perform action render(<App />); userEvent.click(screen.getByLabelText('Project')); // Assert: Verify outcome expect(screen.getAllByRole('article')).toHaveLength(5); });
-
Test One Thing Per Test
- Each test should verify single behavior
- Makes failures easier to diagnose
- Improves test maintainability
// Good test names
describe('Resource filtering', () => {
it('should show only resources matching search term', () => {});
it('should show all resources when search is empty', () => {});
it('should be case-insensitive', () => {});
});
// Bad test names
describe('Tests', () => {
it('works', () => {});
it('test 2', () => {});
});-
Create Realistic Fixtures
- Use subset of real data structure
- Include edge cases (null, empty arrays, special chars)
-
Centralize Test Data
- Store in
src/test/fixtures/ - Reuse across multiple tests
- Keep DRY
- Store in
-
Mock External Dependencies
vi.mock('./data/resources.json', () => ({ default: mockResources }));
// Use debug utilities
import { render, screen, debug } from '@testing-library/react';
test('example', () => {
render(<App />);
screen.debug(); // Prints DOM
screen.logTestingPlaygroundURL(); // Opens playground
});-
Parallelize Tests
- Vitest runs tests in parallel by default
- Keep tests isolated and independent
-
Use beforeEach Wisely
- Reset state between tests
- Don't overuse expensive setup
-
Lazy Load Heavy Dependencies
const { someHeavyFunction } = await import('./heavy');
// BAD - Testing internal state
expect(wrapper.state('selectedTheme')).toBe('leadership');
// GOOD - Testing user-visible behavior
expect(screen.getByText('Leadership & Alignment')).toHaveClass('selected');// BAD - Side effects leak between tests
it('test 1', () => {
localStorage.setItem('theme', 'dark');
// ... test code
});
// GOOD - Clean up after each test
afterEach(() => {
localStorage.clear();
cleanup();
});// BAD - Brittle, breaks on refactoring
screen.getByTestId('submit-button');
// GOOD - User-centric, accessible
screen.getByRole('button', { name: /submit/i });// BAD - Race condition
userEvent.click(button);
expect(screen.getByText('Success')).toBeInTheDocument();
// GOOD - Wait for async update
await userEvent.click(button);
await waitFor(() => {
expect(screen.getByText('Success')).toBeInTheDocument();
});// BAD - Mocking everything defeats the purpose
vi.mock('recharts', () => ({ Pie: () => null }));
vi.mock('./data/resources.json', () => ({ default: [] }));
// GOOD - Only mock external dependencies (network, timers)
vi.mock('node-fetch');// BAD - Brittle, hard to review changes
expect(wrapper).toMatchSnapshot();
// GOOD - Test specific behavior
expect(screen.getByRole('heading')).toHaveTextContent('Project Delivery Toolkit');-
Use React Testing Library's
user-eventoverfireEvent// Simulates real user interactions better await userEvent.click(button);
-
Test Concurrent Features Carefully
- React 19 has concurrent rendering
- Use
waitForfor state updates - Avoid
act()warnings
-
Test Hooks with
renderHookimport { renderHook } from '@testing-library/react'; const { result } = renderHook(() => useCustomHook());
-
ESM Module Mocking
// Use vi.mock with factory function vi.mock('./module', () => ({ default: mockImplementation }));
-
Environment Variables
// Mock in test setup vi.stubEnv('RESOURCES_CSV_URL', 'http://mock.com');
-
Fast Refresh Compatible
- Vitest supports HMR during development
- Run
npm run testin watch mode
-
Don't Test Tailwind Classes
// BAD expect(element).toHaveClass('bg-blue-500 text-white'); // GOOD - Test computed styles if needed expect(element).toHaveStyle({ backgroundColor: 'rgb(59, 130, 246)' });
-
Use Semantic Queries
- Test accessibility, not styling
- Use
getByRole,getByLabelText
-
Mock Recharts for Unit Tests
vi.mock('recharts', () => ({ PieChart: ({ children }) => <div data-testid="pie-chart">{children}</div>, Pie: ({ data }) => <div data-testid="pie">{data.length} segments</div>, Cell: ({ fill }) => <div style={{ fill }} />, }));
-
Test Data Passed to Chart
const { container } = render(<App />); const pie = container.querySelector('[data-testid="pie"]'); expect(pie).toHaveTextContent('6 segments');
-
Use E2E for Visual Testing
- Playwright for screenshot comparisons
- Test chart renders correctly in real browser
-
Data Variations
- Empty resources array
- Missing required fields (title, url, id)
- Null/undefined values
- Pipe-delimited fields with extra spacing
- Single vs multiple personas/barriers
-
URL State
- Malformed query parameters
- Invalid theme/barrier IDs
- Special characters in search
- Very long search queries
- Multiple parameters combined
-
User Interactions
- Rapid clicking (debounce/throttle)
- Selecting theme then barrier (single-selection)
- Clearing filters with no filters applied
- Searching with no results
- Resizing viewport during interaction
-
Build Process
- Missing environment variables
- Network timeouts
- Invalid CSV format
- Empty CSV files
- BOM (Byte Order Mark) handling
-
Accessibility
- Keyboard-only navigation
- Screen reader announcements
- Focus trap in modals
- Color contrast ratios
- Touch target sizes (mobile)
describe('Edge cases', () => {
it('should handle resources with missing personas field', () => {
const resource = { id: '1', title: 'Test', personas: undefined };
const normalized = normalizeResource(resource);
expect(normalized.personas).toEqual([]);
});
it('should handle empty search results gracefully', () => {
render(<App />);
userEvent.type(screen.getByRole('searchbox'), 'xyznonexistent');
expect(screen.getByText(/no resources found/i)).toBeInTheDocument();
});
it('should handle network failure during build', async () => {
fetch.mockRejectedValueOnce(new Error('Network error'));
await expect(main()).rejects.toThrow();
});
it('should handle malformed URL parameters', () => {
window.history.pushState({}, '', '?theme=invalid&barrier=<script>');
render(<App />);
// Should not crash, should sanitize or ignore
expect(screen.getByRole('main')).toBeInTheDocument();
});
it('should handle very long search queries', async () => {
const longQuery = 'a'.repeat(1000);
render(<App />);
await userEvent.type(screen.getByRole('searchbox'), longQuery);
expect(screen.getByRole('searchbox')).toHaveValue(longQuery);
});
});-
Keep Tests Up-to-Date
- Update tests when features change
- Refactor tests alongside code
- Remove obsolete tests
-
Monitor Test Health
- Track flaky tests
- Keep test execution time < 30s for unit/component
- Fix broken tests immediately
-
Review Coverage Reports Monthly
- Identify untested code paths
- Prioritize high-risk areas
- Set incremental coverage goals
When reviewing PRs, verify:
- New features include tests
- Tests follow naming conventions
- Coverage doesn't decrease
- Tests are isolated and don't depend on order
- Mocks are cleaned up after tests
- E2E tests updated for UI changes
- Edge cases are tested
- Tests pass in CI/CD pipeline
Tests enable safe refactoring:
-
Extract Components from App.jsx
- Write tests for current behavior
- Extract component (e.g.,
<SearchBar />) - Run tests to verify behavior unchanged
- Refactor component tests
-
Extract Custom Hooks
- Test current App.jsx behavior
- Extract hook (e.g.,
useURLState) - Test hook with
renderHook - Verify App.jsx tests still pass
-
Performance Optimizations
- Establish performance baseline with tests
- Apply optimization (e.g., useMemo, useCallback)
- Verify tests pass
- Benchmark performance improvement
| Phase | Duration | Effort | Dependencies |
|---|---|---|---|
| Phase 1: Foundation | 2 weeks | 40 hours | None |
| Phase 2: Component Testing | 2 weeks | 40 hours | Phase 1 |
| Phase 3: Integration Testing | 2 weeks | 30 hours | Phase 2 |
| Phase 4: E2E Testing | 1 week | 20 hours | Phase 3 |
| Phase 5: Refactoring | Ongoing | Variable | Phase 2 |
Total: 7-8 weeks, ~130 hours
Option A: Dedicated QA Engineer
- 1 QA engineer full-time for 8 weeks
- Writes all tests, establishes patterns
- Trains development team
Option B: Shared Responsibility
- All developers write tests for their features
- 1 person owns test infrastructure setup (Week 1)
- Code review ensures test quality
Track these KPIs:
- Test coverage percentage (target: 75%+)
- Number of tests (target: 80+ total)
- Test execution time (target: <60s for full suite)
- Bug escape rate (bugs found in production)
- Deployment confidence score (team survey)
Risk: 694-line component is hard to test comprehensively
Mitigation:
- Start with unit tests for pure functions
- Test component behavior via user interactions
- Plan gradual refactoring once tests are in place
Risk: Build script fetches from Google Sheets; tests need mocks
Mitigation:
- Mock
node-fetchin tests - Create realistic fixture data
- Test both success and failure scenarios
- Consider snapshot testing for data structures
Risk: Recharts rendering is complex to test
Mitigation:
- Focus on data passed to Recharts, not rendering
- Test click handlers and callbacks
- Use E2E tests for visual verification
- Consider visual regression testing
Risk: Team may be unfamiliar with testing tools
Mitigation:
- Provide training sessions
- Create example tests as templates
- Pair programming for first tests
- Document testing patterns in TESTING.md
Risk: Tests become outdated and start failing
Mitigation:
- Enforce test updates in code reviews
- Make test failures block deployments
- Keep tests simple and focused
- Avoid testing implementation details
-
Get Buy-In
- Share this proposal with team
- Discuss timeline and resource allocation
- Identify test champions
-
Setup Environment
- Install Vitest and React Testing Library
- Configure vitest.config.js
- Add test scripts to package.json
-
Write First Tests
- Test
lightenfunction (easiest win) - Test
toArrayutility - Verify test runner works
- Test
- Complete testing infrastructure setup
- Write 10+ utility function tests
- Achieve 90%+ coverage on extracted utils
- Document testing patterns in TESTING.md
- 50+ total tests written
- 60%+ overall coverage
- Component tests for App.jsx
- CI/CD pipeline running tests
- 80+ total tests
- 75%+ overall coverage
- E2E tests for critical flows
- Begin refactoring App.jsx with test safety net
- Vitest Documentation
- React Testing Library
- Playwright Documentation
- Testing JavaScript (Kent C. Dodds)
- Workshop 1: Introduction to Vitest & RTL (2 hours)
- Workshop 2: Writing Effective Component Tests (2 hours)
- Workshop 3: E2E Testing with Playwright (2 hours)
- Workshop 4: Test-Driven Development (TDD) (2 hours)
This Project Delivery Toolkit is a well-structured, modern React application that currently has zero test coverage. Implementing this testing proposal will:
✅ Reduce bugs by catching issues before production ✅ Increase confidence when deploying and refactoring ✅ Improve code quality through testable design ✅ Enable safe refactoring of the monolithic App.jsx ✅ Accelerate development with faster feedback loops
The recommended approach uses Vitest + React Testing Library for unit/component tests and Playwright for E2E tests, leveraging the existing Vite build infrastructure.
Priority: Start with Phase 1 (Foundation) immediately, focusing on extracting and testing pure functions. This provides quick wins and establishes testing patterns for the team.
Key Success Factor: Commit to writing tests for all new features going forward. Treat tests as first-class citizens in code reviews.
Document Version: 1.0 Created: 2025-10-28 Last Updated: 2025-10-28 Author: Testing Proposal Analysis