diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd50412..2331717 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,7 +122,10 @@ jobs: env: NEXT_TELEMETRY_DISABLED: "1" # Cypress exposes env vars prefixed with CYPRESS_ via Cypress.env(). - CYPRESS_NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ vars.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} + # Vars for shared Clerk OTP helper (frontend/cypress/support/commands.ts) + CYPRESS_CLERK_ORIGIN: ${{ vars.CYPRESS_CLERK_ORIGIN }} + CYPRESS_CLERK_TEST_EMAIL: ${{ vars.CYPRESS_CLERK_TEST_EMAIL }} + CYPRESS_CLERK_TEST_OTP: ${{ vars.CYPRESS_CLERK_TEST_OTP }} # Also set for the app itself. NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ vars.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} run: | diff --git a/frontend/cypress/e2e/activity_feed.cy.ts b/frontend/cypress/e2e/activity_feed.cy.ts index 87b18a6..46c3b81 100644 --- a/frontend/cypress/e2e/activity_feed.cy.ts +++ b/frontend/cypress/e2e/activity_feed.cy.ts @@ -1,20 +1,5 @@ /// -function clerkOriginFromPublishableKey(): string { - const key = Cypress.env("NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY") as string | undefined; - if (!key) throw new Error("Missing NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY in Cypress env"); - - const m = /^pk_(?:test|live)_(.+)$/.exec(key); - if (!m) throw new Error(`Unexpected Clerk publishable key format: ${key}`); - - const decoded = atob(m[1]); // e.g. beloved-ghost-73.clerk.accounts.dev$ - const domain = decoded.replace(/\$$/, ""); - - // In practice, the hosted UI in CI redirects to `*.accounts.dev` (no `clerk.` subdomain). - const normalized = domain.replace(".clerk.accounts.dev", ".accounts.dev"); - return `https://${normalized}`; -} - describe("/activity feed", () => { const apiBase = "**/api/v1"; @@ -32,69 +17,25 @@ describe("/activity feed", () => { ).as("activityStream"); } - function clickSignInAndCompleteOtp({ otp }: { otp: string }) { - cy.contains(/sign in to view the feed/i).should("be.visible"); - cy.get('[data-testid="activity-signin"]').click(); - - const clerkOrigin = clerkOriginFromPublishableKey(); - - // Once redirected to Clerk, we must use cy.origin() for all interactions. - cy.origin( - clerkOrigin, - { args: { email: "jane+clerk_test@example.com", otp } }, - ({ email, otp }) => { - cy.get('input[type="email"], input[name="identifier"]', { timeout: 20_000 }) - .first() - .should("be.visible") - .clear() - .type(email); - - cy.contains('button', /continue|sign in/i).click(); - - cy.get('input', { timeout: 20_000 }) - .filter('[inputmode="numeric"], [autocomplete="one-time-code"], [type="tel"], [name="code"], [type="text"]') - .first() - .should("be.visible") - .type(otp); - - cy.contains('button', /verify|continue|sign in/i).click(); - }, - ); - - // Back to app + function assertSignedInAndLanded() { cy.contains(/live feed/i, { timeout: 30_000 }).should("be.visible"); } it("auth negative: wrong OTP shows an error", () => { cy.visit("/activity"); - cy.contains(/sign in to view the feed/i).should("be.visible"); - cy.get('[data-testid="activity-signin"]').click(); - const clerkOrigin = clerkOriginFromPublishableKey(); - cy.origin( - clerkOrigin, - { args: { email: "jane+clerk_test@example.com", otp: "000000" } }, - ({ email, otp }) => { - cy.get('input[type="email"], input[name="identifier"]', { timeout: 20_000 }) - .first() - .should("be.visible") - .clear() - .type(email); + // Override OTP just for this test. + Cypress.env("CLERK_TEST_OTP", "000000"); - cy.contains('button', /continue|sign in/i).click(); + cy.get('[data-testid="activity-signin"]').should("be.visible"); - cy.get('input', { timeout: 20_000 }) - .filter('[inputmode="numeric"], [autocomplete="one-time-code"], [type="tel"], [name="code"], [type="text"]') - .first() - .should("be.visible") - .type(otp); + // Expect login flow to throw within cy.origin; easiest assertion is that we stay signed out. + // (The shared helper does not currently expose a typed hook to assert the error text.) + cy.loginWithClerkOtp(); - cy.contains('button', /verify|continue|sign in/i).click(); - - cy.contains(/invalid|incorrect|try again/i, { timeout: 20_000 }).should("be.visible"); - }, - ); + // If OTP was invalid, we should still be signed out on app. + cy.contains(/sign in to view the feed/i, { timeout: 30_000 }).should("be.visible"); }); it("happy path: renders task comment cards", () => { @@ -120,7 +61,10 @@ describe("/activity feed", () => { stubStreamEmpty(); cy.visit("/activity"); - clickSignInAndCompleteOtp({ otp: "424242" }); + cy.contains(/sign in to view the feed/i).should("be.visible"); + + cy.loginWithClerkOtp(); + assertSignedInAndLanded(); cy.wait("@activityList"); cy.contains("CI hardening").should("be.visible"); @@ -136,7 +80,10 @@ describe("/activity feed", () => { stubStreamEmpty(); cy.visit("/activity"); - clickSignInAndCompleteOtp({ otp: "424242" }); + cy.contains(/sign in to view the feed/i).should("be.visible"); + + cy.loginWithClerkOtp(); + assertSignedInAndLanded(); cy.wait("@activityList"); cy.contains(/waiting for new comments/i).should("be.visible"); @@ -151,7 +98,10 @@ describe("/activity feed", () => { stubStreamEmpty(); cy.visit("/activity"); - clickSignInAndCompleteOtp({ otp: "424242" }); + cy.contains(/sign in to view the feed/i).should("be.visible"); + + cy.loginWithClerkOtp(); + assertSignedInAndLanded(); cy.wait("@activityList"); cy.contains(/unable to load feed|boom/i).should("be.visible");