diff --git a/plans/e2e-testing-setup.md b/plans/e2e-testing-setup.md new file mode 100644 index 0000000..68cf398 --- /dev/null +++ b/plans/e2e-testing-setup.md @@ -0,0 +1,600 @@ +# E2E Testing Setup Plan + +**Created:** October 21, 2025 +**Status:** Planned +**Goal:** Enable end-to-end testing with Cypress for authenticated user flows + +--- + +## Overview + +Currently, 23 of 26 Cypress tests are skipped because they require authentication. This plan outlines the steps to enable full E2E testing with proper test user authentication and database setup. + +--- + +## Phase 1: Test User Authentication Setup + +### 1.1 Create Test User in Clerk + +**Goal:** Set up a dedicated test user account for Cypress tests + +**Steps:** +1. Log into Clerk Dashboard +2. Navigate to Users section +3. Create a new test user: + - Email: `test@brackt.local` (or use env variable) + - Password: Generate secure password + - Mark as verified +4. Save user credentials securely + +**Environment Variables:** +```bash +# Add to .env.test or Cypress env +CYPRESS_TEST_USER_EMAIL=test@brackt.local +CYPRESS_TEST_USER_PASSWORD= +CYPRESS_TEST_USER_ID= +``` + +### 1.2 Implement Cypress Login Command + +**File:** `cypress/support/commands.ts` + +**Implementation:** +```typescript +Cypress.Commands.add('login', (email?: string, password?: string) => { + const testEmail = email || Cypress.env('TEST_USER_EMAIL'); + const testPassword = password || Cypress.env('TEST_USER_PASSWORD'); + + cy.session([testEmail, testPassword], () => { + cy.visit('/sign-in'); + + // Wait for Clerk to load + cy.get('[name="identifier"]', { timeout: 10000 }).should('be.visible'); + + // Enter email + cy.get('[name="identifier"]').type(testEmail); + cy.get('button[type="submit"]').click(); + + // Enter password + cy.get('[name="password"]', { timeout: 10000 }).should('be.visible'); + cy.get('[name="password"]').type(testPassword); + cy.get('button[type="submit"]').click(); + + // Wait for redirect after successful login + cy.url().should('not.include', '/sign-in'); + cy.url().should('not.include', '/sign-up'); + }, { + validate() { + // Verify session is still valid + cy.getCookie('__session').should('exist'); + }, + }); +}); + +// Add TypeScript declaration +declare global { + namespace Cypress { + interface Chainable { + login(email?: string, password?: string): Chainable; + } + } +} +``` + +### 1.3 Update Cypress Configuration + +**File:** `cypress.config.js` + +**Add:** +```javascript +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}', + video: true, + screenshotOnRunFailure: true, + experimentalSessionAndOrigin: true, // Enable session support + }, + env: { + TEST_USER_EMAIL: process.env.CYPRESS_TEST_USER_EMAIL, + TEST_USER_PASSWORD: process.env.CYPRESS_TEST_USER_PASSWORD, + }, +}); +``` + +--- + +## Phase 2: Test Database Setup + +### 2.1 Create Test Database + +**Goal:** Separate test database to avoid polluting production/dev data + +**Steps:** +1. Create new PostgreSQL database: + ```bash + createdb brackt_test + ``` + +2. Add test database URL to environment: + ```bash + # .env.test + DATABASE_URL=postgresql://user:password@localhost:5432/brackt_test + ``` + +3. Run migrations on test database: + ```bash + DATABASE_URL=postgresql://user:password@localhost:5432/brackt_test npm run db:migrate + ``` + +### 2.2 Create Database Seed Script + +**File:** `cypress/support/database.ts` + +**Purpose:** Seed test data before E2E tests + +**Implementation:** +```typescript +import { database } from '~/database/context'; +import * as schema from '~/database/schema'; + +export async function seedTestDatabase() { + const db = database(); + + // Clear existing data + await db.delete(schema.picks); + await db.delete(schema.draftSlots); + await db.delete(schema.teams); + await db.delete(schema.seasonSports); + await db.delete(schema.seasons); + await db.delete(schema.commissioners); + await db.delete(schema.leagues); + + // Create test league + const [league] = await db.insert(schema.leagues).values({ + id: 'test-league-1', + name: 'Test League', + createdBy: Cypress.env('TEST_USER_ID'), + isPublicDraftBoard: false, + }).returning(); + + // Create test season + const [season] = await db.insert(schema.seasons).values({ + id: 'test-season-1', + leagueId: league.id, + year: new Date().getFullYear(), + status: 'pre_draft', + draftRounds: 20, + draftInitialTime: 120, + draftIncrementTime: 15, + currentPickNumber: 1, + }).returning(); + + // Create commissioner + await db.insert(schema.commissioners).values({ + leagueId: league.id, + userId: Cypress.env('TEST_USER_ID'), + }); + + // Create teams + const teams = Array.from({ length: 12 }, (_, i) => ({ + id: `test-team-${i + 1}`, + seasonId: season.id, + name: `Team ${i + 1}`, + ownerId: i === 0 ? Cypress.env('TEST_USER_ID') : null, + })); + + await db.insert(schema.teams).values(teams); + + return { league, season, teams }; +} + +export async function cleanTestDatabase() { + const db = database(); + + await db.delete(schema.picks); + await db.delete(schema.draftSlots); + await db.delete(schema.teams); + await db.delete(schema.seasonSports); + await db.delete(schema.seasons); + await db.delete(schema.commissioners); + await db.delete(schema.leagues); +} +``` + +### 2.3 Add Cypress Tasks for Database Operations + +**File:** `cypress.config.js` + +**Update:** +```javascript +import { seedTestDatabase, cleanTestDatabase } from './cypress/support/database'; + +export default defineConfig({ + e2e: { + setupNodeEvents(on, config) { + on('task', { + async 'db:seed'() { + await seedTestDatabase(); + return null; + }, + async 'db:clean'() { + await cleanTestDatabase(); + return null; + }, + }); + + return config; + }, + // ... rest of config + }, +}); +``` + +### 2.4 Update Cypress Commands + +**File:** `cypress/support/commands.ts` + +**Add:** +```typescript +Cypress.Commands.add('seedDatabase', () => { + cy.task('db:seed'); +}); + +Cypress.Commands.add('cleanDatabase', () => { + cy.task('db:clean'); +}); + +declare global { + namespace Cypress { + interface Chainable { + seedDatabase(): Chainable; + cleanDatabase(): Chainable; + } + } +} +``` + +--- + +## Phase 3: Update Test Files + +### 3.1 Enable League Creation Tests + +**File:** `cypress/e2e/league-creation.cy.ts` + +**Changes:** +```typescript +describe('League Creation Flow', () => { + beforeEach(() => { + cy.cleanDatabase(); + cy.login(); + }); + + it('should display the league creation form', () => { + cy.visit('/leagues/new'); + + cy.get('input[name="name"]').should('exist'); + cy.get('#teamCount').should('exist'); + cy.get('#draftRounds').should('exist'); + cy.get('select[name="draftSpeed"]').should('exist'); + }); + + it('should create a new league', () => { + cy.visit('/leagues/new'); + + cy.get('input[name="name"]').type('My Test League'); + + cy.get('#teamCount').click(); + cy.contains('[role="option"]', '12 Teams').click(); + + cy.get('#draftRounds').click(); + cy.contains('[role="option"]', '20').click(); + + cy.get('select[name="draftSpeed"]').select('standard'); + + cy.get('button[type="submit"]').click(); + + cy.url().should('include', '/leagues/'); + cy.contains('My Test League').should('exist'); + }); +}); +``` + +### 3.2 Enable League Settings Tests + +**File:** `cypress/e2e/league-settings.cy.ts` + +**Changes:** +```typescript +describe('League Settings', () => { + beforeEach(() => { + cy.cleanDatabase(); + cy.seedDatabase(); + cy.login(); + }); + + it('should display league settings page', () => { + cy.visit('/leagues/test-league-1/settings'); + + cy.get('input[name="name"]').should('have.value', 'Test League'); + }); + + it('should update league name', () => { + cy.visit('/leagues/test-league-1/settings'); + + cy.get('input[name="name"]').clear().type('Updated League Name'); + cy.contains('button', /save/i).click(); + + cy.contains('Updated League Name').should('exist'); + }); +}); +``` + +### 3.3 Enable Draft Room Tests + +**File:** `cypress/e2e/draft-room.cy.ts` + +**Changes:** +```typescript +describe('Draft Room', () => { + beforeEach(() => { + cy.cleanDatabase(); + cy.seedDatabase(); + cy.login(); + }); + + it('should display draft grid', () => { + cy.visit('/leagues/test-league-1/draft/test-season-1'); + + cy.contains('Team 1').should('exist'); + cy.contains('Team 2').should('exist'); + }); + + it('should start draft as commissioner', () => { + cy.visit('/leagues/test-league-1/draft/test-season-1'); + + cy.contains('button', /start draft/i).click(); + + // Verify timers appear + cy.get('[class*="font-mono"]').should('exist'); + }); +}); +``` + +--- + +## Phase 4: CI/CD Integration + +### 4.1 Update GitHub Actions Workflow + +**File:** `.github/workflows/test.yml` + +**Add E2E testing job:** +```yaml +jobs: + test: + # ... existing unit test job + + e2e: + name: 🎭 E2E Tests + runs-on: ubuntu-latest + needs: test + + 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: Start dev server + run: npm run dev & + env: + DATABASE_URL: postgresql://test:test@localhost:5432/brackt_test + PORT: 3000 + + - name: Wait for server + run: npx wait-on http://localhost:3000 --timeout 60000 + + - name: Run E2E tests + run: npm run test:e2e:headless + env: + DATABASE_URL: postgresql://test:test@localhost:5432/brackt_test + CYPRESS_TEST_USER_EMAIL: ${{ secrets.CYPRESS_TEST_USER_EMAIL }} + CYPRESS_TEST_USER_PASSWORD: ${{ secrets.CYPRESS_TEST_USER_PASSWORD }} + CYPRESS_TEST_USER_ID: ${{ secrets.CYPRESS_TEST_USER_ID }} + + - name: Upload Cypress videos + uses: actions/upload-artifact@v3 + if: failure() + with: + name: cypress-videos + path: cypress/videos + + - name: Upload Cypress screenshots + uses: actions/upload-artifact@v3 + if: failure() + with: + name: cypress-screenshots + path: cypress/screenshots +``` + +### 4.2 Add GitHub Secrets + +**Required secrets in GitHub repository settings:** +- `CYPRESS_TEST_USER_EMAIL` +- `CYPRESS_TEST_USER_PASSWORD` +- `CYPRESS_TEST_USER_ID` + +--- + +## Phase 5: Documentation & Scripts + +### 5.1 Update TESTING.md + +**Add section:** +```markdown +## Running E2E Tests Locally + +### Prerequisites +1. Test database created and migrated +2. Test user created in Clerk +3. Environment variables set + +### Setup +```bash +# Create test database +createdb brackt_test + +# Run migrations +DATABASE_URL=postgresql://user:password@localhost:5432/brackt_test npm run db:migrate + +# Set environment variables +export CYPRESS_TEST_USER_EMAIL=test@brackt.local +export CYPRESS_TEST_USER_PASSWORD=your-password +export CYPRESS_TEST_USER_ID=clerk-user-id +``` + +### Running Tests +```bash +# Start dev server in one terminal +npm run dev + +# Run E2E tests in another terminal +npm run test:e2e + +# Or run headless +npm run test:e2e:headless +``` + +### Troubleshooting +- **Login fails:** Check Clerk credentials and user exists +- **Database errors:** Ensure test database is migrated +- **Timeout errors:** Increase timeout in cypress.config.js +``` + +### 5.2 Add Helper Scripts + +**File:** `package.json` + +**Add scripts:** +```json +{ + "scripts": { + "test:e2e:setup": "createdb brackt_test && DATABASE_URL=postgresql://user:password@localhost:5432/brackt_test npm run db:migrate", + "test:e2e:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:3000 && npm run test:e2e\"", + "test:e2e:ci": "npm run test:e2e:headless" + } +} +``` + +--- + +## Implementation Checklist + +### Phase 1: Authentication +- [ ] Create test user in Clerk Dashboard +- [ ] Save credentials to environment variables +- [ ] Implement `cy.login()` command +- [ ] Update Cypress config with env variables +- [ ] Test login flow manually + +### Phase 2: Database +- [ ] Create test database +- [ ] Run migrations on test database +- [ ] Create seed script +- [ ] Add Cypress tasks for database operations +- [ ] Test seeding and cleaning + +### Phase 3: Tests +- [ ] Remove `.skip()` from league-creation tests +- [ ] Remove `.skip()` from league-settings tests +- [ ] Remove `.skip()` from draft-room tests +- [ ] Add `beforeEach` hooks with login and seed +- [ ] Run tests locally to verify + +### Phase 4: CI/CD +- [ ] Add E2E job to GitHub Actions +- [ ] Add GitHub secrets for test user +- [ ] Test CI pipeline with a PR +- [ ] Verify artifacts upload on failure + +### Phase 5: Documentation +- [ ] Update TESTING.md with E2E instructions +- [ ] Add troubleshooting section +- [ ] Document environment setup +- [ ] Add helper scripts to package.json + +--- + +## Success Criteria + +✅ All 26 Cypress tests passing (3 smoke + 23 E2E) +✅ Tests run successfully in CI/CD +✅ Test database properly seeded and cleaned +✅ Login flow working reliably +✅ Screenshots/videos captured on failure +✅ Documentation complete and accurate + +--- + +## Estimated Timeline + +- **Phase 1:** 2-3 hours (authentication setup) +- **Phase 2:** 3-4 hours (database setup and seeding) +- **Phase 3:** 2-3 hours (updating test files) +- **Phase 4:** 1-2 hours (CI/CD integration) +- **Phase 5:** 1 hour (documentation) + +**Total:** 9-13 hours + +--- + +## Notes + +- Start with Phase 1 and test thoroughly before moving on +- Keep test user credentials secure (use GitHub secrets, not .env files in repo) +- Consider using a separate Clerk environment for testing +- Test database should be reset between test runs +- Monitor CI/CD costs if using paid runners + +--- + +## Future Enhancements + +- **Visual regression testing** with Percy or Chromatic +- **Parallel test execution** to speed up CI +- **Test data factories** for more flexible seeding +- **API mocking** for external services +- **Performance testing** with Lighthouse CI