import "../../index.css"; import { MessageId, type TurnId } from "@t3tools/contracts";import { page } from "vitest/browser";import { useCallback, useState, type ComponentProps } from "react";import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";import { render } from "vitest-browser-react"; import { deriveTimelineEntries, type WorkLogEntry } from "../../session-logic";import { type ChatMessage, type ProposedPlan, type TurnDiffSummary } from "../../types";import { MessagesTimeline } from "./MessagesTimeline";import { deriveMessagesTimelineRows, estimateMessagesTimelineRowHeight,} from "./MessagesTimeline.logic"; const DEFAULT_VIEWPORT = { width: 960, height: 1_100,};const MARKDOWN_CWD = "/repo/project"; interface RowMeasurement { actualHeightPx: number; estimatedHeightPx: number; timelineWidthPx: number; virtualizerSizePx: number; renderedInVirtualizedRegion: boolean;} interface VirtualizationScenario { name: string; targetRowId: string; props: Omit<ComponentProps<typeof MessagesTimeline>, "scrollContainer">; maxEstimateDeltaPx: number;} interface VirtualizerSnapshot { totalSize: number; measurements: ReadonlyArray<{ id: string; kind: string; index: number; size: number; start: number; end: number; }>;} function MessagesTimelineBrowserHarness( props: Omit<ComponentProps<typeof MessagesTimeline>, "scrollContainer">,) { const [scrollContainer, setScrollContainer] = useState<HTMLDivElement | null>(null); const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>( () => props.expandedWorkGroups, ); const handleToggleWorkGroup = useCallback( (groupId: string) => { setExpandedWorkGroups((current) => ({ ...current, [groupId]: !(current[groupId] ?? false), })); props.onToggleWorkGroup(groupId); }, [props], ); return ( <div ref={setScrollContainer} data-testid="messages-timeline-scroll-container" className="h-full overflow-y-auto overscroll-y-contain" > <MessagesTimeline {...props} scrollContainer={scrollContainer} expandedWorkGroups={expandedWorkGroups} onToggleWorkGroup={handleToggleWorkGroup} /> </div> );} function isoAt(offsetSeconds: number): string { return new Date(Date.UTC(2026, 2, 17, 19, 12, 28) + offsetSeconds * 1_000).toISOString();} function createMessage(input: { id: string; role: ChatMessage["role"]; text: string; offsetSeconds: number; attachments?: ChatMessage["attachments"];}): ChatMessage { return { id: MessageId.makeUnsafe(input.id), role: input.role, text: input.text, ...(input.attachments ? { attachments: input.attachments } : {}), createdAt: isoAt(input.offsetSeconds), ...(input.role === "assistant" ? { completedAt: isoAt(input.offsetSeconds + 1) } : {}), streaming: false, };} function createToolWorkEntry(input: { id: string; offsetSeconds: number; label?: string; detail?: string;}): WorkLogEntry { return { id: input.id, createdAt: isoAt(input.offsetSeconds), label: input.label ?? "exec_command completed", ...(input.detail ? { detail: input.detail } : {}), tone: "tool", toolTitle: "exec_command", };} function createPlan(input: { id: string; offsetSeconds: number; planMarkdown: string;}): ProposedPlan { return { id: input.id as ProposedPlan["id"], turnId: null, planMarkdown: input.planMarkdown, implementedAt: null, implementationThreadId: null, createdAt: isoAt(input.offsetSeconds), updatedAt: isoAt(input.offsetSeconds + 1), };} function createBaseTimelineProps(input: { messages?: ChatMessage[]; proposedPlans?: ProposedPlan[]; workEntries?: WorkLogEntry[]; expandedWorkGroups?: Record<string, boolean>; completionDividerBeforeEntryId?: string | null; turnDiffSummaryByAssistantMessageId?: Map<MessageId, TurnDiffSummary>; onVirtualizerSnapshot?: ComponentProps<typeof MessagesTimeline>["onVirtualizerSnapshot"];}): Omit<ComponentProps<typeof MessagesTimeline>, "scrollContainer"> { return { hasMessages: true, isWorking: false, activeTurnInProgress: false, activeTurnStartedAt: null, timelineEntries: deriveTimelineEntries( input.messages ?? [], input.proposedPlans ?? [], input.workEntries ?? [], ), completionDividerBeforeEntryId: input.completionDividerBeforeEntryId ?? null, completionSummary: null, turnDiffSummaryByAssistantMessageId: input.turnDiffSummaryByAssistantMessageId ?? new Map(), nowIso: isoAt(10_000), expandedWorkGroups: input.expandedWorkGroups ?? {}, onToggleWorkGroup: () => {}, onOpenTurnDiff: () => {}, revertTurnCountByUserMessageId: new Map(), onRevertUserMessage: () => {}, isRevertingCheckpoint: false, onImageExpand: () => {}, markdownCwd: MARKDOWN_CWD, resolvedTheme: "light", timestampFormat: "locale", workspaceRoot: MARKDOWN_CWD, ...(input.onVirtualizerSnapshot ? { onVirtualizerSnapshot: input.onVirtualizerSnapshot } : {}), };} function createFillerMessages(input: { prefix: string; startOffsetSeconds: number; pairCount: number;}): ChatMessage[] { const messages: ChatMessage[] = []; for (let index = 0; index < input.pairCount; index += 1) { const baseOffset = input.startOffsetSeconds + index * 4; messages.push( createMessage({ id: `${input.prefix}-user-${index}`, role: "user", text: `filler user message ${index}`, offsetSeconds: baseOffset, }), ); messages.push( createMessage({ id: `${input.prefix}-assistant-${index}`, role: "assistant", text: `filler assistant message ${index}`, offsetSeconds: baseOffset + 1, }), ); } return messages;} function createChangedFilesSummary( targetMessageId: MessageId, files: TurnDiffSummary["files"],): Map<MessageId, TurnDiffSummary> { return new Map([ [ targetMessageId, { turnId: "turn-changed-files" as TurnId, completedAt: isoAt(10), assistantMessageId: targetMessageId, files, }, ], ]);} function createChangedFilesScenario(input: { name: string; rowId: string; files: TurnDiffSummary["files"]; maxEstimateDeltaPx?: number;}): VirtualizationScenario { const beforeMessages = createFillerMessages({ prefix: `${input.rowId}-before`, startOffsetSeconds: 0, pairCount: 2, }); const afterMessages = createFillerMessages({ prefix: `${input.rowId}-after`, startOffsetSeconds: 40, pairCount: 8, }); const changedFilesMessage = createMessage({ id: input.rowId, role: "assistant", text: "Validation passed on the merged tree.", offsetSeconds: 12, }); return { name: input.name, targetRowId: changedFilesMessage.id, props: createBaseTimelineProps({ messages: [...beforeMessages, changedFilesMessage, ...afterMessages], turnDiffSummaryByAssistantMessageId: createChangedFilesSummary( changedFilesMessage.id, input.files, ), }), maxEstimateDeltaPx: input.maxEstimateDeltaPx ?? 72, };} function createAssistantMessageScenario(input: { name: string; rowId: string; text: string; maxEstimateDeltaPx?: number;}): VirtualizationScenario { const beforeMessages = createFillerMessages({ prefix: `${input.rowId}-before`, startOffsetSeconds: 0, pairCount: 2, }); const afterMessages = createFillerMessages({ prefix: `${input.rowId}-after`, startOffsetSeconds: 40, pairCount: 8, }); const assistantMessage = createMessage({ id: input.rowId, role: "assistant", text: input.text, offsetSeconds: 12, }); return { name: input.name, targetRowId: assistantMessage.id, props: createBaseTimelineProps({ messages: [...beforeMessages, assistantMessage, ...afterMessages], }), maxEstimateDeltaPx: input.maxEstimateDeltaPx ?? 16, };} function buildStaticScenarios(): VirtualizationScenario[] { const beforeMessages = createFillerMessages({ prefix: "before", startOffsetSeconds: 0, pairCount: 2, }); const afterMessages = createFillerMessages({ prefix: "after", startOffsetSeconds: 40, pairCount: 8, }); const longUserMessage = createMessage({ id: "target-user-long", role: "user", text: "x".repeat(3_200), offsetSeconds: 12, }); const workEntries = Array.from({ length: 4 }, (_, index) => createToolWorkEntry({ id: `target-work-${index}`, offsetSeconds: 12 + index, detail: `tool output line ${index + 1}`, }), ); const moderatePlan = createPlan({ id: "target-plan", offsetSeconds: 12, planMarkdown: [ "# Stabilize virtualization", "", "- Gather baseline measurements", "- Add browser harness coverage", "- Compare estimated and rendered heights", "- Fix the broken rows without broad refactors", "- Re-run lint and typecheck", ].join("\n"), }); return [ { name: "long user message", targetRowId: longUserMessage.id, props: createBaseTimelineProps({ messages: [...beforeMessages, longUserMessage, ...afterMessages], }), maxEstimateDeltaPx: 56, }, { name: "grouped work log row", targetRowId: workEntries[0]!.id, props: createBaseTimelineProps({ messages: [...beforeMessages, ...afterMessages], workEntries, }), maxEstimateDeltaPx: 56, }, { name: "expanded grouped work log row with show more enabled", targetRowId: "target-work-expanded-0", props: createBaseTimelineProps({ messages: [...beforeMessages, ...afterMessages], workEntries: Array.from({ length: 10 }, (_, index) => createToolWorkEntry({ id: `target-work-expanded-${index}`, offsetSeconds: 12 + index, detail: `tool output line ${index + 1}`, }), ), expandedWorkGroups: { "target-work-expanded-0": true, }, }), maxEstimateDeltaPx: 72, }, { name: "proposed plan row", targetRowId: moderatePlan.id, props: createBaseTimelineProps({ messages: [...beforeMessages, ...afterMessages], proposedPlans: [moderatePlan], }), maxEstimateDeltaPx: 96, }, createAssistantMessageScenario({ name: "assistant single-paragraph row with plain prose", rowId: "target-assistant-plain-prose", text: [ "The host is still expanding to content somewhere in the grid layout.", "I'm stripping it back further to a plain block container so the test width", "is actually the timeline width.", ].join(" "), }), createAssistantMessageScenario({ name: "assistant single-paragraph row with inline code", rowId: "target-assistant-inline-code", text: [ "Typecheck found one exact-optional-property issue in the browser harness:", "I was always passing `onVirtualizerSnapshot`, including `undefined`.", "I'm tightening that object construction and rerunning the checks.", ].join(" "), maxEstimateDeltaPx: 28, }), createChangedFilesScenario({ name: "assistant changed-files row with a compacted single-chain directory", rowId: "target-assistant-changed-files-single-chain", files: [ { path: "apps/web/src/components/chat/ChangedFilesTree.tsx", additions: 37, deletions: 45 }, { path: "apps/web/src/components/chat/ChangedFilesTree.test.tsx", additions: 0, deletions: 26, }, ], }), createChangedFilesScenario({ name: "assistant changed-files row with a branch after compaction", rowId: "target-assistant-changed-files-branch-point", files: [ { path: "apps/server/src/git/Layers/GitCore.ts", additions: 44, deletions: 38 }, { path: "apps/server/src/git/Layers/GitCore.test.ts", additions: 18, deletions: 9 }, { path: "apps/server/src/provider/Layers/CodexAdapter.ts", additions: 27, deletions: 8, }, { path: "apps/server/src/provider/Layers/CodexAdapter.test.ts", additions: 36, deletions: 0, }, ], }), createChangedFilesScenario({ name: "assistant changed-files row with mixed root and nested entries", rowId: "target-assistant-changed-files-mixed-root", files: [ { path: "README.md", additions: 5, deletions: 1 }, { path: "packages/contracts/src/orchestration.ts", additions: 13, deletions: 3 }, { path: "packages/shared/src/git.ts", additions: 8, deletions: 2 }, ], }), ];} async function nextFrame(): Promise<void> { await new Promise<void>((resolve) => { window.requestAnimationFrame(() => resolve()); });} async function waitForLayout(): Promise<void> { await nextFrame(); await nextFrame(); await nextFrame();} async function setViewport(viewport: { width: number; height: number }): Promise<void> { await page.viewport(viewport.width, viewport.height); await waitForLayout();} async function waitForProductionStyles(): Promise<void> { await vi.waitFor( () => { expect( getComputedStyle(document.documentElement).getPropertyValue("--background").trim(), ).not.toBe(""); expect(getComputedStyle(document.body).marginTop).toBe("0px"); }, { timeout: 4_000, interval: 16 }, );} async function waitForElement<T extends Element>( query: () => T | null, errorMessage: string,): Promise<T> { let element: T | null = null; await vi.waitFor( () => { element = query(); expect(element, errorMessage).toBeTruthy(); }, { timeout: 8_000, interval: 16 }, ); if (!element) { throw new Error(errorMessage); } return element;} async function measureTimelineRow(input: { host: HTMLElement; props: Omit<ComponentProps<typeof MessagesTimeline>, "scrollContainer">; targetRowId: string;}): Promise<RowMeasurement> { const scrollContainer = await waitForElement( () => input.host.querySelector<HTMLDivElement>( '[data-testid="messages-timeline-scroll-container"]', ), "Unable to find MessagesTimeline scroll container.", ); const rowSelector = `[data-timeline-row-id="${input.targetRowId}"]`; const virtualRowSelector = `[data-virtual-row-id="${input.targetRowId}"]`; let timelineWidthPx = 0; let actualHeightPx = 0; let virtualizerSizePx = 0;Lines 1–500 of 1037. Load more
import "../../index.css"; import { MessageId, type TurnId } from "@t3tools/contracts"; import { page } from "vitest/browser"; import { useCallback, useState, type ComponentProps } from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { render } from "vitest-browser-react"; import { deriveTimelineEntries, type WorkLogEntry } from "../../session-logic"; import { type ChatMessage, type ProposedPlan, type TurnDiffSummary } from "../../types"; import { MessagesTimeline } from "./MessagesTimeline"; import { deriveMessagesTimelineRows, estimateMessagesTimelineRowHeight, } from "./MessagesTimeline.logic"; const DEFAULT_VIEWPORT = { width: 960, height: 1_100, }; const MARKDOWN_CWD = "/repo/project"; interface RowMeasurement { actualHeightPx: number; estimatedHeightPx: number; timelineWidthPx: number; virtualizerSizePx: number; renderedInVirtualizedRegion: boolean; } interface VirtualizationScenario { name: string; targetRowId: string; props: Omit<ComponentProps<typeof MessagesTimeline>, "scrollContainer">; maxEstimateDeltaPx: number; } interface VirtualizerSnapshot { totalSize: number; measurements: ReadonlyArray<{ id: string; kind: string; index: number; size: number; start: number; end: number; }>; } function MessagesTimelineBrowserHarness( props: Omit<ComponentProps<typeof MessagesTimeline>, "scrollContainer">, ) { const [scrollContainer, setScrollContainer] = useState<HTMLDivElement | null>(null); const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>( () => props.expandedWorkGroups, ); const handleToggleWorkGroup = useCallback( (groupId: string) => { setExpandedWorkGroups((current) => ({ ...current, [groupId]: !(current[groupId] ?? false), })); props.onToggleWorkGroup(groupId); }, [props], ); return ( <div ref={setScrollContainer} data-testid="messages-timeline-scroll-container" className="h-full overflow-y-auto overscroll-y-contain" > <MessagesTimeline {...props} scrollContainer={scrollContainer} expandedWorkGroups={expandedWorkGroups} onToggleWorkGroup={handleToggleWorkGroup} /> </div> ); } function isoAt(offsetSeconds: number): string { return new Date(Date.UTC(2026, 2, 17, 19, 12, 28) + offsetSeconds * 1_000).toISOString(); } function createMessage(input: { id: string; role: ChatMessage["role"]; text: string; offsetSeconds: number; attachments?: ChatMessage["attachments"]; }): ChatMessage { return { id: MessageId.makeUnsafe(input.id), role: input.role, text: input.text, ...(input.attachments ? { attachments: input.attachments } : {}), createdAt: isoAt(input.offsetSeconds), ...(input.role === "assistant" ? { completedAt: isoAt(input.offsetSeconds + 1) } : {}), streaming: false, }; } function createToolWorkEntry(input: { id: string; offsetSeconds: number; label?: string; detail?: string; }): WorkLogEntry { return { id: input.id, createdAt: isoAt(input.offsetSeconds), label: input.label ?? "exec_command completed", ...(input.detail ? { detail: input.detail } : {}), tone: "tool", toolTitle: "exec_command", }; } function createPlan(input: { id: string; offsetSeconds: number; planMarkdown: string; }): ProposedPlan { return { id: input.id as ProposedPlan["id"], turnId: null, planMarkdown: input.planMarkdown, implementedAt: null, implementationThreadId: null, createdAt: isoAt(input.offsetSeconds), updatedAt: isoAt(input.offsetSeconds + 1), }; } function createBaseTimelineProps(input: { messages?: ChatMessage[]; proposedPlans?: ProposedPlan[]; workEntries?: WorkLogEntry[]; expandedWorkGroups?: Record<string, boolean>; completionDividerBeforeEntryId?: string | null; turnDiffSummaryByAssistantMessageId?: Map<MessageId, TurnDiffSummary>; onVirtualizerSnapshot?: ComponentProps<typeof MessagesTimeline>["onVirtualizerSnapshot"]; }): Omit<ComponentProps<typeof MessagesTimeline>, "scrollContainer"> { return { hasMessages: true, isWorking: false, activeTurnInProgress: false, activeTurnStartedAt: null, timelineEntries: deriveTimelineEntries( input.messages ?? [], input.proposedPlans ?? [], input.workEntries ?? [], ), completionDividerBeforeEntryId: input.completionDividerBeforeEntryId ?? null, completionSummary: null, turnDiffSummaryByAssistantMessageId: input.turnDiffSummaryByAssistantMessageId ?? new Map(), nowIso: isoAt(10_000), expandedWorkGroups: input.expandedWorkGroups ?? {}, onToggleWorkGroup: () => {}, onOpenTurnDiff: () => {}, revertTurnCountByUserMessageId: new Map(), onRevertUserMessage: () => {}, isRevertingCheckpoint: false, onImageExpand: () => {}, markdownCwd: MARKDOWN_CWD, resolvedTheme: "light", timestampFormat: "locale", workspaceRoot: MARKDOWN_CWD, ...(input.onVirtualizerSnapshot ? { onVirtualizerSnapshot: input.onVirtualizerSnapshot } : {}), }; } function createFillerMessages(input: { prefix: string; startOffsetSeconds: number; pairCount: number; }): ChatMessage[] { const messages: ChatMessage[] = []; for (let index = 0; index < input.pairCount; index += 1) { const baseOffset = input.startOffsetSeconds + index * 4; messages.push( createMessage({ id: `${input.prefix}-user-${index}`, role: "user", text: `filler user message ${index}`, offsetSeconds: baseOffset, }), ); messages.push( createMessage({ id: `${input.prefix}-assistant-${index}`, role: "assistant", text: `filler assistant message ${index}`, offsetSeconds: baseOffset + 1, }), ); } return messages; } function createChangedFilesSummary( targetMessageId: MessageId, files: TurnDiffSummary["files"], ): Map<MessageId, TurnDiffSummary> { return new Map([ [ targetMessageId, { turnId: "turn-changed-files" as TurnId, completedAt: isoAt(10), assistantMessageId: targetMessageId, files, }, ], ]); } function createChangedFilesScenario(input: { name: string; rowId: string; files: TurnDiffSummary["files"]; maxEstimateDeltaPx?: number; }): VirtualizationScenario { const beforeMessages = createFillerMessages({ prefix: `${input.rowId}-before`, startOffsetSeconds: 0, pairCount: 2, }); const afterMessages = createFillerMessages({ prefix: `${input.rowId}-after`, startOffsetSeconds: 40, pairCount: 8, }); const changedFilesMessage = createMessage({ id: input.rowId, role: "assistant", text: "Validation passed on the merged tree.", offsetSeconds: 12, }); return { name: input.name, targetRowId: changedFilesMessage.id, props: createBaseTimelineProps({ messages: [...beforeMessages, changedFilesMessage, ...afterMessages], turnDiffSummaryByAssistantMessageId: createChangedFilesSummary( changedFilesMessage.id, input.files, ), }), maxEstimateDeltaPx: input.maxEstimateDeltaPx ?? 72, }; } function createAssistantMessageScenario(input: { name: string; rowId: string; text: string; maxEstimateDeltaPx?: number; }): VirtualizationScenario { const beforeMessages = createFillerMessages({ prefix: `${input.rowId}-before`, startOffsetSeconds: 0, pairCount: 2, }); const afterMessages = createFillerMessages({ prefix: `${input.rowId}-after`, startOffsetSeconds: 40, pairCount: 8, }); const assistantMessage = createMessage({ id: input.rowId, role: "assistant", text: input.text, offsetSeconds: 12, }); return { name: input.name, targetRowId: assistantMessage.id, props: createBaseTimelineProps({ messages: [...beforeMessages, assistantMessage, ...afterMessages], }), maxEstimateDeltaPx: input.maxEstimateDeltaPx ?? 16, }; } function buildStaticScenarios(): VirtualizationScenario[] { const beforeMessages = createFillerMessages({ prefix: "before", startOffsetSeconds: 0, pairCount: 2, }); const afterMessages = createFillerMessages({ prefix: "after", startOffsetSeconds: 40, pairCount: 8, }); const longUserMessage = createMessage({ id: "target-user-long", role: "user", text: "x".repeat(3_200), offsetSeconds: 12, }); const workEntries = Array.from({ length: 4 }, (_, index) => createToolWorkEntry({ id: `target-work-${index}`, offsetSeconds: 12 + index, detail: `tool output line ${index + 1}`, }), ); const moderatePlan = createPlan({ id: "target-plan", offsetSeconds: 12, planMarkdown: [ "# Stabilize virtualization", "", "- Gather baseline measurements", "- Add browser harness coverage", "- Compare estimated and rendered heights", "- Fix the broken rows without broad refactors", "- Re-run lint and typecheck", ].join("\n"), }); return [ { name: "long user message", targetRowId: longUserMessage.id, props: createBaseTimelineProps({ messages: [...beforeMessages, longUserMessage, ...afterMessages], }), maxEstimateDeltaPx: 56, }, { name: "grouped work log row", targetRowId: workEntries[0]!.id, props: createBaseTimelineProps({ messages: [...beforeMessages, ...afterMessages], workEntries, }), maxEstimateDeltaPx: 56, }, { name: "expanded grouped work log row with show more enabled", targetRowId: "target-work-expanded-0", props: createBaseTimelineProps({ messages: [...beforeMessages, ...afterMessages], workEntries: Array.from({ length: 10 }, (_, index) => createToolWorkEntry({ id: `target-work-expanded-${index}`, offsetSeconds: 12 + index, detail: `tool output line ${index + 1}`, }), ), expandedWorkGroups: { "target-work-expanded-0": true, }, }), maxEstimateDeltaPx: 72, }, { name: "proposed plan row", targetRowId: moderatePlan.id, props: createBaseTimelineProps({ messages: [...beforeMessages, ...afterMessages], proposedPlans: [moderatePlan], }), maxEstimateDeltaPx: 96, }, createAssistantMessageScenario({ name: "assistant single-paragraph row with plain prose", rowId: "target-assistant-plain-prose", text: [ "The host is still expanding to content somewhere in the grid layout.", "I'm stripping it back further to a plain block container so the test width", "is actually the timeline width.", ].join(" "), }), createAssistantMessageScenario({ name: "assistant single-paragraph row with inline code", rowId: "target-assistant-inline-code", text: [ "Typecheck found one exact-optional-property issue in the browser harness:", "I was always passing `onVirtualizerSnapshot`, including `undefined`.", "I'm tightening that object construction and rerunning the checks.", ].join(" "), maxEstimateDeltaPx: 28, }), createChangedFilesScenario({ name: "assistant changed-files row with a compacted single-chain directory", rowId: "target-assistant-changed-files-single-chain", files: [ { path: "apps/web/src/components/chat/ChangedFilesTree.tsx", additions: 37, deletions: 45 }, { path: "apps/web/src/components/chat/ChangedFilesTree.test.tsx", additions: 0, deletions: 26, }, ], }), createChangedFilesScenario({ name: "assistant changed-files row with a branch after compaction", rowId: "target-assistant-changed-files-branch-point", files: [ { path: "apps/server/src/git/Layers/GitCore.ts", additions: 44, deletions: 38 }, { path: "apps/server/src/git/Layers/GitCore.test.ts", additions: 18, deletions: 9 }, { path: "apps/server/src/provider/Layers/CodexAdapter.ts", additions: 27, deletions: 8, }, { path: "apps/server/src/provider/Layers/CodexAdapter.test.ts", additions: 36, deletions: 0, }, ], }), createChangedFilesScenario({ name: "assistant changed-files row with mixed root and nested entries", rowId: "target-assistant-changed-files-mixed-root", files: [ { path: "README.md", additions: 5, deletions: 1 }, { path: "packages/contracts/src/orchestration.ts", additions: 13, deletions: 3 }, { path: "packages/shared/src/git.ts", additions: 8, deletions: 2 }, ], }), ]; } async function nextFrame(): Promise<void> { await new Promise<void>((resolve) => { window.requestAnimationFrame(() => resolve()); }); } async function waitForLayout(): Promise<void> { await nextFrame(); await nextFrame(); await nextFrame(); } async function setViewport(viewport: { width: number; height: number }): Promise<void> { await page.viewport(viewport.width, viewport.height); await waitForLayout(); } async function waitForProductionStyles(): Promise<void> { await vi.waitFor( () => { expect( getComputedStyle(document.documentElement).getPropertyValue("--background").trim(), ).not.toBe(""); expect(getComputedStyle(document.body).marginTop).toBe("0px"); }, { timeout: 4_000, interval: 16 }, ); } async function waitForElement<T extends Element>( query: () => T | null, errorMessage: string, ): Promise<T> { let element: T | null = null; await vi.waitFor( () => { element = query(); expect(element, errorMessage).toBeTruthy(); }, { timeout: 8_000, interval: 16 }, ); if (!element) { throw new Error(errorMessage); } return element; } async function measureTimelineRow(input: { host: HTMLElement; props: Omit<ComponentProps<typeof MessagesTimeline>, "scrollContainer">; targetRowId: string; }): Promise<RowMeasurement> { const scrollContainer = await waitForElement( () => input.host.querySelector<HTMLDivElement>( '[data-testid="messages-timeline-scroll-container"]', ), "Unable to find MessagesTimeline scroll container.", ); const rowSelector = `[data-timeline-row-id="${input.targetRowId}"]`; const virtualRowSelector = `[data-virtual-row-id="${input.targetRowId}"]`; let timelineWidthPx = 0; let actualHeightPx = 0; let virtualizerSizePx = 0;