11import { MessageId } from "@t3tools/contracts" ;
22import { renderToStaticMarkup } from "react-dom/server" ;
3- import { describe , expect , it } from "vitest" ;
3+ import { beforeAll , describe , expect , it , vi } from "vitest" ;
4+
5+ function matchMedia ( ) {
6+ return {
7+ matches : false ,
8+ addEventListener : ( ) => { } ,
9+ removeEventListener : ( ) => { } ,
10+ } ;
11+ }
12+
13+ beforeAll ( ( ) => {
14+ const classList = {
15+ add : ( ) => { } ,
16+ remove : ( ) => { } ,
17+ toggle : ( ) => { } ,
18+ contains : ( ) => false ,
19+ } ;
20+
21+ vi . stubGlobal ( "localStorage" , {
22+ getItem : ( ) => null ,
23+ setItem : ( ) => { } ,
24+ removeItem : ( ) => { } ,
25+ clear : ( ) => { } ,
26+ } ) ;
27+ vi . stubGlobal ( "window" , {
28+ matchMedia,
29+ addEventListener : ( ) => { } ,
30+ removeEventListener : ( ) => { } ,
31+ desktopBridge : undefined ,
32+ } ) ;
33+ vi . stubGlobal ( "document" , {
34+ documentElement : {
35+ classList,
36+ offsetHeight : 0 ,
37+ } ,
38+ } ) ;
39+ vi . stubGlobal ( "requestAnimationFrame" , ( callback : FrameRequestCallback ) => {
40+ callback ( 0 ) ;
41+ return 0 ;
42+ } ) ;
43+ } ) ;
444
545const ACTIVE_THREAD_ENVIRONMENT_ID = "environment-local" as never ;
646
@@ -12,7 +52,6 @@ describe("MessagesTimeline", () => {
1252 hasMessages
1353 isWorking = { false }
1454 activeTurnInProgress = { false }
15- activeTurnId = { null }
1655 activeTurnStartedAt = { null }
1756 scrollContainer = { null }
1857 timelineEntries = { [
@@ -70,7 +109,6 @@ describe("MessagesTimeline", () => {
70109 hasMessages
71110 isWorking = { false }
72111 activeTurnInProgress = { false }
73- activeTurnId = { null }
74112 activeTurnStartedAt = { null }
75113 scrollContainer = { null }
76114 timelineEntries = { [
@@ -110,170 +148,4 @@ describe("MessagesTimeline", () => {
110148 expect ( markup ) . toContain ( "Context compacted" ) ;
111149 expect ( markup ) . toContain ( "Work log" ) ;
112150 } ) ;
113-
114- it ( "does not render the assistant copy button while streaming" , async ( ) => {
115- const { MessagesTimeline } = await import ( "./MessagesTimeline" ) ;
116- const markup = renderToStaticMarkup (
117- < MessagesTimeline
118- hasMessages
119- isWorking = { false }
120- activeTurnInProgress
121- activeTurnId = { "turn-1" as never }
122- activeTurnStartedAt = "2026-03-17T19:12:28.000Z"
123- scrollContainer = { null }
124- timelineEntries = { [
125- {
126- id : "assistant-entry" ,
127- kind : "message" ,
128- createdAt : "2026-03-17T19:12:28.000Z" ,
129- message : {
130- id : MessageId . make ( "assistant-1" ) ,
131- role : "assistant" ,
132- text : "" ,
133- turnId : "turn-1" as never ,
134- createdAt : "2026-03-17T19:12:28.000Z" ,
135- streaming : true ,
136- } ,
137- } ,
138- ] }
139- completionDividerBeforeEntryId = { null }
140- completionSummary = { null }
141- turnDiffSummaryByAssistantMessageId = { new Map ( ) }
142- nowIso = "2026-03-17T19:12:30.000Z"
143- expandedWorkGroups = { { } }
144- onToggleWorkGroup = { ( ) => { } }
145- changedFilesExpandedByTurnId = { { } }
146- onSetChangedFilesExpanded = { ( ) => { } }
147- onOpenTurnDiff = { ( ) => { } }
148- revertTurnCountByUserMessageId = { new Map ( ) }
149- onRevertUserMessage = { ( ) => { } }
150- isRevertingCheckpoint = { false }
151- onImageExpand = { ( ) => { } }
152- activeThreadEnvironmentId = { ACTIVE_THREAD_ENVIRONMENT_ID }
153- markdownCwd = { undefined }
154- resolvedTheme = "light"
155- timestampFormat = "locale"
156- workspaceRoot = { undefined }
157- /> ,
158- ) ;
159-
160- expect ( markup ) . not . toContain ( 'aria-label="Copy assistant response"' ) ;
161- } , 10_000 ) ;
162-
163- it ( "does not render the assistant copy button until the active turn settles" , async ( ) => {
164- const { MessagesTimeline } = await import ( "./MessagesTimeline" ) ;
165- const markup = renderToStaticMarkup (
166- < MessagesTimeline
167- hasMessages
168- isWorking = { false }
169- activeTurnInProgress
170- activeTurnId = { "turn-1" as never }
171- activeTurnStartedAt = "2026-03-17T19:12:28.000Z"
172- scrollContainer = { null }
173- timelineEntries = { [
174- {
175- id : "assistant-entry" ,
176- kind : "message" ,
177- createdAt : "2026-03-17T19:12:29.000Z" ,
178- message : {
179- id : MessageId . make ( "assistant-1" ) ,
180- role : "assistant" ,
181- text : "Partial answer before tool work finishes." ,
182- turnId : "turn-1" as never ,
183- createdAt : "2026-03-17T19:12:29.000Z" ,
184- completedAt : "2026-03-17T19:12:30.000Z" ,
185- streaming : false ,
186- } ,
187- } ,
188- {
189- id : "work-entry" ,
190- kind : "work" ,
191- createdAt : "2026-03-17T19:12:31.000Z" ,
192- entry : {
193- id : "work-1" ,
194- createdAt : "2026-03-17T19:12:31.000Z" ,
195- label : "Ran command" ,
196- tone : "tool" ,
197- } ,
198- } ,
199- ] }
200- completionDividerBeforeEntryId = { null }
201- completionSummary = { null }
202- turnDiffSummaryByAssistantMessageId = { new Map ( ) }
203- nowIso = "2026-03-17T19:12:33.000Z"
204- expandedWorkGroups = { { } }
205- onToggleWorkGroup = { ( ) => { } }
206- changedFilesExpandedByTurnId = { { } }
207- onSetChangedFilesExpanded = { ( ) => { } }
208- onOpenTurnDiff = { ( ) => { } }
209- revertTurnCountByUserMessageId = { new Map ( ) }
210- onRevertUserMessage = { ( ) => { } }
211- isRevertingCheckpoint = { false }
212- onImageExpand = { ( ) => { } }
213- activeThreadEnvironmentId = { ACTIVE_THREAD_ENVIRONMENT_ID }
214- markdownCwd = { undefined }
215- resolvedTheme = "light"
216- timestampFormat = "locale"
217- workspaceRoot = { undefined }
218- /> ,
219- ) ;
220-
221- expect ( markup ) . not . toContain ( 'aria-label="Copy assistant response"' ) ;
222- } , 10_000 ) ;
223-
224- it ( "renders the assistant copy button next to the footer metadata with row hover visibility" , async ( ) => {
225- const { MessagesTimeline } = await import ( "./MessagesTimeline" ) ;
226- const markup = renderToStaticMarkup (
227- < MessagesTimeline
228- hasMessages
229- isWorking = { false }
230- activeTurnInProgress = { false }
231- activeTurnId = { null }
232- activeTurnStartedAt = { null }
233- scrollContainer = { null }
234- timelineEntries = { [
235- {
236- id : "assistant-entry" ,
237- kind : "message" ,
238- createdAt : "2026-03-17T19:12:29.000Z" ,
239- message : {
240- id : MessageId . make ( "assistant-1" ) ,
241- role : "assistant" ,
242- text : "Final answer." ,
243- turnId : "turn-1" as never ,
244- createdAt : "2026-03-17T19:12:29.000Z" ,
245- completedAt : "2026-03-17T19:12:30.000Z" ,
246- streaming : false ,
247- } ,
248- } ,
249- ] }
250- completionDividerBeforeEntryId = { null }
251- completionSummary = { null }
252- turnDiffSummaryByAssistantMessageId = { new Map ( ) }
253- nowIso = "2026-03-17T19:12:33.000Z"
254- expandedWorkGroups = { { } }
255- onToggleWorkGroup = { ( ) => { } }
256- changedFilesExpandedByTurnId = { { } }
257- onSetChangedFilesExpanded = { ( ) => { } }
258- onOpenTurnDiff = { ( ) => { } }
259- revertTurnCountByUserMessageId = { new Map ( ) }
260- onRevertUserMessage = { ( ) => { } }
261- isRevertingCheckpoint = { false }
262- onImageExpand = { ( ) => { } }
263- activeThreadEnvironmentId = { ACTIVE_THREAD_ENVIRONMENT_ID }
264- markdownCwd = { undefined }
265- resolvedTheme = "light"
266- timestampFormat = "locale"
267- workspaceRoot = { undefined }
268- /> ,
269- ) ;
270-
271- expect ( markup ) . toContain ( 'aria-label="Copy assistant response"' ) ;
272- expect ( markup ) . toContain ( "group/assistant" ) ;
273- expect ( markup ) . toContain ( "group-hover/assistant:opacity-100" ) ;
274- expect ( markup ) . toContain ( 'class="text-[10px] text-muted-foreground/30"' ) ;
275- expect ( markup . indexOf ( 'class="text-[10px] text-muted-foreground/30"' ) ) . toBeLessThan (
276- markup . indexOf ( 'aria-label="Copy assistant response"' ) ,
277- ) ;
278- } , 10_000 ) ;
279151} ) ;
0 commit comments