Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Co-authored-by: Julian Coy <julian@ex-machina.co> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: Frank <frank@anoma.ly> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: Dustin Deus <deusdustin@gmail.com> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: Jay <air@live.ca> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: James Long <jlongster@users.noreply.github.com> Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com> Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com> Co-authored-by: Victor Navarro <vn4varro@gmail.com>
228 lines
9.4 KiB
TypeScript
228 lines
9.4 KiB
TypeScript
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<HTMLElement>('[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<HTMLElement>('[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<HTMLElement>('[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<HTMLElement>("[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,
|
|
},
|
|
)
|
|
}
|