mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-23 18:55:37 -04:00
9e0d3976e1
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>
251 lines
9.8 KiB
TypeScript
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")
|
|
}
|