test(e2e): migrate Cypress auth to @clerk/testing commands

This commit is contained in:
Kunal
2026-02-08 14:52:03 +00:00
parent 76dc011459
commit bd9ee7883a
7 changed files with 91 additions and 38 deletions

View File

@@ -121,14 +121,24 @@ jobs:
- name: Run Cypress E2E - name: Run Cypress E2E
env: env:
NEXT_TELEMETRY_DISABLED: "1" NEXT_TELEMETRY_DISABLED: "1"
# Vars for shared Clerk OTP helper (frontend/cypress/support/commands.ts) # Clerk testing tokens (official @clerk/testing Cypress integration)
# Provide deterministic test creds directly (no secretless skipping). CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
CYPRESS_CLERK_TEST_EMAIL: "jane+clerk_test@example.com" CLERK_PUBLISHABLE_KEY: ${{ vars.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
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 }}
# Also set for the app itself. # Also set for the app itself.
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ vars.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} 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: | run: |
cd frontend cd frontend
npm run e2e -- --browser chrome 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/**

View File

@@ -1,4 +1,5 @@
import { defineConfig } from "cypress"; import { defineConfig } from "cypress";
import { clerkSetup } from "@clerk/testing/cypress";
export default defineConfig({ export default defineConfig({
env: { env: {
@@ -12,5 +13,8 @@ export default defineConfig({
baseUrl: "http://localhost:3000", baseUrl: "http://localhost:3000",
specPattern: "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}", specPattern: "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}",
supportFile: "cypress/support/e2e.ts", supportFile: "cypress/support/e2e.ts",
setupNodeEvents(on, config) {
return clerkSetup({ config });
},
}, },
}); });

View File

@@ -2,6 +2,7 @@
describe("/activity feed", () => { describe("/activity feed", () => {
const apiBase = "**/api/v1"; const apiBase = "**/api/v1";
const email = Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com";
function stubStreamEmpty() { function stubStreamEmpty() {
cy.intercept( cy.intercept(
@@ -21,18 +22,10 @@ describe("/activity feed", () => {
cy.contains(/live feed/i, { timeout: 30_000 }).should("be.visible"); cy.contains(/live feed/i, { timeout: 30_000 }).should("be.visible");
} }
it("auth negative: wrong OTP keeps us on sign-in", () => { it("auth negative: signed-out user cannot access /activity", () => {
// Start from app-origin sign-in to avoid cross-origin confusion. // Story: signed-out user tries to visit /activity and is redirected to sign-in.
cy.visit("/sign-in"); cy.visit("/activity");
cy.location("pathname", { timeout: 20_000 }).should("match", /\/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("happy path: renders task comment cards", () => { it("happy path: renders task comment cards", () => {
@@ -58,7 +51,9 @@ describe("/activity feed", () => {
stubStreamEmpty(); stubStreamEmpty();
// Story: user signs in, then visits /activity and sees the live feed. // 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"); cy.visit("/activity");
assertSignedInAndLanded(); assertSignedInAndLanded();
@@ -77,7 +72,9 @@ describe("/activity feed", () => {
stubStreamEmpty(); stubStreamEmpty();
// Story: user signs in, then visits /activity and sees an empty-state message. // 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"); cy.visit("/activity");
assertSignedInAndLanded(); assertSignedInAndLanded();
@@ -95,7 +92,9 @@ describe("/activity feed", () => {
stubStreamEmpty(); stubStreamEmpty();
// Story: user signs in, then visits /activity; API fails and user sees an error. // 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"); cy.visit("/activity");
assertSignedInAndLanded(); assertSignedInAndLanded();

View File

@@ -1,19 +1,15 @@
describe("Clerk login (OTP)", () => { describe("Clerk login", () => {
it("can sign in via Clerk modal", () => { it("user can sign in via Clerk testing commands", () => {
// Skip unless explicitly configured. const email = Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com";
const clerkOrigin = Cypress.env("CLERK_ORIGIN");
const email = Cypress.env("CLERK_TEST_EMAIL");
const otp = Cypress.env("CLERK_TEST_OTP");
if (!clerkOrigin || !email || !otp) { // Prereq per Clerk docs: visit a non-protected page that loads Clerk.
cy.log("Skipping: missing CYPRESS_CLERK_ORIGIN / CYPRESS_CLERK_TEST_EMAIL / CYPRESS_CLERK_TEST_OTP"); cy.visit("/sign-in");
return; cy.clerkLoaded();
}
cy.clerkSignIn({ strategy: "email_code", identifier: email });
// After login, user should be able to access protected route.
cy.visit("/activity"); cy.visit("/activity");
cy.loginWithClerkOtp(); cy.contains(/live feed/i, { timeout: 30_000 }).should("be.visible");
// After login, the SignedIn UI should render.
cy.contains(/live feed/i, { timeout: 20_000 }).should("be.visible");
}); });
}); });

View File

@@ -1,4 +1,5 @@
// Cypress support file. // Cypress support file.
// Place global hooks/commands here. // Place global hooks/commands here.
import "@clerk/testing/cypress";
import "./commands"; import "./commands";

View File

@@ -26,6 +26,7 @@
"remark-gfm": "^4.0.1" "remark-gfm": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"@clerk/testing": "^1.13.35",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0", "@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^14.5.2",
@@ -364,9 +365,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@clerk/backend": { "node_modules/@clerk/backend": {
"version": "2.29.7", "version": "2.30.1",
"resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-2.29.7.tgz", "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-2.30.1.tgz",
"integrity": "sha512-OSfFQ85L0FV2wSzqlr0hRvluIu3Z5ClgLiBE6Qx7XjSGyJoqEvP5OP4fl5Nt5icgGvH0EwA1dljPGyQpaqbQEw==", "integrity": "sha512-GoxnJzVH0ycNPAGCDMfo3lPBFbo5nehpLSVFjgGEnzIRGGahBtAB8PQT7KM2zo58pD8apjb/+suhcB/WCiEasQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@clerk/shared": "^3.44.0", "@clerk/shared": "^3.44.0",
@@ -453,6 +454,34 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "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": { "node_modules/@clerk/types": {
"version": "4.101.14", "version": "4.101.14",
"resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.101.14.tgz", "resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.101.14.tgz",
@@ -6634,6 +6663,19 @@
"license": "MIT", "license": "MIT",
"peer": true "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": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",

View File

@@ -33,6 +33,7 @@
"remark-gfm": "^4.0.1" "remark-gfm": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"@clerk/testing": "^1.13.35",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0", "@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^14.5.2",