packages/app/component-tests/timeline-virtualizer.spec.ts
Rama dev · réplica de lectura del repositorio original.
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!)
}
})
}