From 26a5dcf88297263b66c3dbb9b04b9f27087af7c7 Mon Sep 17 00:00:00 2001 From: azizu06 Date: Sun, 12 Jul 2026 15:17:42 -0400 Subject: [PATCH] Fix feed detail score hydration --- e2e/feed.spec.ts | 19 ++++++++++++++++++ src/app/feed/[id]/page.tsx | 2 +- src/components/react-bits/count-up.test.tsx | 22 +++++++++++++++++++++ src/components/react-bits/count-up.tsx | 10 +++++++--- 4 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 src/components/react-bits/count-up.test.tsx diff --git a/e2e/feed.spec.ts b/e2e/feed.spec.ts index 068fe58..b7c895e 100644 --- a/e2e/feed.spec.ts +++ b/e2e/feed.spec.ts @@ -15,6 +15,16 @@ async function mockYoutubeThumbnails(page: Page) { ); } +async function mockYoutubeEmbed(page: Page) { + await page.route("https://www.youtube-nocookie.com/embed/**", (route) => + route.fulfill({ + status: 200, + contentType: "text/html", + body: "Mock YouTube embed", + }), + ); +} + async function expectSafeExternalLink(link: Locator) { await expect(link).toBeVisible(); const href = await link.getAttribute("href"); @@ -27,6 +37,14 @@ async function expectSafeExternalLink(link: Locator) { test("verified feed lists vetted cards and opens the detail embed", async ({ page, }) => { + await mockYoutubeThumbnails(page); + await mockYoutubeEmbed(page); + const runtimeErrors: string[] = []; + page.on("console", (message) => { + if (message.type() === "error") runtimeErrors.push(message.text()); + }); + page.on("pageerror", (error) => runtimeErrors.push(error.message)); + await page.goto("/"); // Shared header exposes the active Feed route. @@ -120,6 +138,7 @@ test("verified feed lists vetted cards and opens the detail embed", async ({ await backLink.click(); await expect(page).toHaveURL(/\/$/); await expect(page.locator(".feed-card").first()).toBeVisible(); + expect(runtimeErrors).toEqual([]); }); test("searches, filters, resets, and recovers across every feed state", async ({ diff --git a/src/app/feed/[id]/page.tsx b/src/app/feed/[id]/page.tsx index d9a79f5..3d3feee 100644 --- a/src/app/feed/[id]/page.tsx +++ b/src/app/feed/[id]/page.tsx @@ -142,7 +142,7 @@ export default async function FeedDetail({ className={`score-num ${tone}`} aria-label={`Cap Score ${item.capScore} out of 100`} > - +

{CAP_LABELS[item.capLabel]} diff --git a/src/components/react-bits/count-up.test.tsx b/src/components/react-bits/count-up.test.tsx new file mode 100644 index 0000000..4b6e340 --- /dev/null +++ b/src/components/react-bits/count-up.test.tsx @@ -0,0 +1,22 @@ +import { render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { CountUp } from "./count-up"; + +describe("CountUp", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("renders a stable server-compatible value when animation is disabled", () => { + const requestFrame = vi.fn().mockReturnValue(1); + vi.stubGlobal("matchMedia", vi.fn().mockReturnValue({ matches: false })); + vi.stubGlobal("requestAnimationFrame", requestFrame); + vi.stubGlobal("cancelAnimationFrame", vi.fn()); + + render(); + + expect(screen.getByText("28")).toBeVisible(); + expect(requestFrame).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/react-bits/count-up.tsx b/src/components/react-bits/count-up.tsx index bfd4c99..455523c 100644 --- a/src/components/react-bits/count-up.tsx +++ b/src/components/react-bits/count-up.tsx @@ -17,6 +17,7 @@ type CountUpProps = { from?: number; durationMs?: number; className?: string; + animate?: boolean; }; // Animate only when we can both detect motion preference and drive frames. @@ -33,14 +34,17 @@ export function CountUp({ from = 0, durationMs = 200, className, + animate = true, }: CountUpProps) { - const [value, setValue] = useState(() => (canAnimate() ? from : to)); + const [value, setValue] = useState(() => + animate && canAnimate() ? from : to, + ); const frameRef = useRef(null); useEffect(() => { // The initializer already resolves the resting value; only animate when we // can drive frames, updating state asynchronously inside the rAF callback. - if (!canAnimate()) return; + if (!animate || !canAnimate()) return; const start = performance.now(); const tick = (now: number) => { @@ -57,7 +61,7 @@ export function CountUp({ return () => { if (frameRef.current !== null) cancelAnimationFrame(frameRef.current); }; - }, [to, from, durationMs]); + }, [to, from, durationMs, animate]); return {value}; }