diff --git a/e2e/api.spec.ts b/e2e/api.spec.ts index 007783e2a..99b8642f9 100644 --- a/e2e/api.spec.ts +++ b/e2e/api.spec.ts @@ -51,8 +51,7 @@ test("[API E2E] /api/metrics/streak returns 401 without a session", async ({ expect([401, 302, 403]).toContain(res.status()); }); -test("[API E2E] /api/metrics/contributions returns 200 with valid session cookie", async ({ - page, +test("[API E2E] /api/metrics/contributions accepts valid session cookie", async ({ request, }) => { const sessionToken = await buildSessionCookie(); @@ -117,20 +116,13 @@ test("[API E2E] /api/goals POST without session returns 401 or 403", async ({ }); test("[API E2E] /api/metrics/contributions with days param returns valid JSON when authenticated", async ({ - page, + request, }) => { const sessionToken = await buildSessionCookie(); - await page.context().addCookies([ - { - name: "next-auth.session-token", - value: sessionToken, - domain: "127.0.0.1", - path: "/", - httpOnly: true, - sameSite: "Lax", - secure: false, - expires: Math.floor(Date.now() / 1000) + 60 * 60, + const res = await request.get("/api/metrics/contributions?days=30", { + headers: { + Cookie: `next-auth.session-token=${sessionToken}`, }, ]); diff --git a/e2e/dashboard-widgets.spec.js b/e2e/dashboard-widgets.spec.js index 282119595..1bdabe034 100644 --- a/e2e/dashboard-widgets.spec.js +++ b/e2e/dashboard-widgets.spec.js @@ -316,6 +316,7 @@ function mockMetricResponse(url) { longest: 9, lastCommitDate: "2026-05-18", totalActiveDays: 12, + freezeDates: [], }; } if (url.includes("/api/metrics/weekly-summary")) { diff --git a/e2e/dashboard.spec.ts b/e2e/dashboard.spec.ts index f68197388..f64a2d52b 100644 --- a/e2e/dashboard.spec.ts +++ b/e2e/dashboard.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from "@playwright/test"; import { encode } from "next-auth/jwt"; +import { scrollToWidget } from "./helpers/dashboard-mocks"; /** * dashboard.spec.ts @@ -141,7 +142,7 @@ async function injectMockSession(page: import("@playwright/test").Page) { await page.route("**/api/streak/freeze**", (route) => route.fulfill({ contentType: "application/json", - body: JSON.stringify({ freezes: [] }), + body: JSON.stringify({ hasFreeze: false, freezeDate: null }), }) ); @@ -316,35 +317,29 @@ test("[Dashboard E2E] dashboard heading is visible after mock login", async ({ }); test("[Dashboard E2E] Commits widget renders", async ({ page }) => { - await page.goto("/dashboard", { waitUntil: "load" }); + await page.goto("/dashboard", { waitUntil: "domcontentloaded" }); await expect( page.getByRole("heading", { name: "Dashboard", exact: true }) ).toBeVisible({ timeout: 30_000 }); - await expect( - page.getByRole("heading", { name: "Your Commits" }) - ).toBeVisible({ timeout: 10_000 }); + await scrollToWidget(page, "Your Commits"); }); test("[Dashboard E2E] PR Analytics widget renders", async ({ page }) => { - await page.goto("/dashboard", { waitUntil: "load" }); + await page.goto("/dashboard", { waitUntil: "domcontentloaded" }); await expect( page.getByRole("heading", { name: "Dashboard", exact: true }) ).toBeVisible({ timeout: 30_000 }); - await expect( - page.getByRole("heading", { name: "PR Analytics" }) - ).toBeVisible({ timeout: 10_000 }); + await scrollToWidget(page, "PR Analytics"); }); test("[Dashboard E2E] Goals widget renders with mocked goal", async ({ page, }) => { - await page.goto("/dashboard", { waitUntil: "load" }); + await page.goto("/dashboard", { waitUntil: "domcontentloaded" }); await expect( page.getByRole("heading", { name: "Dashboard", exact: true }) ).toBeVisible({ timeout: 30_000 }); - await expect( - page.getByRole("heading", { name: "Goals", exact: true }) - ).toBeVisible({ timeout: 10_000 }); + await scrollToWidget(page, "Goals"); await expect(page.getByText("Make 10 commits")).toBeVisible({ timeout: 10_000, }); @@ -388,7 +383,7 @@ test("[Dashboard E2E] no uncaught console errors on dashboard load", async ({ }); test("[Dashboard E2E] weekly summary widget renders", async ({ page }) => { - await page.goto("/dashboard", { waitUntil: "load" }); + await page.goto("/dashboard", { waitUntil: "domcontentloaded" }); await expect( page.getByRole("heading", { name: "Dashboard", exact: true }) ).toBeVisible({ timeout: 30_000 }); diff --git a/e2e/goals.spec.ts b/e2e/goals.spec.ts index 2b33eeba6..adfebd2d3 100644 --- a/e2e/goals.spec.ts +++ b/e2e/goals.spec.ts @@ -1,5 +1,9 @@ import { expect, test } from "@playwright/test"; import { encode } from "next-auth/jwt"; +import { + installDashboardApiMocks, + scrollToWidget, +} from "./helpers/dashboard-mocks"; /** * goals.spec.ts @@ -79,43 +83,15 @@ async function setupGoalsMocks(page: import("@playwright/test").Page) { }) ); - // Stub remaining metric routes so the page loads without errors. - const stubs = [ - "**/api/metrics/contributions**", - "**/api/metrics/streak**", - "**/api/streak/freeze**", - "**/api/metrics/prs**", - "**/api/metrics/pr-breakdown**", - "**/api/metrics/pr-review-trend**", - "**/api/metrics/issues**", - "**/api/metrics/languages**", - "**/api/metrics/weekly-summary**", - "**/api/ai-insights**", - "**/api/metrics/repos**", - "**/api/metrics/pinned-repos**", - "**/api/metrics/compare**", - "**/api/metrics/repo-health**", - "**/api/metrics/ci**", - "**/api/user/github-accounts**", - "**/api/integrations/jira**", - "**/api/metrics/activity**", - "**/api/metrics/commit-time**", - "**/api/metrics/personal-records**", - "**/api/metrics/discussions**", - "**/api/metrics/inactive-repos**", - "**/api/local-coding/stats**", - "**/api/metrics/coding-time**", - "**/api/metrics/coding-activity-insights**", - "**/api/wakatime**", - "**/api/metrics/productive-hours**", - "**/api/user/pinned-repos/details**", - "**/api/metrics/repo-explorer**", - ]; - for (const pattern of stubs) { - await page.route(pattern, (route) => - route.fulfill({ contentType: "application/json", body: JSON.stringify({}) }) - ); - } + await installDashboardApiMocks(page); +} + +async function openGoalsWidget(page: import("@playwright/test").Page) { + await page.goto("/dashboard", { waitUntil: "domcontentloaded" }); + await expect( + page.getByRole("heading", { name: "Dashboard", exact: true }) + ).toBeVisible({ timeout: 30_000 }); + await scrollToWidget(page, "Goals"); } test("[Goals E2E] goals widget renders on dashboard", async ({ page }) => { @@ -132,13 +108,7 @@ test("[Goals E2E] goals widget renders on dashboard", async ({ page }) => { }); }); - await page.goto("/dashboard", { waitUntil: "load" }); - await expect( - page.getByRole("heading", { name: "Dashboard", exact: true }) - ).toBeVisible({ timeout: 30_000 }); - await expect( - page.getByRole("heading", { name: "Goals", exact: true }) - ).toBeVisible({ timeout: 10_000 }); + await openGoalsWidget(page); }); test("[Goals E2E] creating a goal sends POST /api/goals with correct payload", async ({ @@ -164,10 +134,7 @@ test("[Goals E2E] creating a goal sends POST /api/goals with correct payload", a }); }); - await page.goto("/dashboard", { waitUntil: "load" }); - await expect( - page.getByRole("heading", { name: "Dashboard", exact: true }) - ).toBeVisible({ timeout: 30_000 }); + await openGoalsWidget(page); // Scroll the goal form into view and wait for it to be interactive. const titleInput = page.getByLabel("Goal title"); @@ -231,12 +198,8 @@ test("[Goals E2E] newly created goal appears in the goals list", async ({ }); }); - await page.goto("/dashboard", { waitUntil: "load" }); - await expect( - page.getByRole("heading", { name: "Dashboard", exact: true }) - ).toBeVisible({ timeout: 30_000 }); + await openGoalsWidget(page); - // Existing goal should be present. await expect(page.getByText("Existing Goal")).toBeVisible({ timeout: 10_000 }); // Scroll goal form into view and wait for it to be interactive. @@ -303,10 +266,7 @@ test("[Goals E2E] deleting a goal removes it from the list", async ({ return route.fulfill({ status: 204, body: "" }); }); - await page.goto("/dashboard", { waitUntil: "load" }); - await expect( - page.getByRole("heading", { name: "Dashboard", exact: true }) - ).toBeVisible({ timeout: 30_000 }); + await openGoalsWidget(page); await expect(page.getByText("Goal to Delete")).toBeVisible({ timeout: 10_000 }); // Click the delete button (trash icon) next to this goal. diff --git a/e2e/notifications.spec.js b/e2e/notifications.spec.js index 0fe61305f..3f3caff3d 100644 --- a/e2e/notifications.spec.js +++ b/e2e/notifications.spec.js @@ -43,7 +43,10 @@ function mockMetricResponse(url) { longest: 9, lastCommitDate: "2026-05-18", totalActiveDays: 12, + freezeDates: [], }; + if (url.includes("/api/streak/freeze")) + return { hasFreeze: false, freezeDate: null }; if (url.includes("/api/metrics/weekly-summary")) return { commits: { current: 10, previous: 7, delta: 3, trend: "up" }, @@ -99,7 +102,11 @@ function mockMetricResponse(url) { timezone: "UTC", }; if (url.includes("/api/metrics/contributions")) - return { data: { "2026-05-16": 3, "2026-05-17": 5, "2026-05-18": 2 } }; + return { + days: 365, + total: 10, + data: { "2026-05-16": 3, "2026-05-17": 5, "2026-05-18": 2 }, + }; if (url.includes("/api/metrics/productive-hours")) return { grid: [], peak: null, total: 0, days: 0, timezone: "UTC" }; if (url.includes("/api/user/pinned-repos/details")) diff --git a/e2e/streak.spec.ts b/e2e/streak.spec.ts index e390fdb5a..9ffb84b88 100644 --- a/e2e/streak.spec.ts +++ b/e2e/streak.spec.ts @@ -1,10 +1,10 @@ import { expect, test } from "@playwright/test"; import { encode } from "next-auth/jwt"; - -/** - * streak.spec.ts - * Covers: streak widget shows numeric values; freeze button is present. - */ +import { + installDashboardApiMocks, + scrollToWidget, + streakSection, +} from "./helpers/dashboard-mocks"; const AUTH_SECRET = process.env.NEXTAUTH_SECRET ?? "test-nextauth-secret-for-playwright-tests"; @@ -163,20 +163,18 @@ test.beforeEach(async ({ page }) => { test("[Streak E2E] streak widget section is rendered on dashboard", async ({ page, }) => { - await page.goto("/dashboard", { waitUntil: "load" }); + await page.goto("/dashboard", { waitUntil: "domcontentloaded" }); await expect( page.getByRole("heading", { name: "Dashboard", exact: true }) ).toBeVisible({ timeout: 30_000 }); - // The streak section may use "Streak", "Current Streak", or similar heading. - await expect( - page.getByRole("heading", { name: /streak/i }).first() - ).toBeVisible({ timeout: 10_000 }); + + await scrollToWidget(page, "Commit Streaks"); }); test("[Streak E2E] streak widget shows the mocked current streak value", async ({ page, }) => { - await page.goto("/dashboard", { waitUntil: "load" }); + await page.goto("/dashboard", { waitUntil: "domcontentloaded" }); await expect( page.getByRole("heading", { name: "Dashboard", exact: true }) ).toBeVisible({ timeout: 30_000 }); @@ -195,19 +193,25 @@ test("[Streak E2E] streak widget shows the mocked current streak value", async ( test("[Streak E2E] streak widget shows the mocked longest streak value", async ({ page, }) => { - await page.goto("/dashboard", { waitUntil: "load" }); + await page.goto("/dashboard", { waitUntil: "domcontentloaded" }); await expect( page.getByRole("heading", { name: "Dashboard", exact: true }) ).toBeVisible({ timeout: 30_000 }); - // The mock returns longest: 21. - await expect(page.getByText(/21/).first()).toBeVisible({ timeout: 10_000 }); + const section = streakSection(page); + await section.scrollIntoViewIfNeeded(); + await expect(section.getByText("Longest Streak")).toBeVisible({ + timeout: 15_000, + }); + await expect(section.getByText("21", { exact: true })).toBeVisible({ + timeout: 10_000, + }); }); test("[Streak E2E] freeze button is present in the streak widget", async ({ page, }) => { - await page.goto("/dashboard", { waitUntil: "load" }); + await page.goto("/dashboard", { waitUntil: "domcontentloaded" }); await expect( page.getByRole("heading", { name: "Dashboard", exact: true }) ).toBeVisible({ timeout: 30_000 }); @@ -240,19 +244,19 @@ test("[Streak E2E] streak freeze API is called when freeze button is clicked", a }); }); - await page.goto("/dashboard", { waitUntil: "load" }); + await page.goto("/dashboard", { waitUntil: "domcontentloaded" }); await expect( page.getByRole("heading", { name: "Dashboard", exact: true }) ).toBeVisible({ timeout: 30_000 }); - const freezeBtn = page - .getByRole("button", { name: /freeze|protect/i }) - .first(); - await expect(freezeBtn).toBeVisible({ timeout: 10_000 }); - await freezeBtn.click(); + const freezeButton = streakSection(page).getByRole("button", { + name: "Freeze Streak", + }); + await freezeButton.scrollIntoViewIfNeeded(); + await expect(freezeButton).toBeVisible({ timeout: 15_000 }); + await freezeButton.click(); - // Give the network request time to fire. await expect .poll(() => freezeRequests.length, { timeout: 8_000 }) .toBeGreaterThan(0); -}); \ No newline at end of file +}); diff --git a/src/middleware.ts b/src/middleware.ts index 53e4725c8..b6a70646d 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -51,17 +51,17 @@ const RATE_LIMIT_CONFIG = { /** * Maximum allowed API metrics requests for authenticated users in the window. */ - AUTHENTICATED_LIMIT: isDev ? 5000 : 60, + AUTHENTICATED_LIMIT: isRelaxedRateLimit ? 5000 : 60, /** * Maximum allowed API metrics requests for anonymous users in the window. */ - ANONYMOUS_LIMIT: isDev ? 1000 : 10, + ANONYMOUS_LIMIT: isRelaxedRateLimit ? 1000 : 10, /** * Maximum allowed sign-in attempts for authentication routes in the window. */ - AUTH_LIMIT: isDev ? 1000 : AUTH_LIMIT, + AUTH_LIMIT: isRelaxedRateLimit ? 1000 : AUTH_LIMIT, } as const; const memoryBuckets = new Map();