# ๐งช Comprehensive Testing Plan for Brackt.com
**Created:** October 20, 2025
**Status:** Planned
## **Overview**
This plan sets up a complete testing infrastructure with:
- **Vitest + React Testing Library** - Unit and component tests
- **Cypress** - End-to-end integration tests
- **Test fixtures** - Reusable test data
- **CI/CD Integration** - Automated testing in GitHub Actions
- **CI-ready** - Scripts for automated testing
---
## **Phase 1: Vitest + React Testing Library Setup**
### **1.1 Installation**
```bash
npm install -D vitest @vitest/ui @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
```
### **1.2 Configuration Files**
**`vitest.config.ts`**
```typescript
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./app/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'app/test/'],
},
},
resolve: {
alias: {
'~': path.resolve(__dirname, './app'),
},
},
});
```
**`app/test/setup.ts`**
```typescript
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
afterEach(() => {
cleanup();
});
```
### **1.3 Example Unit Tests**
**`app/models/__tests__/league.test.ts`**
```typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { createLeague, updateLeague, findLeagueById } from '../league';
// Mock database
vi.mock('~/database/context', () => ({
database: vi.fn(() => ({
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnValue({
returning: vi.fn().mockResolvedValue([{
id: 'league-1',
name: 'Test League',
createdBy: 'user-1',
isPublicDraftBoard: false,
}]),
}),
}),
query: {
leagues: {
findFirst: vi.fn(),
},
},
})),
}));
describe('League Model', () => {
it('should create a new league', async () => {
const league = await createLeague({
name: 'Test League',
createdBy: 'user-1',
});
expect(league).toMatchObject({
name: 'Test League',
createdBy: 'user-1',
});
});
it('should validate league name length', async () => {
// Test validation logic
expect(() => {
if ('ab'.length < 3) throw new Error('Name too short');
}).toThrow('Name too short');
});
});
```
### **1.4 Component Tests**
**`app/components/__tests__/DraftGrid.test.tsx`**
```typescript
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { DraftGrid } from '../DraftGrid';
describe('DraftGrid Component', () => {
const mockSlots = [
{ id: '1', draftOrder: 1, team: { id: 't1', name: 'Team 1' } },
{ id: '2', draftOrder: 2, team: { id: 't2', name: 'Team 2' } },
];
const mockGrid = [
[
{ participant: { name: 'Player 1' }, sport: { name: 'NFL' } },
null,
],
];
it('should render team names', () => {
render(
);
expect(screen.getByText('Team 1')).toBeInTheDocument();
expect(screen.getByText('Team 2')).toBeInTheDocument();
});
it('should highlight current pick', () => {
render(
);
expect(screen.getByText('On Clock')).toBeInTheDocument();
});
it('should display picked players', () => {
render(
);
expect(screen.getByText('Player 1')).toBeInTheDocument();
expect(screen.getByText('NFL')).toBeInTheDocument();
});
});
```
---
## **Phase 2: Cypress E2E Testing Setup**
### **2.1 Installation**
```bash
npm install -D cypress @testing-library/cypress
```
### **2.2 Configuration**
**`cypress.config.ts`**
```typescript
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
setupNodeEvents(on, config) {
// implement node event listeners here
},
supportFile: 'cypress/support/e2e.ts',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
},
env: {
TEST_USER_EMAIL: 'test@example.com',
TEST_USER_PASSWORD: 'testpassword123',
},
});
```
**`cypress/support/e2e.ts`**
```typescript
import '@testing-library/cypress/add-commands';
// Custom commands
Cypress.Commands.add('login', (email: string, password: string) => {
cy.session([email, password], () => {
cy.visit('/sign-in');
cy.get('input[name="email"]').type(email);
cy.get('input[name="password"]').type(password);
cy.get('button[type="submit"]').click();
cy.url().should('not.include', '/sign-in');
});
});
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable;
}
}
}
```
### **2.3 E2E Test Examples**
**`cypress/e2e/league-creation.cy.ts`**
```typescript
describe('League Creation Flow', () => {
beforeEach(() => {
cy.login(Cypress.env('TEST_USER_EMAIL'), Cypress.env('TEST_USER_PASSWORD'));
});
it('should create a new league with all settings', () => {
cy.visit('/leagues/new');
// Fill in league details
cy.get('input[name="name"]').type('My Test League');
cy.get('select[name="teamCount"]').select('12');
cy.get('input[name="draftRounds"]').clear().type('20');
// Set draft speed
cy.get('select[name="draftSpeed"]').select('standard');
// Set draft date/time
cy.get('input[name="draftDate"]').type('2025-12-01');
cy.get('input[name="draftTime"]').type('19:00');
// Select sports (assuming checkboxes)
cy.findByLabelText(/NFL/i).check();
cy.findByLabelText(/NBA/i).check();
cy.findByLabelText(/MLB/i).check();
// Submit form
cy.get('button[type="submit"]').click();
// Verify redirect to league page
cy.url().should('include', '/leagues/');
cy.findByText('My Test League').should('exist');
});
it('should validate required fields', () => {
cy.visit('/leagues/new');
// Try to submit without filling anything
cy.get('button[type="submit"]').click();
// Should show validation errors
cy.findByText(/league name is required/i).should('exist');
});
});
```
**`cypress/e2e/league-settings.cy.ts`**
```typescript
describe('League Settings', () => {
beforeEach(() => {
cy.login(Cypress.env('TEST_USER_EMAIL'), Cypress.env('TEST_USER_PASSWORD'));
});
it('should update league name', () => {
// Assuming we have a test league
cy.visit('/leagues/test-league-id/settings');
cy.get('input[name="name"]').clear().type('Updated League Name');
cy.get('button[type="submit"]').contains('Save').click();
// Verify success
cy.findByText(/settings updated/i).should('exist');
cy.findByDisplayValue('Updated League Name').should('exist');
});
it('should toggle public draft board', () => {
cy.visit('/leagues/test-league-id/settings');
// Check the public draft board checkbox
cy.get('input[name="isPublicDraftBoard"]').check();
cy.get('button[type="submit"]').contains('Save').click();
// Verify it's checked after save
cy.get('input[name="isPublicDraftBoard"]').should('be.checked');
});
it('should change draft speed', () => {
cy.visit('/leagues/test-league-id/settings');
cy.get('select[name="draftSpeed"]').select('fast');
cy.get('button[type="submit"]').contains('Save').click();
// Verify selection persists
cy.get('select[name="draftSpeed"]').should('have.value', 'fast');
});
});
```
**`cypress/e2e/draft-room.cy.ts`**
```typescript
describe('Draft Room', () => {
beforeEach(() => {
cy.login(Cypress.env('TEST_USER_EMAIL'), Cypress.env('TEST_USER_PASSWORD'));
});
it('should display draft grid with all teams', () => {
cy.visit('/leagues/test-league-id/draft/test-season-id');
// Verify team headers are visible
cy.findByText('Team 1').should('exist');
cy.findByText('Team 2').should('exist');
// Verify draft grid is rendered
cy.get('[title*="Overall Pick"]').should('have.length.at.least', 12);
});
it('should show current pick highlighted', () => {
cy.visit('/leagues/test-league-id/draft/test-season-id');
// Current pick should have special styling
cy.findByText('On Clock').should('exist');
cy.findByText('On Clock')
.parent()
.should('have.class', 'border-blue-500');
});
it('should allow commissioner to start draft', () => {
cy.visit('/leagues/test-league-id/draft/test-season-id');
// Click start draft button
cy.findByRole('button', { name: /start draft/i }).click();
// Verify draft started
cy.findByText(/draft started/i).should('exist');
});
it('should make a pick', () => {
cy.visit('/leagues/test-league-id/draft/test-season-id');
// Search for a player
cy.get('input[placeholder*="Search"]').type('Aaron Judge');
// Click on player to select
cy.findByText('Aaron Judge').click();
// Confirm pick
cy.findByRole('button', { name: /confirm pick/i }).click();
// Verify pick appears in grid
cy.findByText('Aaron Judge').should('exist');
});
it('should add player to queue', () => {
cy.visit('/leagues/test-league-id/draft/test-season-id');
// Find a player and add to queue
cy.findByText('Mike Trout').parent().findByRole('button', { name: /add to queue/i }).click();
// Verify in queue section
cy.findByText(/your queue/i).parent().findByText('Mike Trout').should('exist');
});
it('should display timers for all teams', () => {
cy.visit('/leagues/test-league-id/draft/test-season-id');
// Start draft first
cy.findByRole('button', { name: /start draft/i }).click();
// Verify timers are displayed (format: MM:SS)
cy.get('[class*="font-mono"]').should('contain.text', ':');
});
it('should allow commissioner to pause/resume draft', () => {
cy.visit('/leagues/test-league-id/draft/test-season-id');
// Pause draft
cy.findByRole('button', { name: /pause draft/i }).click();
cy.findByText(/draft paused/i).should('exist');
// Resume draft
cy.findByRole('button', { name: /resume draft/i }).click();
cy.findByText(/draft resumed/i).should('exist');
});
});
```
---
## **Phase 3: Test Fixtures & Helpers**
**`app/test/fixtures/league.ts`**
```typescript
export const mockLeague = {
id: 'league-1',
name: 'Test League',
createdBy: 'user-1',
isPublicDraftBoard: false,
createdAt: new Date('2025-01-01'),
updatedAt: new Date('2025-01-01'),
};
export const mockSeason = {
id: 'season-1',
leagueId: 'league-1',
year: 2025,
status: 'pre_draft' as const,
draftRounds: 20,
draftInitialTime: 120,
draftIncrementTime: 15,
currentPickNumber: 1,
draftPaused: false,
};
export const mockTeams = [
{ id: 'team-1', name: 'Team 1', seasonId: 'season-1', ownerId: 'user-1' },
{ id: 'team-2', name: 'Team 2', seasonId: 'season-1', ownerId: 'user-2' },
];
```
**`cypress/support/commands/database.ts`**
```typescript
// Helper to seed test database
Cypress.Commands.add('seedDatabase', () => {
cy.task('db:seed');
});
Cypress.Commands.add('cleanDatabase', () => {
cy.task('db:clean');
});
```
---
## **Phase 4: Package.json Scripts**
```json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"test:e2e": "cypress open",
"test:e2e:headless": "cypress run",
"test:all": "npm run test && npm run test:e2e:headless"
}
}
```
---
## **Phase 5: GitHub Actions CI/CD Integration**
### **5.1 Update Deploy Workflow**
**`.github/workflows/deploy.yml`** (Updated)
```yaml
name: Deploy to Production
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
# Run tests first
test:
name: Run Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: brackt_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run database migrations
run: npm run db:migrate
env:
DATABASE_URL: postgresql://test:test@localhost:5432/brackt_test
- name: Run unit tests
run: npm run test -- --run
env:
DATABASE_URL: postgresql://test:test@localhost:5432/brackt_test
- name: Run E2E tests
run: npm run test:e2e:headless
env:
DATABASE_URL: postgresql://test:test@localhost:5432/brackt_test
CYPRESS_BASE_URL: http://localhost:3000
- name: Upload test coverage
uses: codecov/codecov-action@v3
if: always()
with:
files: ./coverage/coverage-final.json
flags: unittests
- name: Upload Cypress videos
uses: actions/upload-artifact@v3
if: failure()
with:
name: cypress-videos
path: cypress/videos
# Build and deploy only if tests pass
deploy:
name: Deploy to Netlify
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
env:
NODE_ENV: production
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v2
with:
publish-dir: './build/client'
production-branch: main
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy from GitHub Actions"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
```
### **5.2 Add PR Check Workflow**
**`.github/workflows/pr-checks.yml`** (New)
```yaml
name: PR Checks
on:
pull_request:
branches:
- main
- develop
jobs:
lint-and-test:
name: Lint and Test
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: brackt_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
continue-on-error: true
- name: Run type check
run: npm run typecheck
continue-on-error: true
- name: Run unit tests
run: npm run test -- --run
env:
DATABASE_URL: postgresql://test:test@localhost:5432/brackt_test
- name: Comment test results
uses: actions/github-script@v6
if: always()
with:
script: |
const fs = require('fs');
const coverage = JSON.parse(fs.readFileSync('./coverage/coverage-summary.json', 'utf8'));
const total = coverage.total;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Test Results\n\n` +
`โ
Tests passed!\n\n` +
`**Coverage:**\n` +
`- Lines: ${total.lines.pct}%\n` +
`- Statements: ${total.statements.pct}%\n` +
`- Functions: ${total.functions.pct}%\n` +
`- Branches: ${total.branches.pct}%`
});
```
### **5.3 Required Status Checks**
In GitHub repository settings, configure branch protection rules:
1. Go to **Settings** โ **Branches** โ **Branch protection rules**
2. Add rule for `main` branch
3. Enable:
- โ
Require status checks to pass before merging
- โ
Require branches to be up to date before merging
4. Select required checks:
- `test` (from deploy.yml)
- `lint-and-test` (from pr-checks.yml)
---
## **Implementation Checklist**
### **Phase 1: Vitest Setup**
- [ ] Install Vitest and React Testing Library dependencies
- [ ] Create `vitest.config.ts`
- [ ] Create `app/test/setup.ts`
- [ ] Create example unit tests for league model
- [ ] Create example component tests for DraftGrid
- [ ] Add test scripts to package.json
### **Phase 2: Cypress Setup**
- [ ] Install Cypress dependencies
- [ ] Create `cypress.config.ts`
- [ ] Create `cypress/support/e2e.ts` with custom commands
- [ ] Create league creation E2E tests
- [ ] Create league settings E2E tests
- [ ] Create draft room E2E tests
### **Phase 3: Test Fixtures**
- [ ] Create `app/test/fixtures/league.ts`
- [ ] Create `app/test/fixtures/season.ts`
- [ ] Create `app/test/fixtures/team.ts`
- [ ] Create Cypress database helpers
### **Phase 4: CI/CD Integration**
- [ ] Update `.github/workflows/deploy.yml` with test jobs
- [ ] Create `.github/workflows/pr-checks.yml`
- [ ] Configure GitHub branch protection rules
- [ ] Add required status checks
- [ ] Test CI pipeline with a PR
### **Phase 5: Documentation**
- [ ] Document how to run tests locally
- [ ] Document how to write new tests
- [ ] Document CI/CD pipeline
- [ ] Add testing best practices guide
---
## **Test Coverage Goals**
- **Unit Tests:** 80%+ coverage for models and utilities
- **Component Tests:** All major UI components tested
- **E2E Tests:** Critical user flows covered
- League creation and editing
- Draft room functionality
- Commissioner controls
- Public draft board access
---
## **Success Criteria**
โ
All tests pass locally
โ
CI/CD pipeline runs tests automatically
โ
PRs cannot merge without passing tests
โ
Coverage reports generated and tracked
โ
E2E tests run in headless mode
โ
Test failures block deployments
---
## **Future Enhancements**
- **Visual Regression Testing** - Percy or Chromatic
- **Performance Testing** - Lighthouse CI
- **Load Testing** - k6 for draft room Socket.IO
- **Accessibility Testing** - axe-core integration
- **Mutation Testing** - Stryker for test quality