import { composeStories } from "@storybook/react" import { expect, userEvent, waitFor } from "storybook/test" import { afterEach, test } from "vitest" import { forgetScrollOffsets } from "../Main/scrollMemory.ts" import { mountStory } from "../mountStory.testHelpers.ts" import { expectAgentDrivable } from "../testing/index.ts" import { DESKTOP, setViewport, } from "../viewport.testHelpers.ts" import * as stories from "./ReactRouterAdapter.stories.tsx" const { Playground } = composeStories(stories) // The offsets are module state, so they outlive a mount. A test // must not. afterEach(() => { forgetScrollOffsets() }) /** * The wiring, against a real router. * * `Main.test.tsx` proves the scroll mechanism against a hand-rolled * history stack — the right subject for a `ResizeObserver` and a * clamped offset. What it cannot prove is that the entry reaches * `Main` at all when the value comes from `useLocation()` rather * than from a story's own `useState`. That is the seam every app * now depends on, and it is the kind that fails quietly: the links * still navigate and the list still scrolls. */ test("an app renders one component and gets both seams", async () => { await setViewport(DESKTOP) const { canvas } = await mountStory(Playground) const main = canvas.getByRole("main") // The link seam: react-router's `` renders an ``, // so this is also what an unwired app looks like — the assertion // that separates them is the absence of a page load below. const link = expectAgentDrivable(canvas, { name: "Open Episode 12", role: "link", }) await expect(link).toHaveAttribute( "href", "/library/Episode 12", ) main.scrollTop = 900 await waitFor(async () => { await expect(main.scrollTop).toBe(900) }) await new Promise((resolve) => { globalThis.requestAnimationFrame(() => { globalThis.requestAnimationFrame(resolve) }) }) await userEvent.click(link) // A soft navigation: the route changed inside the same document. // A plain `` would have left the story mounted and unchanged. // // `waitFor`, because the route commit is a state update and the // click resolves before it. It passed locally and failed on CI // for exactly that reason. await waitFor(async () => { await expect( canvas.getByRole("heading", { name: "Episode" }), ).toBeVisible() }) await userEvent.click( expectAgentDrivable(canvas, { name: "Back", role: "button", }), ) // The scroll seam, from the same one component. await waitFor(async () => { await expect(canvas.getByRole("main").scrollTop).toBe( 900, ) }) })