19 KiB
🧪 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
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
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
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
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
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(
<DraftGrid
draftSlots={mockSlots}
draftGrid={mockGrid}
currentPick={2}
/>
);
expect(screen.getByText('Team 1')).toBeInTheDocument();
expect(screen.getByText('Team 2')).toBeInTheDocument();
});
it('should highlight current pick', () => {
render(
<DraftGrid
draftSlots={mockSlots}
draftGrid={mockGrid}
currentPick={2}
/>
);
expect(screen.getByText('On Clock')).toBeInTheDocument();
});
it('should display picked players', () => {
render(
<DraftGrid
draftSlots={mockSlots}
draftGrid={mockGrid}
currentPick={2}
/>
);
expect(screen.getByText('Player 1')).toBeInTheDocument();
expect(screen.getByText('NFL')).toBeInTheDocument();
});
});
Phase 2: Cypress E2E Testing Setup
2.1 Installation
npm install -D cypress @testing-library/cypress
2.2 Configuration
cypress.config.ts
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
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<void>;
}
}
}
2.3 E2E Test Examples
cypress/e2e/league-creation.cy.ts
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
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
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
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
// 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
{
"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)
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)
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:
- Go to Settings → Branches → Branch protection rules
- Add rule for
mainbranch - Enable:
- ✅ Require status checks to pass before merging
- ✅ Require branches to be up to date before merging
- 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.tswith 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.ymlwith 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