import { expect, sourceURL, story } from "../../stories/playwright/story" const fixture = sourceURL(new URL("./timeline-virtualizer.fixture.tsx", import.meta.url)) story.beforeEach(async ({ mount }) => { const component = await mount("opencode-composer-flow--mixed-attachments") await expect(component.getByRole("textbox", { name: "Prompt", exact: true })).toBeVisible() }) story("spaces the first mobile message without changing desktop spacing", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }) await page.evaluate(async (fixture) => { const { mountTimelineVirtualizer } = await import(fixture) mountTimelineVirtualizer({ count: 1, rowHeight: 60, immediate: true }) }, fixture) const root = page.getByTestId("timeline-virtualizer-fixture") await root.getByRole("button", { name: "Complete Markdown", exact: true }).click() const content = root.locator("[data-timeline-virtual-content]") await expect(content).toHaveCSS("visibility", "visible") const gap = () => root.locator('[data-timeline-key="user-message:message-0"]').evaluate((element) => { const viewport = element.closest("[data-scrollable]")! return element.getBoundingClientRect().top - viewport.getBoundingClientRect().top }) await expect.poll(gap).toBe(16) await root.evaluate((element) => element.setAttribute("dir", "rtl")) await expect.poll(gap).toBe(16) await page.setViewportSize({ width: 1280, height: 900 }) await expect.poll(gap).toBe(0) await page.setViewportSize({ width: 390, height: 844 }) await expect.poll(gap).toBe(16) }) story("bounds the cheap suffix and reveals only ready measured rows", async ({ page }) => { await page.evaluate(async (fixture) => { const { mountTimelineVirtualizer } = await import(fixture) mountTimelineVirtualizer({ count: 100, rowHeight: 60, immediate: true }) }, fixture) const root = page.getByTestId("timeline-virtualizer-fixture") const content = root.locator("[data-timeline-virtual-content]") await expect(root).toHaveAttribute("data-observed-height", "180") await expect(content).toHaveCSS("visibility", "hidden") await expect(content.locator("[data-timeline-key]")).toHaveCount(4) await root.getByRole("button", { name: "Complete Markdown", exact: true }).click() await expect(content).toHaveCSS("visibility", "visible") await expect(root).toHaveAttribute("data-first-reveal", /.+/) expect(await root.evaluate((element) => JSON.parse(element.dataset.firstReveal!))).toMatchObject({ rows: [96, 97, 98, 99], pendingMarkdown: 0, clipped: [], viewportHeight: 180, }) }) story("reveals at the end when Markdown never reports ready", async ({ page }) => { // A block that never set data-markdown-ready kept every reveal check waiting, and the timeline stayed invisible. await page.evaluate(async (fixture) => { const { mountTimelineVirtualizer } = await import(fixture) mountTimelineVirtualizer({ count: 100, rowHeight: 60, immediate: true }) }, fixture) const root = page.getByTestId("timeline-virtualizer-fixture") const content = root.locator("[data-timeline-virtual-content]") await expect(content).toHaveCSS("visibility", "hidden") await expect(content).toHaveCSS("visibility", "visible") const reveal = await root.evaluate((element) => JSON.parse(element.dataset.firstReveal!)) expect(reveal.pendingMarkdown).toBeGreaterThan(0) expect(reveal.rows).toContain(99) const viewport = root.locator("[data-scrollable]") await expect .poll(() => viewport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop)) .toBeLessThanOrEqual(1) }) story("treats the reachable end as the end under page zoom", async ({ page }) => { // At 110% the browser stops scrolling more than a pixel short of scrollHeight - clientHeight. A mount that waited // for that distance to close stayed blank, or froze the renderer in a microtask loop, and scrolling back down to // the bottom never resumed following. await page.evaluate(async (fixture) => { document.documentElement.style.zoom = "1.1" const { mountTimelineVirtualizer } = await import(fixture) mountTimelineVirtualizer({ count: 4, rowHeight: 57.8, immediate: true, viewportHeight: 200.5 }) }, fixture) const root = page.getByTestId("timeline-virtualizer-fixture") const content = root.locator("[data-timeline-virtual-content]") await root.getByRole("button", { name: "Complete Markdown", exact: true }).click() await expect(content).toHaveCSS("visibility", "visible") const viewport = root.locator("[data-scrollable]") expect( await viewport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop), ).toBeGreaterThan(1) const controls = root.getByTestId("timeline-controls") await viewport.dispatchEvent("wheel", { deltaY: -1 }) await expect(controls).toHaveAttribute("data-pinned", "false") for (const top of [40, Number.MAX_SAFE_INTEGER]) await viewport.evaluate( (element, top) => new Promise((resolve) => { element.addEventListener("scroll", resolve, { once: true }) element.scrollTop = top }), top, ) await expect(controls).toHaveAttribute("data-pinned", "true") }) story("leaves a reader who scrolled up in place after a scroll to the zoomed end", async ({ page }) => { // At 110% the end the browser reaches sits more than a pixel short of scrollHeight - clientHeight. TanStack kept // scrolling toward that max on every frame for five seconds, so a row that grew meanwhile pulled the reader back down. await page.evaluate(async (fixture) => { document.documentElement.style.zoom = "1.1" const { mountTimelineVirtualizer } = await import(fixture) mountTimelineVirtualizer({ count: 40, rowHeight: 57.8, immediate: true, viewportHeight: 200.5 }) }, fixture) const root = page.getByTestId("timeline-virtualizer-fixture") await root.getByRole("button", { name: "Complete Markdown", exact: true }).click() await expect(root.locator("[data-timeline-virtual-content]")).toHaveCSS("visibility", "visible") const viewport = root.locator("[data-scrollable]") const distance = () => viewport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop) expect(await distance()).toBeGreaterThan(1.01) // A scroll command settles or scrolls again on its next frame. const frames = () => page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))) await root.getByRole("button", { name: "Scroll to end", exact: true }).click() await frames() // The wheel and the scroll it causes arrive together. A wheel alone would leave an earlier arrival at the end free to // pin again once that scroll settles. await viewport.evaluate( (element) => new Promise((resolve) => { element.addEventListener("scroll", resolve, { once: true }) element.dispatchEvent(new WheelEvent("wheel", { deltaY: -1, bubbles: true })) element.scrollTop -= 40 }), ) await expect(root.getByTestId("timeline-controls")).toHaveAttribute("data-pinned", "false") const height = await viewport.evaluate((element) => element.scrollHeight) await root.getByRole("button", { name: "Grow last row", exact: true }).click() await expect.poll(() => viewport.evaluate((element) => element.scrollHeight)).toBeGreaterThan(height + 30) await frames() expect(await distance()).toBeGreaterThan(70) }) story("holds a revealed tool inside a group of several through a later resize", async ({ page }) => { // The anchor looked only for an element naming the tool alone, so a grouped tool let go on the next resize. await page.evaluate(async (fixture) => { const { mountTimelineVirtualizer } = await import(fixture) mountTimelineVirtualizer({ count: 40, rowHeight: 60, immediate: true }) }, fixture) const root = page.getByTestId("timeline-virtualizer-fixture") const controls = root.getByTestId("timeline-controls") await root.getByRole("button", { name: "Complete Markdown", exact: true }).click() await expect(root.locator("[data-timeline-virtual-content]")).toHaveCSS("visibility", "visible") await expect(controls).toHaveAttribute("data-pinned", "true") await root.getByRole("button", { name: "Anchor first row", exact: true }).click() await expect(root.locator('[data-timeline-part-ids="prt_first,prt_grouped"]')).toBeAttached() await root.getByRole("button", { name: "Grow last row", exact: true }).click() const viewport = root.locator("[data-scrollable]") await expect.poll(() => viewport.evaluate((element) => element.scrollTop)).toBe(0) await expect(controls).toHaveAttribute("data-anchored", "true") }) for (const input of [ { name: "offset-only", count: 1, rowHeight: 600, reveal: { scrollTop: 0 } }, { name: "zero-height", count: 4, rowHeight: 60, reveal: {} }, ]) { story(`reveals ready measured rows after an ${input.name} reconnect`, async ({ page }) => { await page.evaluate( async ({ fixture, input }) => { const { mountTimelineVirtualizer } = await import(fixture) mountTimelineVirtualizer(input) }, { fixture, input }, ) const root = page.getByTestId("timeline-virtualizer-fixture") const content = root.locator("[data-timeline-virtual-content]") await expect(root).toHaveAttribute("data-observed-height", "180") await expect(content).toHaveCSS("visibility", "hidden") await expect(content.locator("[data-timeline-key]")).toHaveCount(1) if (input.name === "offset-only") { // Pinned to the end: the 600px row and the virtualizer's 92px tail, less the 180px viewport. await expect(root).toHaveAttribute("data-last-scroll-top", "512") await root.locator("[data-scrollable]").dispatchEvent("wheel", { deltaY: -1 }) await expect(root.getByTestId("timeline-controls")).toHaveAttribute("data-pinned", "false") } if (input.name === "zero-height") { await root.getByRole("button", { name: "Hide viewport", exact: true }).click() // Wait for ResizeObserver to clear the actual range, not just for display:none. await expect(root).toHaveAttribute("data-observed-height", "0") await expect(content.locator("[data-timeline-key]")).toHaveCount(0) } await expect(root).not.toHaveAttribute("data-first-reveal") const resizes = await root.getAttribute("data-viewport-resizes") await root.getByRole("button", { name: "Reconnect ready rows", exact: true }).click() await expect(content).toHaveCSS("visibility", "visible") await expect(root).toHaveAttribute("data-first-reveal", /.+/) expect(await root.evaluate((element) => JSON.parse(element.dataset.firstReveal!))).toMatchObject({ rows: input.count === 1 ? [0] : [0, 1, 2, 3], pendingMarkdown: 0, clipped: [], viewportHeight: 180, ...input.reveal, }) if (input.name === "offset-only") { // This repair must not depend on another native scroll or resize delivery. await expect(root).toHaveAttribute("data-scrolls", "0") await expect(root).toHaveAttribute("data-viewport-resizes", resizes!) } }) }