| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255 |
- import { test } from "@playwright/test"
- import {
- defineVisualRegions,
- reportVisualStability,
- startVisualProbe,
- stopVisualProbe,
- visualPlan,
- } from "../../utils/visual-stability"
- import {
- assistantMessage,
- partUpdated,
- setupTimeline,
- shell,
- textPart,
- userMessage,
- waitForVisualSettle,
- } from "./fixture"
- const profiles = [
- {
- name: "empty running to completed",
- updates: [{ state: "completed" as const, output: "", delay: 350 }],
- },
- {
- name: "50 lines arriving incrementally",
- updates: [
- { state: "running" as const, output: lines(1), delay: 100 },
- { state: "running" as const, output: lines(10), delay: 160 },
- { state: "running" as const, output: lines(25), delay: 90 },
- { state: "running" as const, output: lines(50), delay: 220 },
- { state: "completed" as const, output: lines(50), delay: 500 },
- ],
- },
- {
- name: "wide ANSI and CRLF output",
- updates: [
- {
- state: "running" as const,
- output: Array.from({ length: 20 }, (_, index) => `\u001b[32mline ${index}\u001b[0m ${"wide-".repeat(30)}`).join(
- "\r\n",
- ),
- delay: 240,
- },
- {
- state: "completed" as const,
- output: Array.from({ length: 20 }, (_, index) => `line ${index} ${"wide-".repeat(30)}`).join("\n"),
- delay: 500,
- },
- ],
- },
- ] as const
- for (const profile of profiles) {
- test(`keeps rows stable for shell ${profile.name}`, async ({ page }, testInfo) => {
- const shellID = `prt_matrix_${profiles.indexOf(profile)}_01_shell`
- const followingID = `prt_matrix_${profiles.indexOf(profile)}_02_following`
- const timeline = await setupTimeline(page, {
- messages: [
- userMessage(),
- assistantMessage([shell(shellID, "running"), textPart(followingID, "Following shell row")], {
- completed: false,
- }),
- ],
- settings: { shellToolPartsExpanded: true },
- cpuRate: 4,
- seedHistory: true,
- })
- const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
- await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight))
- 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)
- for (const update of profile.updates) {
- await timeline.send(partUpdated(shell(shellID, update.state, update.output)), update.delay)
- }
- const trace = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(
- testInfo,
- `shell-${profiles.indexOf(profile)}`,
- 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, maxReversals: 1 },
- { type: "label-stability", regions: "all" },
- { type: "preserve-bottom-anchor" },
- { type: "flow", regions: ["shell", "following"] },
- ],
- { perMarker: true },
- ),
- )
- })
- }
- test("keeps following row stable when a collapsed shell receives 50 lines", async ({ page }, testInfo) => {
- const shellID = "prt_matrix_collapsed_01_shell"
- const followingID = "prt_matrix_collapsed_02_following"
- const timeline = await setupTimeline(page, {
- messages: [
- userMessage(),
- assistantMessage([shell(shellID, "running"), textPart(followingID, "Following collapsed shell")], {
- completed: false,
- }),
- ],
- settings: { shellToolPartsExpanded: false },
- cpuRate: 4,
- seedHistory: true,
- })
- 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", lines(50))), 240)
- await timeline.send(partUpdated(shell(shellID, "completed", lines(50))), 500)
- const trace = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(
- testInfo,
- "collapsed-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: ["following"], maxPositionReversals: 0 },
- { type: "label-stability", regions: "all" },
- { type: "flow", regions: ["shell", "following"] },
- ],
- { perMarker: true },
- ),
- )
- })
- test("keeps rows stable when a running shell becomes an error", async ({ page }, testInfo) => {
- const shellID = "prt_matrix_error_01_shell"
- const followingID = "prt_matrix_error_02_following"
- const timeline = await setupTimeline(page, {
- messages: [
- userMessage(),
- assistantMessage([shell(shellID, "running", lines(10)), textPart(followingID, "Following failed shell")], {
- completed: false,
- }),
- ],
- settings: { shellToolPartsExpanded: true },
- cpuRate: 4,
- seedHistory: true,
- })
- 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, "error"),
- state: {
- status: "error",
- input: { command: `echo ${shellID}` },
- error: "Command failed after output",
- metadata: {},
- time: { start: 1700000001000, end: 1700000002000 },
- },
- }),
- 500,
- )
- const trace = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(
- testInfo,
- "shell-error",
- 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: ["following"], maxPositionReversals: 0 },
- { type: "label-stability", regions: "all" },
- { type: "preserve-bottom-anchor" },
- { type: "flow", regions: ["shell", "following"] },
- ],
- { perMarker: true },
- ),
- )
- })
- test("keeps rows stable when later text arrives before shell output", async ({ page }, testInfo) => {
- const shellID = "prt_late_text_01_shell"
- const followingID = "prt_late_text_02_following"
- const timeline = await setupTimeline(page, {
- messages: [userMessage(), assistantMessage([shell(shellID, "running")], { completed: false })],
- settings: { shellToolPartsExpanded: true },
- cpuRate: 4,
- seedHistory: true,
- })
- await waitForVisualSettle(page, [`[data-timeline-part-id="${shellID}"]`])
- 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(textPart(followingID, "Later assistant content arrived before shell output.")), 240)
- await timeline.send(partUpdated(shell(shellID, "running", lines(20))), 300)
- await timeline.send(partUpdated(shell(shellID, "completed", lines(20))), 600)
- const trace = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(
- testInfo,
- "late-text-before-shell-output",
- trace,
- visualPlan(
- regions,
- [
- { type: "required", regions: ["shell", "following"] },
- { type: "unique", regions: ["shell", "following"] },
- { type: "stable", regions: ["shell"] },
- { 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 },
- ),
- )
- })
- function lines(count: number) {
- return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
- }
|