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
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/**

View File

@@ -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 });
},
},
});

View File

@@ -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();

View File

@@ -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");
});
});

View File

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

View File

@@ -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",

View File

@@ -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",