| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- import { expect, test } from "@playwright/test"
- import {
- defineVisualRegions,
- mapVisualRegions,
- reportVisualStability,
- startVisualProbe,
- stopVisualProbe,
- visualPlan,
- } from "../../utils/visual-stability"
- import {
- assistantMessage,
- completedAssistantInfo,
- messageUpdated,
- partDelta,
- partUpdated,
- reasoningPart,
- setupTimeline,
- shell,
- status,
- textPart,
- userMessage,
- waitForVisualSettle,
- } from "./fixture"
- test.describe("timeline visual lifecycle stability", () => {
- test("streams empty, short, and long parallel shells to staggered completion", async ({ page }, testInfo) => {
- test.setTimeout(180_000)
- const ids = ["prt_parallel_01_empty", "prt_parallel_02_short", "prt_parallel_03_long"] as const
- const initial = ids.map((id) => shell(id, "running"))
- const followingID = "prt_parallel_04_following"
- const assistant = assistantMessage([...initial, textPart(followingID, "Following all parallel shells.")], {
- completed: false,
- })
- const timeline = await setupTimeline(page, {
- messages: [userMessage(), assistant],
- settings: { shellToolPartsExpanded: true, showReasoningSummaries: true },
- cpuRate: 4,
- eventRetry: 24,
- seedHistory: true,
- })
- await timeline.send(status("busy"), 150)
- for (const id of ids) await timeline.waitForPart(id)
- const scroller = page.locator(".scroll-view__viewport", {
- has: page.locator('[data-timeline-row="AssistantPart"]'),
- })
- await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight))
- const regions = defineVisualRegions({
- prt_shell_empty: shellRegion(ids[0]),
- prt_shell_short: shellRegion(ids[1]),
- prt_shell_long: shellRegion(ids[2]),
- following: shellRegion(followingID),
- })
- await waitForVisualSettle(page, [`[data-timeline-part-id="${followingID}"]`])
- await startVisualProbe(page, regions)
- await timeline.sendAll([
- { event: partUpdated(shell(ids[0]!, "completed", "")), delay: 180 },
- { event: partUpdated(shell(ids[2]!, "running", lines(10))), delay: 70 },
- { event: partUpdated(shell(ids[1]!, "running", lines(2))), delay: 110 },
- { event: partUpdated(shell(ids[2]!, "running", lines(25))), delay: 80 },
- { event: partUpdated(shell(ids[1]!, "completed", lines(2))), delay: 260 },
- { event: partUpdated(shell(ids[2]!, "running", lines(50))), delay: 100 },
- { event: partUpdated(shell(ids[2]!, "completed", lines(50))), delay: 450 },
- { event: messageUpdated(completedAssistantInfo(assistant.info)), delay: 100 },
- { event: status("idle"), delay: 700 },
- ])
- const trace = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(
- testInfo,
- "parallel-shells",
- trace,
- visualPlan(
- regions,
- [
- { type: "required", regions: ["prt_shell_empty", "prt_shell_short", "prt_shell_long", "following"] },
- { type: "unique", regions: ["prt_shell_empty", "prt_shell_short", "prt_shell_long"] },
- { type: "stable", regions: ["prt_shell_empty", "prt_shell_short", "prt_shell_long", "following"] },
- { type: "opacity", regions: "all" },
- { type: "continuity", regions: "all" },
- { type: "motion", regions: "all", maxPositionReversals: 0, maxReversals: 4 },
- { type: "label-stability", regions: "all" },
- { type: "preserve-bottom-anchor" },
- { type: "flow", regions: ["prt_shell_empty", "prt_shell_short", "prt_shell_long", "following"] },
- ],
- { perMarker: true },
- ),
- )
- await expect(page.locator(`[data-timeline-part-id="${ids[2]}"] [data-slot="bash-pre"]`)).toContainText("line 50")
- const short = page.locator(`[data-timeline-part-id="${ids[1]}"]`)
- await short.locator('[data-slot="collapsible-trigger"]').click()
- await expect(short.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "false")
- await timeline.send(partUpdated(textPart("prt_late_sibling", "A later sibling rerender.")), 250)
- await expect(short.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "false")
- })
- test("replaces thinking with streamed reasoning and text without a blank visible turn", async ({
- page,
- }, testInfo) => {
- const reasoningID = "prt_reasoning_visible"
- const textID = "prt_streamed_text"
- const assistant = assistantMessage([], { completed: false })
- const timeline = await setupTimeline(page, {
- messages: [userMessage(), assistant],
- settings: { showReasoningSummaries: true },
- cpuRate: 4,
- })
- await timeline.send(status("busy"), 120)
- await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
- const regions = defineVisualRegions({
- thinking: { selector: '[data-timeline-row="Thinking"]' },
- reasoning: {
- selector: `[data-timeline-part-id="${reasoningID}"]`,
- closest: '[data-timeline-row="AssistantPart"]',
- },
- text: { selector: `[data-timeline-part-id="${textID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
- })
- await startVisualProbe(page, regions)
- await timeline.send(partUpdated(reasoningPart(reasoningID, "")), 100)
- await expect(page.locator(`[data-timeline-part-id="${reasoningID}"]`)).toHaveCount(0)
- await timeline.send(partUpdated(reasoningPart(reasoningID, "## Planning\n\nChecking the visible timeline.")), 160)
- await timeline.waitForPart(reasoningID)
- await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
- await timeline.send(partUpdated(textPart(textID, "Starting")), 100)
- await timeline.send(partDelta(textID, " **stable"), 90)
- await timeline.send(partDelta(textID, " output** with `code` and [a link"), 130)
- await timeline.send(partDelta(textID, "](https://example.com)."), 220)
- await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 120)
- await timeline.send(status("idle"), 500)
- const trace = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(
- testInfo,
- "reasoning-text-handoff",
- trace,
- visualPlan(regions, [
- { type: "required", regions: ["reasoning", "text"] },
- { type: "continuous-any", regions: ["thinking", "reasoning", "text"] },
- { type: "unique", regions: ["reasoning", "text"] },
- { type: "stable", regions: ["reasoning", "text"] },
- { type: "opacity", regions: "all" },
- { type: "continuity", regions: "all" },
- { type: "motion", regions: "all", maxReversals: 4 },
- { type: "label-stability", regions: "all" },
- { type: "flow", regions: ["reasoning", "text"] },
- ]),
- )
- await expect(page.locator(`[data-timeline-part-id="${textID}"]`)).toContainText("stable output")
- })
- })
- function lines(count: number) {
- return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
- }
- function shellRegion(id: string) {
- return { selector: `[data-timeline-part-id="${id}"]`, closest: '[data-timeline-row="AssistantPart"]' }
- }
|