mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 20:21:39 +03:00
223 lines
11 KiB
TypeScript
223 lines
11 KiB
TypeScript
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!)
|
|
}
|
|
})
|
|
}
|