Files
Aiden Cline 9e0d3976e1 chore: merge dev into v2 (#35591)
Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Jack <jack@anoma.ly>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: James Long <longster@gmail.com>
Co-authored-by: Dustin Deus <deusdustin@gmail.com>
Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local>
Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: runvip <164729189+runvip@users.noreply.github.com>
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: Julian Coy <julian@ex-machina.co>
Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: Simon Klee <hello@simonklee.dk>
Co-authored-by: Jay <air@live.ca>
Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com>
2026-07-06 16:05:29 -05:00

251 lines
9.8 KiB
TypeScript

import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../../utils/visual-stability"
import {
assistantMessage,
partUpdated,
setupTimeline,
shell,
textPart,
toolPart,
userMessage,
waitForVisualSettle,
type TimelineMessage,
} from "./fixture"
test.describe("timeline adverse visual stability", () => {
test("does not pull a scrolled-away user while an active shell grows", async ({ page }, testInfo) => {
const activeShellID = "prt_adverse_01_shell"
const messages = [
...history(24),
userMessage(),
assistantMessage([shell(activeShellID, "running")], { completed: false }),
]
const timeline = await setupTimeline(page, {
messages,
settings: { shellToolPartsExpanded: true },
cpuRate: 4,
eventRetry: 30,
})
const scroller = page.locator(".scroll-view__viewport", {
has: page.locator('[data-timeline-row="AssistantPart"]'),
})
await scroller.evaluate((element) => {
element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY: -450 }))
element.scrollTop = Math.max(0, element.scrollHeight - element.clientHeight - 450)
})
await page.waitForTimeout(150)
await expect
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
.toBeGreaterThan(100)
const anchor = await scroller.evaluate((element) => {
const view = element.getBoundingClientRect()
return [...element.querySelectorAll<HTMLElement>("[data-timeline-key]")].find((row) => {
const rect = row.getBoundingClientRect()
return rect.top >= view.top + 40 && rect.bottom <= view.bottom - 40
})?.dataset.timelineKey
})
expect(anchor).toBeTruthy()
await waitForVisualSettle(page, [`[data-timeline-key="${anchor}"]`])
const regions = defineVisualRegions({
anchor: { selector: `[data-timeline-key="${anchor}"]` },
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(shell(activeShellID, "running", lines(1))), 180)
await timeline.send(partUpdated(shell(activeShellID, "running", lines(10))), 90)
await timeline.send(partUpdated(shell(activeShellID, "running", lines(50))), 350)
await timeline.send(partUpdated(shell(activeShellID, "completed", lines(50))), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"scrolled-away-shell",
trace,
visualPlan(regions, [
{ type: "required", regions: ["anchor"] },
{ type: "unique", regions: ["anchor"] },
{ type: "stable", regions: ["anchor"] },
{ type: "fixed", regions: ["anchor"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
]),
)
})
test("preserves an explicit shell state across virtualization", async ({ page }) => {
const targetID = "prt_virtual_shell"
const messages = [
userMessage(undefined, { id: "msg_0000_virtual_user", created: 1700000000000 }),
assistantMessage([shell(targetID, "completed", lines(20))], {
id: "msg_0001_virtual_assistant",
parentID: "msg_0000_virtual_user",
created: 1700000001000,
}),
...history(35, 10),
]
await setupTimeline(page, { messages, settings: { shellToolPartsExpanded: false } })
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await scroller.evaluate((element) => {
element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY: -1_000 }))
element.scrollTop = 0
})
await page.waitForTimeout(300)
const trigger = page.locator(`[data-timeline-part-id="${targetID}"] [data-slot="collapsible-trigger"]`)
await expect(trigger).toBeVisible()
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight))
await expect(page.locator(`[data-timeline-part-id="${targetID}"]`)).toHaveCount(0)
await scroller.evaluate((element) => (element.scrollTop = 0))
await expect(trigger).toBeVisible()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
})
test("keeps narrow viewport rows ordered during long shell growth", async ({ page }, testInfo) => {
const shellID = "prt_narrow_01_shell"
const followingID = "prt_narrow_02_following"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage(
[shell(shellID, "running"), textPart(followingID, "A narrow following row that wraps across lines.")],
{
completed: false,
},
),
],
settings: { shellToolPartsExpanded: true },
viewport: { width: 430, height: 800 },
cpuRate: 4,
})
await waitForVisualSettle(page, [
`[data-timeline-part-id="${shellID}"]`,
`[data-timeline-part-id="${followingID}"]`,
])
const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: {
selector: `[data-timeline-part-id="${followingID}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(shell(shellID, "running", wideLines(10))), 100)
await timeline.send(partUpdated(shell(shellID, "running", wideLines(50))), 300)
await timeline.send(partUpdated(shell(shellID, "completed", wideLines(50))), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"narrow-shell",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["shell", "following"] },
{ type: "unique", regions: ["shell", "following"] },
{ type: "stable", regions: ["shell", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["shell", "following"] },
],
{ perMarker: true },
),
)
})
test("keeps visible rows ordered while resizing desktop to narrow and back", async ({ page }, testInfo) => {
const shellID = "prt_resize_01_shell"
const contextIDs = ["prt_resize_02_read", "prt_resize_03_glob"]
const followingID = "prt_resize_04_following"
await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
shell(shellID, "completed", wideLines(15)),
toolPart(contextIDs[0]!, "read", "completed", { filePath: "src/a.ts" }),
toolPart(contextIDs[1]!, "glob", "completed", { path: ".", pattern: "**/*.ts" }),
textPart(followingID, "Following responsive timeline content that wraps on narrow screens."),
]),
],
settings: { shellToolPartsExpanded: true },
cpuRate: 4,
seedHistory: true,
})
const group = `[data-timeline-part-ids="${contextIDs.join(",")}"]`
const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
context: { selector: group, closest: '[data-timeline-row="AssistantPart"]' },
following: {
selector: `[data-timeline-part-id="${followingID}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
})
await startVisualProbe(page, regions)
await page.setViewportSize({ width: 430, height: 800 })
await page.waitForTimeout(500)
await page.setViewportSize({ width: 900, height: 800 })
await page.waitForTimeout(500)
await page.setViewportSize({ width: 1400, height: 900 })
await page.waitForTimeout(500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"responsive-resize",
trace,
visualPlan(regions, [
{ type: "required", regions: ["shell", "context", "following"] },
{ type: "unique", regions: ["shell", "context", "following"] },
{ type: "stable", regions: ["shell", "context", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 4, maxReversals: 4 },
{ type: "label-stability", regions: "all" },
{ type: "flow", regions: ["shell", "context", "following"] },
]),
)
})
})
function history(count: number, offset = 0): TimelineMessage[] {
return Array.from({ length: count }, (_, index) => {
const value = index + offset
const prefix = `msg_0${String(value).padStart(3, "0")}_history`
const userID = `${prefix}_a_user`
return [
userMessage(undefined, { id: userID, created: 1699990000000 + value * 10_000 }),
assistantMessage(
[
textPart(
`prt_history_${String(value).padStart(3, "0")}`,
`Historical response ${value}. ${"Stable history content. ".repeat(8)}`,
),
],
{
id: `${prefix}_b_assistant`,
parentID: userID,
created: 1699990001000 + value * 10_000,
},
),
]
}).flat()
}
function lines(count: number) {
return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
}
function wideLines(count: number) {
return Array.from({ length: count }, (_, index) => `line ${index + 1} ${"wide-output-".repeat(20)}`).join("\n")
}