| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- import { expect, test } from "@playwright/test"
- import {
- defineVisualRegions,
- reportVisualStability,
- startVisualProbe,
- stopVisualProbe,
- visualPlan,
- } from "../../utils/visual-stability"
- import { assistantMessage, setupTimeline, shell, textPart, toolPart, userMessage, waitForVisualSettle } from "./fixture"
- test("expands and collapses a long completed shell without overlap", async ({ page }, testInfo) => {
- const shellID = "prt_interaction_01_shell"
- const followingID = "prt_interaction_02_following"
- await setupTimeline(page, {
- messages: [
- userMessage(),
- assistantMessage([shell(shellID, "completed", lines(50)), textPart(followingID, "Following shell expansion")]),
- ],
- settings: { shellToolPartsExpanded: false },
- cpuRate: 4,
- seedHistory: true,
- })
- const trigger = page.locator(`[data-timeline-part-id="${shellID}"] [data-slot="collapsible-trigger"]`)
- 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"]' },
- })
- const plan = 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"] },
- ])
- await startVisualProbe(page, regions)
- await trigger.click()
- await expect(trigger).toHaveAttribute("aria-expanded", "true")
- await page.waitForTimeout(500)
- const expanded = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(testInfo, "shell-expand", expanded, plan)
- await startVisualProbe(page, regions)
- await trigger.click()
- await expect(trigger).toHaveAttribute("aria-expanded", "false")
- await page.waitForTimeout(500)
- const collapsed = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(testInfo, "shell-collapse", collapsed, plan)
- })
- test("expands and collapses a completed context group without overlap", async ({ page }, testInfo) => {
- const ids = [
- "prt_interaction_01_read",
- "prt_interaction_02_glob",
- "prt_interaction_03_grep",
- "prt_interaction_04_list",
- ]
- const group = `[data-timeline-part-ids="${ids.join(",")}"]`
- const followingID = "prt_interaction_context_following"
- await setupTimeline(page, {
- messages: [
- userMessage(),
- assistantMessage([
- toolPart(ids[0]!, "read", "completed", { filePath: "src/a.ts" }),
- toolPart(ids[1]!, "glob", "completed", { path: ".", pattern: "**/*.ts" }),
- toolPart(ids[2]!, "grep", "completed", { path: ".", pattern: "stable" }),
- toolPart(ids[3]!, "list", "completed", { path: "src" }),
- textPart(followingID, "Following context expansion"),
- ]),
- ],
- cpuRate: 4,
- seedHistory: true,
- })
- const trigger = page.locator(`${group} [data-slot="collapsible-trigger"]`)
- await waitForVisualSettle(page, [group, `[data-timeline-part-id="${followingID}"]`])
- for (const [name, expanded] of [
- ["context-expand", true],
- ["context-collapse", false],
- ["context-reexpand", true],
- ] as const) {
- const regions = defineVisualRegions({
- 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 trigger.click()
- await expect(trigger).toHaveAttribute("aria-expanded", String(expanded))
- await page.waitForTimeout(500)
- const trace = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(
- testInfo,
- name,
- trace,
- visualPlan(regions, [
- { type: "required", regions: ["context", "following"] },
- { type: "unique", regions: ["context", "following"] },
- { type: "stable", regions: ["context", "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: ["context", "following"] },
- ]),
- )
- }
- })
- test("expands and collapses an edit diff without moving twice", async ({ page }, testInfo) => {
- const editID = "prt_interaction_edit"
- const followingID = "prt_interaction_edit_following"
- await setupTimeline(page, {
- messages: [
- userMessage(),
- assistantMessage([
- toolPart(
- editID,
- "edit",
- "completed",
- { filePath: "src/edit.ts" },
- {
- metadata: {
- filediff: {
- file: "src/edit.ts",
- additions: 40,
- deletions: 40,
- before: source(40, false),
- after: source(40, true),
- },
- },
- },
- ),
- textPart(followingID, "Following edit expansion"),
- ]),
- ],
- settings: { editToolPartsExpanded: false },
- cpuRate: 4,
- seedHistory: true,
- })
- const trigger = page.locator(`[data-timeline-part-id="${editID}"] [data-slot="collapsible-trigger"]`).first()
- await waitForVisualSettle(page, [`[data-timeline-part-id="${editID}"]`, `[data-timeline-part-id="${followingID}"]`])
- const regions = defineVisualRegions({
- edit: { selector: `[data-timeline-part-id="${editID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
- following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
- })
- await startVisualProbe(page, regions)
- await trigger.click()
- await expect(trigger).toHaveAttribute("aria-expanded", "true")
- await page.waitForTimeout(900)
- const trace = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(
- testInfo,
- "edit-expand",
- trace,
- visualPlan(regions, [
- { type: "required", regions: ["edit", "following"] },
- { type: "unique", regions: ["edit", "following"] },
- { type: "stable", regions: ["edit", "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: ["edit", "following"] },
- ]),
- )
- })
- test("shows all and expands historical diff summary without overlap", async ({ page }, testInfo) => {
- const firstUser = userMessage(undefined, {
- summary: {
- diffs: Array.from({ length: 12 }, (_, index) => ({
- file: `src/diff-${index}.ts`,
- additions: 1,
- deletions: 1,
- patch: `@@ -1 +1 @@\n-export const value = ${index}\n+export const value = ${index + 1}`,
- })),
- },
- })
- const nextUserID = "msg_2000_diff_interaction_user"
- await setupTimeline(page, {
- messages: [
- firstUser,
- assistantMessage(),
- userMessage(undefined, { id: nextUserID, created: 1700000010000 }),
- assistantMessage([], {
- id: "msg_2001_diff_interaction_assistant",
- parentID: nextUserID,
- created: 1700000011000,
- }),
- ],
- cpuRate: 4,
- })
- const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
- await scroller.evaluate((element) => (element.scrollTop = 0))
- const diff = page.locator('[data-timeline-row="DiffSummary"]')
- const following = page.locator(`[data-message-id="${nextUserID}"]`).first()
- await expect(diff).toBeVisible()
- const regions = defineVisualRegions({
- diff: { selector: '[data-timeline-row="DiffSummary"]' },
- following: { selector: `[data-message-id="${nextUserID}"]` },
- })
- await startVisualProbe(page, regions)
- await page.getByText(/show all/i).click()
- await page.waitForTimeout(500)
- await diff.locator('[data-slot="session-turn-diff-trigger"]').first().click()
- await page.waitForTimeout(900)
- const trace = await stopVisualProbe<keyof typeof regions>(page)
- await reportVisualStability(
- testInfo,
- "diff-summary-expand",
- trace,
- visualPlan(regions, [
- { type: "required", regions: ["diff", "following"] },
- { type: "unique", regions: ["diff", "following"] },
- { type: "stable", regions: ["diff", "following"] },
- { type: "opacity", regions: "all" },
- { type: "continuity", regions: "all" },
- { type: "motion", regions: "all", maxPositionReversals: 1, maxReversals: 2 },
- { type: "label-stability", regions: "all" },
- { type: "flow", regions: ["diff", "following"] },
- ]),
- )
- })
- function lines(count: number) {
- return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
- }
- function source(count: number, changed: boolean) {
- return Array.from(
- { length: count },
- (_, index) => `export const value${index} = ${changed ? index + 1 : index}\n`,
- ).join("")
- }
|