import { expect, test, type Locator, type Page } from "@playwright/test" import { assistantMessage, setupTimeline, shell, textPart, toolPart, userMessage, } from "../performance/timeline-stability/fixture" for (const deviceScaleFactor of [1.25, 1.5]) { test(`keeps the shell outline inside a fractionally short virtual row at ${deviceScaleFactor}x`, async ({ page }) => { const shellID = "prt_shell_outline" const timeline = await setupTimeline(page, { messages: [userMessage(), assistantMessage([shell(shellID, "completed", "shell output")])], settings: { newLayoutDesigns: true, shellToolPartsExpanded: true }, reducedMotion: true, deviceScaleFactor, }) const part = page.locator(`[data-timeline-part-id="${shellID}"]`) const output = part.locator('[data-component="bash-output"]') const row = page.locator("[data-timeline-key]", { has: part }) await expect(output).toBeVisible() await timeline.settle() const geometry = await row.evaluate((element) => { const output = element.querySelector('[data-component="bash-output"]') if (!output) throw new Error("Shell output is unavailable") const rowRect = element.getBoundingClientRect() const outputRect = output.getBoundingClientRect() // Match a rounded-down measurement at a fractional device-pixel phase. element.style.height = `${outputRect.bottom - rowRect.top - 0.49}px` element.style.transform = "translateY(0.25px)" output.style.setProperty("--v2-border-border-base", "rgb(255, 0, 255)") output.style.setProperty("background", "rgb(0, 0, 0)", "important") const style = getComputedStyle(output) return { outputWidth: outputRect.width, outputHeight: outputRect.height, borderColor: style.borderTopColor, boxShadow: style.boxShadow, clipMargin: getComputedStyle(element).overflowClipMargin, } }) await timeline.settle() const clipped = await row.evaluate((element) => { const output = element.querySelector('[data-component="bash-output"]')! return output.getBoundingClientRect().bottom - element.getBoundingClientRect().bottom }) expect(clipped).toBeCloseTo(0.49, 1) expect(await page.evaluate(() => devicePixelRatio)).toBe(deviceScaleFactor) const edges = await captureCardEdges(page, output) expect(edges.box.width).toBeCloseTo(geometry.outputWidth, 2) expect(edges.box.height).toBeCloseTo(geometry.outputHeight, 2) expect(geometry.borderColor).toBe("rgb(255, 0, 255)") expect(geometry.boxShadow).toBe("none") expect(geometry.clipMargin).toBe("0.5px") expect(edges.magenta.top).toBeGreaterThan(0.75) expect(edges.magenta.bottom).toBeGreaterThan(0.75) expect(edges.magenta.vertical).toBeGreaterThanOrEqual(2) }) } test("keeps the patch card inside a fractionally short virtual row", async ({ page }) => { const patchID = "prt_patch_outline" const file = { filePath: "src/outline.ts", relativePath: "src/outline.ts", type: "update", additions: 1, deletions: 1, before: "const outline = false\n", after: "const outline = true\n", } const timeline = await setupTimeline(page, { messages: [ userMessage(), assistantMessage([ toolPart(patchID, "apply_patch", "completed", { files: [file.filePath] }, { metadata: { files: [file] } }), ]), ], settings: { editToolPartsExpanded: true, newLayoutDesigns: true }, reducedMotion: true, }) const part = page.locator(`[data-timeline-part-id="${patchID}"]`) const card = part.locator('[data-component="accordion"][data-scope="apply-patch"]') const row = page.locator("[data-timeline-key]", { has: part }) await expect(card).toBeVisible() await timeline.settle() const geometry = await row.evaluate((element) => { const card = element.querySelector('[data-component="accordion"][data-scope="apply-patch"]') if (!card) throw new Error("Patch card is unavailable") const rowRect = element.getBoundingClientRect() const cardRect = card.getBoundingClientRect() element.style.height = `${cardRect.bottom - rowRect.top - 0.49}px` const clipMargin = getComputedStyle(element).overflowClipMargin const bottom = element.getBoundingClientRect().bottom return { overflow: card.getBoundingClientRect().bottom - bottom, paintOverflow: card.getBoundingClientRect().bottom - bottom - Number.parseFloat(clipMargin), clipMargin, cardWidth: cardRect.width, cardHeight: cardRect.height, } }) await timeline.settle() expect(geometry.overflow).toBeCloseTo(0.49, 1) expect(geometry.paintOverflow).toBeLessThanOrEqual(0) const edges = await captureCardEdges(page, card) expect(edges.box.width).toBeCloseTo(geometry.cardWidth, 2) expect(edges.box.height).toBeCloseTo(geometry.cardHeight, 2) expect(edges.luminance.top).toBeLessThan(245) expect(edges.luminance.bottom).toBeLessThan(245) expect(Math.abs(edges.luminance.bottom - edges.luminance.top)).toBeLessThan(10) expect(geometry.clipMargin).toBe("0.5px") }) test("allows paint rounding for every framed row but not fixed turn gaps", async ({ page }) => { const secondUserID = "msg_outline_second_user" await setupTimeline(page, { messages: [ userMessage(undefined, { summary: { diffs: [ { file: "src/summary.ts", additions: 1, deletions: 1, patch: "@@ -1 +1 @@\n-export const value = 1\n+export const value = 2", }, ], }, }), assistantMessage([textPart("prt_outline_text", "Assistant text")]), userMessage(undefined, { id: secondUserID, created: 1700000010000 }), assistantMessage([], { id: "msg_outline_second_assistant", parentID: secondUserID, created: 1700000011000, }), ], }) await expect(page.locator('[data-timeline-row="DiffSummary"]')).toBeVisible() await expect(page.locator('[data-timeline-row="TurnGap"]')).toBeVisible() const rows = await page.locator("[data-timeline-key]").evaluateAll((elements) => elements.map((element) => ({ tag: element.querySelector("[data-timeline-row]")?.dataset.timelineRow, clipMargin: getComputedStyle(element).overflowClipMargin, })), ) expect(rows.filter((row) => row.tag !== "TurnGap").every((row) => row.clipMargin === "0.5px")).toBe(true) expect(rows.filter((row) => row.tag === "TurnGap")).toEqual([{ tag: "TurnGap", clipMargin: "0px" }]) }) async function captureCardEdges(page: Page, card: Locator) { const box = await card.boundingBox() if (!box) throw new Error("Tool card bounds are unavailable") const viewport = page.viewportSize() if (!viewport) throw new Error("Viewport bounds are unavailable") const screenshot = await page.screenshot() return page.evaluate( async ({ source, box, viewport }) => { const image = new Image() image.src = source await image.decode() const canvas = document.createElement("canvas") canvas.width = image.naturalWidth canvas.height = image.naturalHeight const context = canvas.getContext("2d") if (!context) throw new Error("2D canvas is unavailable") context.drawImage(image, 0, 0) const scale = { x: image.naturalWidth / viewport.width, y: image.naturalHeight / viewport.height, } const rows = (candidates: number[]) => { const left = Math.floor((box.x + 8) * scale.x) const width = Math.floor((box.width - 16) * scale.x) return candidates.map((row) => { const pixels = context.getImageData(left, row, width, 1).data const indexes = Array.from({ length: width }, (_, index) => index * 4) return { luminance: indexes .map((index) => (pixels[index]! + pixels[index + 1]! + pixels[index + 2]!) / 3) .reduce((sum, value) => sum + value, 0) / width, magenta: indexes.filter((index) => pixels[index]! > 200 && pixels[index + 1]! < 180 && pixels[index + 2]! > 200) .length / width, } }) } const pixels = context.getImageData(0, 0, image.naturalWidth, image.naturalHeight).data const columns = new Uint32Array(image.naturalWidth) for (let index = 0; index < pixels.length; index += 4) { if (pixels[index]! <= 200 || pixels[index + 1]! >= 180 || pixels[index + 2]! <= 200) continue columns[(index / 4) % image.naturalWidth] = columns[(index / 4) % image.naturalWidth]! + 1 } const top = box.y * scale.y const bottom = (box.y + box.height) * scale.y const topRows = rows([Math.floor(top) - 1, Math.floor(top), Math.ceil(top)]) const bottomRows = rows([Math.floor(bottom) - 2, Math.floor(bottom) - 1, Math.ceil(bottom) - 1]) return { box, luminance: { top: Math.min(...topRows.map((row) => row.luminance)), bottom: rows([Math.ceil(bottom) - 1])[0]!.luminance, }, magenta: { top: Math.max(...topRows.map((row) => row.magenta)), bottom: Math.max(...bottomRows.map((row) => row.magenta)), vertical: Array.from(columns).filter((count) => count > box.height * scale.y * 0.75).length, }, } }, { source: `data:image/png;base64,${screenshot.toString("base64")}`, viewport, box, }, ) }