diff --git a/frontend/cypress/e2e/activity_feed.cy.ts b/frontend/cypress/e2e/activity_feed.cy.ts new file mode 100644 index 00000000..522730cc --- /dev/null +++ b/frontend/cypress/e2e/activity_feed.cy.ts @@ -0,0 +1,94 @@ +describe("/activity feed", () => { + const apiBase = "**/api/v1"; + + function stubStreamEmpty() { + // Return a minimal SSE response that ends immediately. + cy.intercept( + "GET", + `${apiBase}/activity/task-comments/stream*`, + { + statusCode: 200, + headers: { + "content-type": "text/event-stream", + }, + body: "", + }, + ).as("activityStream"); + } + + it("happy path: renders task comment cards", () => { + cy.intercept("GET", `${apiBase}/activity/task-comments*`, { + statusCode: 200, + body: { + items: [ + { + id: "c1", + message: "Hello world", + agent_name: "Kunal", + agent_role: "QA 2", + board_id: "b1", + board_name: "Testing", + task_id: "t1", + task_title: "CI hardening", + created_at: "2026-02-07T00:00:00Z", + }, + { + id: "c2", + message: "Second comment", + agent_name: "Riya", + agent_role: "QA", + board_id: "b1", + board_name: "Testing", + task_id: "t2", + task_title: "Coverage policy", + created_at: "2026-02-07T00:01:00Z", + }, + ], + }, + }).as("activityList"); + + stubStreamEmpty(); + + cy.visit("/activity", { + onBeforeLoad(win) { + win.localStorage.clear(); + }, + }); + + cy.wait("@activityList"); + + cy.contains(/live feed/i).should("be.visible"); + cy.contains("CI hardening").should("be.visible"); + cy.contains("Coverage policy").should("be.visible"); + cy.contains("Hello world").should("be.visible"); + }); + + it("empty state: shows waiting message when no items", () => { + cy.intercept("GET", `${apiBase}/activity/task-comments*`, { + statusCode: 200, + body: { items: [] }, + }).as("activityList"); + + stubStreamEmpty(); + + cy.visit("/activity"); + cy.wait("@activityList"); + + cy.contains(/waiting for new comments/i).should("be.visible"); + }); + + it("error state: shows failure UI when API errors", () => { + cy.intercept("GET", `${apiBase}/activity/task-comments*`, { + statusCode: 500, + body: { detail: "boom" }, + }).as("activityList"); + + stubStreamEmpty(); + + cy.visit("/activity"); + cy.wait("@activityList"); + + // UI uses query.error.message or fallback. + cy.contains(/unable to load feed|boom/i).should("be.visible"); + }); +}); diff --git a/frontend/src/auth/clerk.tsx b/frontend/src/auth/clerk.tsx index 02cff2c8..e5e4483e 100644 --- a/frontend/src/auth/clerk.tsx +++ b/frontend/src/auth/clerk.tsx @@ -19,20 +19,29 @@ import type { ComponentProps } from "react"; import { isLikelyValidClerkPublishableKey } from "@/auth/clerkKey"; +function isE2EAuthBypassEnabled(): boolean { + // Used only for Cypress E2E to keep tests secretless and deterministic. + // When enabled, we treat the user as signed in and skip Clerk entirely. + return process.env.NEXT_PUBLIC_E2E_AUTH_BYPASS === "1"; +} + export function isClerkEnabled(): boolean { // IMPORTANT: keep this in sync with AuthProvider; otherwise components like // may render without a and crash during prerender. + if (isE2EAuthBypassEnabled()) return false; return isLikelyValidClerkPublishableKey( process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, ); } export function SignedIn(props: { children: ReactNode }) { + if (isE2EAuthBypassEnabled()) return <>{props.children}; if (!isClerkEnabled()) return null; return {props.children}; } export function SignedOut(props: { children: ReactNode }) { + if (isE2EAuthBypassEnabled()) return null; if (!isClerkEnabled()) return <>{props.children}; return {props.children}; } @@ -58,6 +67,15 @@ export function useUser() { } export function useAuth() { + if (isE2EAuthBypassEnabled()) { + return { + isLoaded: true, + isSignedIn: true, + userId: "e2e-user", + sessionId: "e2e-session", + getToken: async () => "e2e-token", + } as const; + } if (!isClerkEnabled()) { return { isLoaded: true,