From bd9ee7883a4044425d0b5b7ee792fe50cf134776 Mon Sep 17 00:00:00 2001 From: Kunal Date: Sun, 8 Feb 2026 14:52:03 +0000 Subject: [PATCH] test(e2e): migrate Cypress auth to @clerk/testing commands --- .github/workflows/ci.yml | 22 ++++++++--- frontend/cypress.config.ts | 4 ++ frontend/cypress/e2e/activity_feed.cy.ts | 29 +++++++------- frontend/cypress/e2e/clerk_login.cy.ts | 24 +++++------- frontend/cypress/support/e2e.ts | 1 + frontend/package-lock.json | 48 ++++++++++++++++++++++-- frontend/package.json | 1 + 7 files changed, 91 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1f9ef48..457ca895 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,14 +121,24 @@ jobs: - name: Run Cypress E2E env: NEXT_TELEMETRY_DISABLED: "1" - # Vars for shared Clerk OTP helper (frontend/cypress/support/commands.ts) - # Provide deterministic test creds directly (no secretless skipping). - CYPRESS_CLERK_TEST_EMAIL: "jane+clerk_test@example.com" - CYPRESS_CLERK_TEST_OTP: "424242" - # Provide publishable key to Cypress so helper can derive CLERK_ORIGIN. - CYPRESS_NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ vars.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} + # Clerk testing tokens (official @clerk/testing Cypress integration) + CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} + CLERK_PUBLISHABLE_KEY: ${{ vars.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} # Also set for the app itself. NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ vars.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} + CLERK_JWKS_URL: ${{ vars.CLERK_JWKS_URL }} + # Test user identifier (used by cy.clerkSignIn) + CYPRESS_CLERK_TEST_EMAIL: "jane+clerk_test@example.com" run: | cd frontend npm run e2e -- --browser chrome + + - name: Upload Cypress artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: cypress-artifacts + if-no-files-found: ignore + path: | + frontend/cypress/screenshots/** + frontend/cypress/videos/** diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts index aab9452d..0f461372 100644 --- a/frontend/cypress.config.ts +++ b/frontend/cypress.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from "cypress"; +import { clerkSetup } from "@clerk/testing/cypress"; export default defineConfig({ env: { @@ -12,5 +13,8 @@ export default defineConfig({ baseUrl: "http://localhost:3000", specPattern: "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}", supportFile: "cypress/support/e2e.ts", + setupNodeEvents(on, config) { + return clerkSetup({ config }); + }, }, }); diff --git a/frontend/cypress/e2e/activity_feed.cy.ts b/frontend/cypress/e2e/activity_feed.cy.ts index 6fb97b4c..a80af55e 100644 --- a/frontend/cypress/e2e/activity_feed.cy.ts +++ b/frontend/cypress/e2e/activity_feed.cy.ts @@ -2,6 +2,7 @@ describe("/activity feed", () => { const apiBase = "**/api/v1"; + const email = Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com"; function stubStreamEmpty() { cy.intercept( @@ -21,18 +22,10 @@ describe("/activity feed", () => { cy.contains(/live feed/i, { timeout: 30_000 }).should("be.visible"); } - it("auth negative: wrong OTP keeps us on sign-in", () => { - // Start from app-origin sign-in to avoid cross-origin confusion. - cy.visit("/sign-in"); - - // Override OTP just for this test. - Cypress.env("CLERK_TEST_OTP", "000000"); - - // Expect login flow to fail; easiest assertion is that we remain on sign-in. - // (The shared helper does not currently expose a typed hook to assert the error text.) - cy.loginWithClerkOtp(); - - cy.location("pathname", { timeout: 30_000 }).should("match", /\/sign-in/); + it("auth negative: signed-out user cannot access /activity", () => { + // Story: signed-out user tries to visit /activity and is redirected to sign-in. + cy.visit("/activity"); + cy.location("pathname", { timeout: 20_000 }).should("match", /\/sign-in/); }); it("happy path: renders task comment cards", () => { @@ -58,7 +51,9 @@ describe("/activity feed", () => { stubStreamEmpty(); // Story: user signs in, then visits /activity and sees the live feed. - cy.loginWithClerkOtp(); + cy.visit("/sign-in"); + cy.clerkLoaded(); + cy.clerkSignIn({ strategy: "email_code", identifier: email }); cy.visit("/activity"); assertSignedInAndLanded(); @@ -77,7 +72,9 @@ describe("/activity feed", () => { stubStreamEmpty(); // Story: user signs in, then visits /activity and sees an empty-state message. - cy.loginWithClerkOtp(); + cy.visit("/sign-in"); + cy.clerkLoaded(); + cy.clerkSignIn({ strategy: "email_code", identifier: email }); cy.visit("/activity"); assertSignedInAndLanded(); @@ -95,7 +92,9 @@ describe("/activity feed", () => { stubStreamEmpty(); // Story: user signs in, then visits /activity; API fails and user sees an error. - cy.loginWithClerkOtp(); + cy.visit("/sign-in"); + cy.clerkLoaded(); + cy.clerkSignIn({ strategy: "email_code", identifier: email }); cy.visit("/activity"); assertSignedInAndLanded(); diff --git a/frontend/cypress/e2e/clerk_login.cy.ts b/frontend/cypress/e2e/clerk_login.cy.ts index 369213a5..8b091d0d 100644 --- a/frontend/cypress/e2e/clerk_login.cy.ts +++ b/frontend/cypress/e2e/clerk_login.cy.ts @@ -1,19 +1,15 @@ -describe("Clerk login (OTP)", () => { - it("can sign in via Clerk modal", () => { - // Skip unless explicitly configured. - const clerkOrigin = Cypress.env("CLERK_ORIGIN"); - const email = Cypress.env("CLERK_TEST_EMAIL"); - const otp = Cypress.env("CLERK_TEST_OTP"); +describe("Clerk login", () => { + it("user can sign in via Clerk testing commands", () => { + const email = Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com"; - if (!clerkOrigin || !email || !otp) { - cy.log("Skipping: missing CYPRESS_CLERK_ORIGIN / CYPRESS_CLERK_TEST_EMAIL / CYPRESS_CLERK_TEST_OTP"); - return; - } + // Prereq per Clerk docs: visit a non-protected page that loads Clerk. + cy.visit("/sign-in"); + cy.clerkLoaded(); + cy.clerkSignIn({ strategy: "email_code", identifier: email }); + + // After login, user should be able to access protected route. cy.visit("/activity"); - cy.loginWithClerkOtp(); - - // After login, the SignedIn UI should render. - cy.contains(/live feed/i, { timeout: 20_000 }).should("be.visible"); + cy.contains(/live feed/i, { timeout: 30_000 }).should("be.visible"); }); }); diff --git a/frontend/cypress/support/e2e.ts b/frontend/cypress/support/e2e.ts index 8c97169f..1814d34d 100644 --- a/frontend/cypress/support/e2e.ts +++ b/frontend/cypress/support/e2e.ts @@ -1,4 +1,5 @@ // Cypress support file. // Place global hooks/commands here. +import "@clerk/testing/cypress"; import "./commands"; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3894dfd4..00dec71e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,6 +26,7 @@ "remark-gfm": "^4.0.1" }, "devDependencies": { + "@clerk/testing": "^1.13.35", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2", @@ -364,9 +365,9 @@ "license": "MIT" }, "node_modules/@clerk/backend": { - "version": "2.29.7", - "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-2.29.7.tgz", - "integrity": "sha512-OSfFQ85L0FV2wSzqlr0hRvluIu3Z5ClgLiBE6Qx7XjSGyJoqEvP5OP4fl5Nt5icgGvH0EwA1dljPGyQpaqbQEw==", + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-2.30.1.tgz", + "integrity": "sha512-GoxnJzVH0ycNPAGCDMfo3lPBFbo5nehpLSVFjgGEnzIRGGahBtAB8PQT7KM2zo58pD8apjb/+suhcB/WCiEasQ==", "license": "MIT", "dependencies": { "@clerk/shared": "^3.44.0", @@ -453,6 +454,34 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/@clerk/testing": { + "version": "1.13.35", + "resolved": "https://registry.npmjs.org/@clerk/testing/-/testing-1.13.35.tgz", + "integrity": "sha512-y95kJZrMt0tvbNek1AWhWrNrgnOy+a53PSzHTHPF9d0kkOgzzu9l/Wq+Y0kBk6p64wtupYomeb7oVCQD7yCc0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clerk/backend": "^2.30.1", + "@clerk/shared": "^3.44.0", + "@clerk/types": "^4.101.14", + "dotenv": "17.2.2" + }, + "engines": { + "node": ">=18.17.0" + }, + "peerDependencies": { + "@playwright/test": "^1", + "cypress": "^13 || ^14" + }, + "peerDependenciesMeta": { + "@playwright/test": { + "optional": true + }, + "cypress": { + "optional": true + } + } + }, "node_modules/@clerk/types": { "version": "4.101.14", "resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.101.14.tgz", @@ -6634,6 +6663,19 @@ "license": "MIT", "peer": true }, + "node_modules/dotenv": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 207cc25b..4a3ddd36 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,6 +33,7 @@ "remark-gfm": "^4.0.1" }, "devDependencies": { + "@clerk/testing": "^1.13.35", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@testing-library/user-event": "^14.5.2",