# ๐Ÿงช 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