@@ -70,17 +70,17 @@ describe('inline search activity', () => {
7070 tools = { [ { ...completedSearch ( 'search-1' , 'Launch review' ) , params : tool . params } ] }
7171 />
7272 )
73- expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' )
74- act ( ( ) => header ( ) . click ( ) )
7573 expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
74+ act ( ( ) => header ( ) . click ( ) )
75+ expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' )
7676 } )
7777
7878 it ( 'renders a completed query field from partial streamed arguments and retains stopped status' , ( ) => {
7979 const streamed = { ...tool , params : undefined , streamingArgs : '{"query":"launch review"' }
8080 render ( < SearchActivity tools = { [ streamed ] } /> )
8181 expect ( container . textContent ) . toContain ( 'launch review' )
8282 render ( < SearchActivity tools = { [ { ...streamed , status : 'cancelled' } ] } /> )
83- expect ( container . textContent ) . toContain ( 'Search stopped')
83+ expect ( container . textContent ) . toBe ( 'launch review · stopped')
8484 expect ( container . textContent ) . not . toContain ( 'Searched sources' )
8585 } )
8686
@@ -99,6 +99,16 @@ describe('inline search activity', () => {
9999 { ...tool , toolName : 'search_sources' , status : 'success' , params : { action : 'list' } } ,
100100 'Checked connected sources' ,
101101 ] ,
102+ [
103+ 'failed sources check' ,
104+ { ...tool , toolName : 'search_sources' , status : 'error' , params : { action : 'list' } } ,
105+ 'Checked connected sources' ,
106+ ] ,
107+ [
108+ 'stopped sources check' ,
109+ { ...tool , toolName : 'search_sources' , status : 'cancelled' , params : { action : 'list' } } ,
110+ 'Connected sources' ,
111+ ] ,
102112 ] as const ) (
103113 'labels a %s search and leaves it static unless its lane names it live' ,
104114 ( _state , call , label ) => {
@@ -205,26 +215,27 @@ function completedSearch(id: string, title: string): ToolCallData {
205215it ( 'keeps all ranked matches in each query and preserves disclosure state when later queries arrive' , ( ) => {
206216 const first = completedSearch ( 'one' , 'First query' )
207217 render ( < SearchActivity tools = { [ first ] } /> )
218+ expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
219+ act ( ( ) => header ( ) . click ( ) )
220+ expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' )
208221 expect ( container . querySelectorAll ( 'a' ) ) . toHaveLength ( 3 )
209222 expect ( container . textContent ) . toContain ( 'First query first' )
210223 expect ( container . textContent ) . not . toContain ( 'First query duplicate' )
211224 expect ( container . textContent ) . not . toContain ( 'Unsafe result' )
212225 expect ( container . textContent ) . toContain ( 'example.com' )
213- expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' )
214- act ( ( ) => header ( ) . click ( ) )
215- expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
216226 render ( < SearchActivity tools = { [ first , completedSearch ( 'two' , 'Second query' ) ] } /> )
217- expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false ' )
227+ expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true ' )
218228 const headers = container . querySelectorAll < HTMLElement > ( '[role="button"]' )
219- expect ( headers [ 1 ] . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true ' )
229+ expect ( headers [ 1 ] . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false ' )
220230 expect ( container . querySelectorAll ( 'a' ) ) . toHaveLength ( 3 )
221- act ( ( ) => header ( ) . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'Enter' , bubbles : true } ) ) )
231+ act ( ( ) => headers [ 1 ] . dispatchEvent ( new KeyboardEvent ( 'keydown' , { key : 'Enter' , bubbles : true } ) ) )
222232 expect ( container . querySelectorAll ( 'a' ) ) . toHaveLength ( 6 )
223233 expect ( container . querySelectorAll ( '[role="region"]' ) ) . toHaveLength ( 2 )
224234} )
225235
226236it ( 'falls back to a document icon when a source favicon is unavailable' , ( ) => {
227237 render ( < SearchActivity tools = { [ completedSearch ( 'one' , 'First query' ) ] } /> )
238+ act ( ( ) => header ( ) . click ( ) )
228239 const firstLink = container . querySelector ( 'a' ) !
229240 expect ( firstLink . querySelector ( 'img' ) ) . not . toBeNull ( )
230241 act ( ( ) => firstLink . querySelector ( 'img' ) ! . dispatchEvent ( new Event ( 'error' ) ) )
@@ -235,8 +246,8 @@ it('falls back to a document icon when a source favicon is unavailable', () => {
235246it ( 'distinguishes empty search results from interrupted calls and never previews failed output' , ( ) => {
236247 render ( < SearchActivity tools = { [ { ...completedSearch ( 'one' , 'First query' ) , status : 'error' } ] } /> )
237248 expect ( container . querySelectorAll ( 'a' ) ) . toHaveLength ( 0 )
238- expect ( container . textContent ) . toContain ( 'Search failed ')
239- expect ( container . textContent ) . not . toContain ( 'No results found' )
249+ expect ( container . textContent ) . toBe ( 'First query ')
250+ expect ( container . querySelector ( '[role="button"]' ) ) . toBeNull ( )
240251 render (
241252 < SearchActivity
242253 tools = { [
@@ -248,5 +259,147 @@ it('distinguishes empty search results from interrupted calls and never previews
248259 ] }
249260 />
250261 )
251- expect ( container . textContent ) . toContain ( 'No results found' )
262+ expect ( container . textContent ) . toBe ( 'Launch review · in:launch review · no results' )
263+ } )
264+
265+ describe ( 'described search rows' , ( ) => {
266+ const described : ToolCallData = { ...tool , activityDescription : 'Searching launch review notes' }
267+ const headerText = ( ) => container . querySelector ( '[role="status"]' ) ! . textContent
268+ const outcomeSuffix = ( ) => container . querySelector ( '[role="status"] + span' )
269+
270+ it . each ( [
271+ [ 'executing' , 'Searching launch review notes' , undefined ] ,
272+ [ 'success' , 'Searched launch review notes' , ' · 3 results' ] ,
273+ [ 'error' , 'Searching launch review notes' , undefined ] ,
274+ [ 'rejected' , 'Searching launch review notes' , ' · declined' ] ,
275+ [ 'cancelled' , 'Stopped searching launch review notes' , undefined ] ,
276+ [ 'interrupted' , 'Stopped searching launch review notes' , undefined ] ,
277+ [ 'skipped' , 'Skipped searching launch review notes' , undefined ] ,
278+ ] as const ) ( 'titles a %s call with the shared status wording' , ( status , title , suffix ) => {
279+ render (
280+ < SearchActivity
281+ tools = { [
282+ {
283+ ...completedSearch ( 'one' , 'Launch review' ) ,
284+ params : tool . params ,
285+ activityDescription : described . activityDescription ,
286+ status,
287+ } ,
288+ ] }
289+ />
290+ )
291+ expect ( headerText ( ) ) . toBe ( title )
292+ expect ( outcomeSuffix ( ) ?. textContent ) . toBe ( suffix )
293+ } )
294+
295+ it ( 'reads in progress only while running, and never on a finished call' , ( ) => {
296+ render ( < SearchActivity tools = { [ described ] } liveToolId = { described . id } /> )
297+ expect ( headerText ( ) ) . toBe ( 'Searching launch review notes' )
298+ expect ( container . querySelector ( '[class*="shimmer"]' ) ) . not . toBeNull ( )
299+ render ( < SearchActivity tools = { [ { ...described , status : 'success' } ] } /> )
300+ expect ( headerText ( ) ) . toBe ( 'Searched launch review notes' )
301+ expect ( container . querySelector ( '[class*="shimmer"]' ) ) . toBeNull ( )
302+ } )
303+
304+ it ( 'moves the raw query into one truncated line at the top of the body' , ( ) => {
305+ render ( < SearchActivity tools = { [ described ] } /> )
306+ expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' )
307+ const body = container . querySelector ( `#${ CSS . escape ( header ( ) . getAttribute ( 'aria-controls' ) ! ) } ` ) !
308+ const queryLine = body . querySelector ( '.text-caption' ) !
309+ expect ( queryLine . textContent ) . toBe ( 'Launch review · in:launch review' )
310+ expect ( queryLine . className ) . toContain ( 'whitespace-nowrap' )
311+ expect ( queryLine . className ) . toContain ( 'overflow-hidden' )
312+ expect ( headerText ( ) ) . not . toContain ( 'Launch review' )
313+ } )
314+
315+ it ( 'shows the query only once when there is no description' , ( ) => {
316+ render ( < SearchActivity tools = { [ completedSearch ( 'one' , 'Launch review' ) ] } /> )
317+ act ( ( ) => header ( ) . click ( ) )
318+ expect ( container . textContent ?. match ( / L a u n c h r e v i e w (? ! f i r s t | s e c o n d | t h i r d ) / g) ) . toHaveLength ( 1 )
319+ } )
320+
321+ it ( 'opens while running, closes on completion, and keeps a manual choice either way' , ( ) => {
322+ render ( < SearchActivity tools = { [ described ] } /> )
323+ expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' )
324+ render ( < SearchActivity tools = { [ { ...described , status : 'success' } ] } /> )
325+ expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
326+
327+ act ( ( ) => root . unmount ( ) )
328+ root = createRoot ( container )
329+ render ( < SearchActivity tools = { [ described ] } /> )
330+ act ( ( ) => header ( ) . click ( ) )
331+ expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
332+ render ( < SearchActivity tools = { [ { ...described , status : 'success' } ] } /> )
333+ expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
334+ act ( ( ) => header ( ) . click ( ) )
335+ expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' )
336+
337+ act ( ( ) => root . unmount ( ) )
338+ root = createRoot ( container )
339+ render ( < SearchActivity tools = { [ described ] } /> )
340+ act ( ( ) => header ( ) . click ( ) )
341+ act ( ( ) => header ( ) . click ( ) )
342+ render ( < SearchActivity tools = { [ { ...described , status : 'success' } ] } /> )
343+ expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'true' )
344+ } )
345+
346+ it ( 'renders a reloaded message with every finished search collapsed' , ( ) => {
347+ render (
348+ < SearchActivity
349+ tools = { [
350+ { ...completedSearch ( 'one' , 'First query' ) , activityDescription : 'Searching specs' } ,
351+ completedSearch ( 'two' , 'Second query' ) ,
352+ { ...described , id : 'three' , status : 'error' } ,
353+ ] }
354+ />
355+ )
356+ const headers = [ ...container . querySelectorAll < HTMLElement > ( '[role="button"]' ) ]
357+ expect ( headers . map ( ( row ) => row . getAttribute ( 'aria-expanded' ) ) ) . toEqual ( [
358+ 'false' ,
359+ 'false' ,
360+ 'false' ,
361+ ] )
362+ expect ( container . querySelectorAll ( 'a' ) ) . toHaveLength ( 0 )
363+ } )
364+
365+ it ( 'keeps a failed search silent: static neutral title, no failure text or error styling' , ( ) => {
366+ render ( < SearchActivity tools = { [ { ...tool , status : 'error' } ] } /> )
367+ expect ( container . querySelector ( '[role="button"]' ) ) . toBeNull ( )
368+ expect ( container . textContent ) . toBe ( 'Launch review · in:launch review' )
369+ render (
370+ < SearchActivity tools = { [ { ...completedSearch ( 'one' , 'x' ) , ...described , status : 'error' } ] } />
371+ )
372+ expect ( header ( ) . getAttribute ( 'aria-expanded' ) ) . toBe ( 'false' )
373+ expect ( headerText ( ) ) . toBe ( 'Searching launch review notes' )
374+ expect ( outcomeSuffix ( ) ) . toBeNull ( )
375+ expect ( container . querySelector ( '[class*="shimmer"]' ) ) . toBeNull ( )
376+ act ( ( ) => header ( ) . click ( ) )
377+ expect ( container . querySelectorAll ( 'a' ) ) . toHaveLength ( 0 )
378+ expect ( container . textContent ) . toBe (
379+ 'Searching launch review notesLaunch review · in:launch review'
380+ )
381+ expect ( container . textContent ) . not . toMatch ( / f a i l / i)
382+ expect ( container . innerHTML ) . not . toContain ( '--text-error' )
383+ } )
384+
385+ it ( 'keeps user-initiated and empty outcomes as muted suffixes' , ( ) => {
386+ render ( < SearchActivity tools = { [ { ...described , status : 'rejected' } ] } /> )
387+ expect ( outcomeSuffix ( ) ?. textContent ) . toBe ( ' · declined' )
388+ expect ( outcomeSuffix ( ) ?. className ) . toContain ( 'text-[var(--text-tertiary)]' )
389+ render ( < SearchActivity tools = { [ { ...tool , status : 'skipped' } ] } /> )
390+ expect ( outcomeSuffix ( ) ?. textContent ) . toBe ( ' · skipped' )
391+ render ( < SearchActivity tools = { [ completedSearch ( 'one' , 'First query' ) ] } /> )
392+ expect ( outcomeSuffix ( ) ?. textContent ) . toBe ( ' · 3 results' )
393+ expect ( container . innerHTML ) . not . toContain ( '--text-error' )
394+ } )
395+
396+ it ( 'lays results out in dense rows with a matching bounded list' , ( ) => {
397+ render ( < SearchActivity tools = { [ completedSearch ( 'one' , 'First query' ) ] } /> )
398+ act ( ( ) => header ( ) . click ( ) )
399+ expect ( container . querySelector ( '[role="region"]' ) ! . className ) . toContain ( 'max-h-[152px]' )
400+ for ( const link of container . querySelectorAll ( 'a' ) ) {
401+ expect ( link . className ) . toContain ( 'h-8' )
402+ expect ( link . className ) . not . toContain ( 'h-10' )
403+ }
404+ } )
252405} )
0 commit comments