brackt/plans/testing-implementation.md

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:

  1. Go to SettingsBranchesBranch 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