-
Notifications
You must be signed in to change notification settings - Fork 0
Copy of PR #22484: feat: optimize E2E test execution speed through strategic consolidation #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| name: E2E Timing Baseline | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| push: | ||
| branches: [ "devin/optimize-e2e-tests-*" ] | ||
|
|
||
| jobs: | ||
| e2e-timing: | ||
| timeout-minutes: 60 | ||
| name: E2E Tests with Timing | ||
| runs-on: buildjet-8vcpu-ubuntu-2204 | ||
|
|
||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| shard: [1, 2, 3] | ||
|
|
||
| env: | ||
| DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/calendso" | ||
| DATABASE_DIRECT_URL: "postgresql://postgres:postgres@localhost:5432/calendso" | ||
| NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }} | ||
| CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }} | ||
| NEXT_PUBLIC_WEBAPP_URL: "http://localhost:3000" | ||
| NEXT_PUBLIC_WEBSITE_URL: "http://localhost:3000" | ||
| NEXTAUTH_URL: "http://localhost:3000" | ||
| E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }} | ||
| E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }} | ||
| PLAYWRIGHT_HEADLESS: 1 | ||
| NEXT_PUBLIC_IS_E2E: 1 | ||
| STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }} | ||
| STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} | ||
| PAYMENT_FEE_PERCENTAGE: 0.005 | ||
| PAYMENT_FEE_FIXED: 10 | ||
| SAML_DATABASE_URL: ${{ secrets.SAML_DATABASE_URL }} | ||
| SAML_ADMINS: pro@example.com | ||
| NEXTAUTH_COOKIE_DOMAIN: .cal.local | ||
| EMAIL_FROM: e2e@cal.com | ||
| NEXT_PUBLIC_SENDER_ID: Cal | ||
| NEXT_PUBLIC_SENDGRID_SENDER_NAME: Cal.com | ||
| TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} | ||
| TURBO_TEAM: ${{ vars.TURBO_TEAM }} | ||
|
|
||
| services: | ||
| postgres: | ||
| image: postgres:13 | ||
| credentials: | ||
| username: ${{ secrets.DOCKERHUB_USERNAME }} | ||
| password: ${{ secrets.DOCKERHUB_TOKEN }} | ||
| env: | ||
| POSTGRES_USER: postgres | ||
| POSTGRES_PASSWORD: postgres | ||
| POSTGRES_DB: calendso | ||
| options: >- | ||
| --health-cmd pg_isready | ||
| --health-interval 10s | ||
| --health-timeout 5s | ||
| --health-retries 5 | ||
| ports: | ||
| - 5432:5432 | ||
| mailhog: | ||
| image: mailhog/mailhog | ||
| ports: | ||
| - 1025:1025 | ||
| - 8025:8025 | ||
|
|
||
| steps: | ||
| - name: Checkout repo | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 2 | ||
|
|
||
| - name: Use Node 18.x | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 18.x | ||
|
|
||
| - name: Cache dependencies | ||
| uses: ./.github/actions/yarn-install | ||
|
|
||
| - name: Cache database | ||
| uses: ./.github/actions/cache-db | ||
|
|
||
| - name: Cache build | ||
| uses: ./.github/actions/cache-build | ||
|
|
||
| - name: Start timing measurement | ||
| run: | | ||
| echo "E2E_START_TIME=$(date +%s)" >> $GITHUB_ENV | ||
| echo "Starting E2E tests at $(date)" | ||
|
|
||
| - name: Run E2E tests with timing | ||
| run: | | ||
| echo "Running shard ${{ matrix.shard }}/3" | ||
| yarn e2e --shard=${{ matrix.shard }}/3 --reporter=html,json | ||
| env: | ||
| NEXT_PUBLIC_IS_E2E: 1 | ||
|
|
||
| - name: Calculate execution time | ||
| if: always() | ||
| run: | | ||
| END_TIME=$(date +%s) | ||
| DURATION=$((END_TIME - E2E_START_TIME)) | ||
| echo "E2E execution time for shard ${{ matrix.shard }}: ${DURATION} seconds" | ||
| echo "SHARD_DURATION=${DURATION}" >> $GITHUB_ENV | ||
|
|
||
| # Create timing report | ||
| mkdir -p timing-reports | ||
| echo "{\"shard\": ${{ matrix.shard }}, \"duration\": ${DURATION}, \"timestamp\": \"$(date -Iseconds)\"}" > timing-reports/shard-${{ matrix.shard }}-timing.json | ||
|
|
||
| - name: Upload timing report | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: timing-report-shard-${{ matrix.shard }} | ||
| path: timing-reports/ | ||
| retention-days: 30 | ||
|
|
||
| - name: Upload test results | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: test-results-shard-${{ matrix.shard }} | ||
| path: | | ||
| test-results/ | ||
| playwright-report/ | ||
| retention-days: 30 | ||
|
|
||
| timing-summary: | ||
| needs: e2e-timing | ||
| if: always() | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Download all timing reports | ||
| uses: actions/download-artifact@v4 | ||
| with: | ||
| pattern: timing-report-shard-* | ||
| merge-multiple: true | ||
| path: timing-reports/ | ||
|
|
||
| - name: Generate timing summary | ||
| run: | | ||
| echo "# E2E Test Timing Summary" > timing-summary.md | ||
| echo "" >> timing-summary.md | ||
| echo "| Shard | Duration (seconds) | Duration (minutes) |" >> timing-summary.md | ||
| echo "|-------|-------------------|-------------------|" >> timing-summary.md | ||
|
|
||
| total_duration=0 | ||
| for file in timing-reports/*.json; do | ||
| if [ -f "$file" ]; then | ||
| shard=$(jq -r '.shard' "$file") | ||
| duration=$(jq -r '.duration' "$file") | ||
| minutes=$((duration / 60)) | ||
| seconds=$((duration % 60)) | ||
| echo "| $shard | $duration | ${minutes}m ${seconds}s |" >> timing-summary.md | ||
| total_duration=$((total_duration + duration)) | ||
| fi | ||
| done | ||
|
|
||
| total_minutes=$((total_duration / 60)) | ||
| total_seconds=$((total_duration % 60)) | ||
| echo "" >> timing-summary.md | ||
| echo "**Total execution time across all shards: ${total_duration}s (${total_minutes}m ${total_seconds}s)**" >> timing-summary.md | ||
| echo "" >> timing-summary.md | ||
| echo "Generated at: $(date -Iseconds)" >> timing-summary.md | ||
|
|
||
| cat timing-summary.md | ||
|
|
||
| - name: Upload timing summary | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: timing-summary | ||
| path: timing-summary.md | ||
| retention-days: 30 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| import { expect } from "@playwright/test"; | ||
|
|
||
| import { test } from "./lib/fixtures"; | ||
|
|
||
| test.describe.configure({ mode: "parallel" }); | ||
|
|
||
| test.describe("Authentication", () => { | ||
| test("Should be able to login with email and password", async ({ page, users }) => { | ||
| const [user] = users.get(); | ||
| await page.goto("/auth/login"); | ||
|
|
||
| await page.locator('[name="email"]').fill(user.email); | ||
| await page.locator('[name="password"]').fill(user.username || "password"); | ||
| await page.locator('[type="submit"]').click(); | ||
|
|
||
| await expect(page).toHaveURL("/event-types"); | ||
| await expect(page.locator('[data-testid="user-dropdown"]')).toBeVisible(); | ||
| }); | ||
|
|
||
| test("Should show error for invalid credentials", async ({ page }) => { | ||
| await page.goto("/auth/login"); | ||
|
|
||
| await page.locator('[name="email"]').fill("invalid@example.com"); | ||
| await page.locator('[name="password"]').fill("wrongpassword"); | ||
| await page.locator('[type="submit"]').click(); | ||
|
|
||
| await expect(page.locator('[data-testid="error-message"]')).toBeVisible(); | ||
| }); | ||
|
|
||
| test("Should be able to logout", async ({ page, users }) => { | ||
| const [user] = users.get(); | ||
| await user.apiLogin(); | ||
| await page.goto("/event-types"); | ||
|
|
||
| await page.locator('[data-testid="user-dropdown"]').click(); | ||
| await page.locator('[data-testid="logout-button"]').click(); | ||
|
|
||
| await expect(page).toHaveURL("/auth/login"); | ||
| }); | ||
|
|
||
| test("Should redirect to login when accessing protected page", async ({ page }) => { | ||
| await page.goto("/event-types"); | ||
| await expect(page).toHaveURL(/\/auth\/login/); | ||
| }); | ||
|
|
||
| test("Should handle forgot password flow", async ({ page }) => { | ||
| await page.goto("/auth/login"); | ||
| await page.locator('[data-testid="forgot-password-link"]').click(); | ||
|
|
||
| await expect(page).toHaveURL("/auth/forgot-password"); | ||
| await expect(page.locator('[data-testid="forgot-password-form"]')).toBeVisible(); | ||
| }); | ||
|
|
||
| test("Should handle signup flow", async ({ page }) => { | ||
| await page.goto("/auth/signup"); | ||
|
|
||
| await page.locator('[name="username"]').fill("testuser"); | ||
| await page.locator('[name="email"]').fill("test@example.com"); | ||
| await page.locator('[name="password"]').fill("password123"); | ||
| await page.locator('[type="submit"]').click(); | ||
|
|
||
| await expect(page.locator('[data-testid="signup-success"]')).toBeVisible(); | ||
| }); | ||
| }); | ||
|
|
||
| test.describe("Two-Factor Authentication", () => { | ||
| test("Should be able to enable 2FA", async ({ page, users }) => { | ||
| const [user] = users.get(); | ||
| await user.apiLogin(); | ||
| await page.goto("/settings/security/two-factor-auth"); | ||
|
|
||
| await page.locator('[data-testid="enable-2fa-button"]').click(); | ||
| await expect(page.locator('[data-testid="2fa-setup"]')).toBeVisible(); | ||
| }); | ||
|
|
||
| test("Should require 2FA code when enabled", async ({ page, users }) => { | ||
| const [user] = users.get(); | ||
| await user.apiLogin(); | ||
|
|
||
| await page.goto("/settings/security/two-factor-auth"); | ||
| await page.locator('[data-testid="enable-2fa-button"]').click(); | ||
|
|
||
| await page.goto("/auth/logout"); | ||
| await page.goto("/auth/login"); | ||
|
|
||
| await page.locator('[name="email"]').fill(user.email); | ||
| await page.locator('[name="password"]').fill(user.username || "password"); | ||
|
Comment on lines
+86
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Using username as password fallback is unsafe. Should use a dedicated test password |
||
| await page.locator('[type="submit"]').click(); | ||
|
|
||
| await expect(page.locator('[data-testid="2fa-input"]')).toBeVisible(); | ||
| }); | ||
|
Comment on lines
+76
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Test needs cleanup after 2FA enablement - enabled 2FA could affect other tests. Consider adding afterEach hook to disable 2FA |
||
| }); | ||
|
|
||
| test.describe("OAuth Authentication", () => { | ||
| test("Should display OAuth login options", async ({ page }) => { | ||
| await page.goto("/auth/login"); | ||
|
|
||
| await expect(page.locator('[data-testid="oauth-google"]')).toBeVisible(); | ||
| await expect(page.locator('[data-testid="oauth-github"]')).toBeVisible(); | ||
| }); | ||
|
|
||
| test("Should handle OAuth callback", async ({ page }) => { | ||
| await page.goto("/auth/login"); | ||
|
|
||
| await page.locator('[data-testid="oauth-google"]').click(); | ||
| await expect(page).toHaveURL(/accounts\.google\.com/); | ||
| }); | ||
|
Comment on lines
+102
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: OAuth test could be flaky - consider mocking OAuth provider response instead of testing actual redirect |
||
| }); | ||
|
|
||
| test.describe("API Authentication", () => { | ||
| test("Should require authentication for API endpoints", async ({ page }) => { | ||
| const response = await page.request.get("/api/user"); | ||
| expect(response.status()).toBe(401); | ||
| }); | ||
|
|
||
| test("Should allow authenticated API requests", async ({ page, users }) => { | ||
| const [user] = users.get(); | ||
| await user.apiLogin(); | ||
|
|
||
| const response = await page.request.get("/api/user"); | ||
| expect(response.status()).toBe(200); | ||
| }); | ||
|
|
||
| test("Should handle API key authentication", async ({ page, users }) => { | ||
| const [user] = users.get(); | ||
| await user.apiLogin(); | ||
|
|
||
| await page.goto("/settings/developer/api-keys"); | ||
| await page.locator('[data-testid="create-api-key"]').click(); | ||
| await page.locator('[name="note"]').fill("Test API Key"); | ||
| await page.locator('[data-testid="save-api-key"]').click(); | ||
|
|
||
| await expect(page.locator('[data-testid="api-key-created"]')).toBeVisible(); | ||
| }); | ||
|
Comment on lines
+124
to
+134
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Add afterEach to clean up created API keys to prevent test pollution |
||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: SHARD_DURATION is set but never used. Remove if not needed.