1- import { act , renderHook } from "@testing-library/react" ;
1+ import {
2+ act ,
3+ cleanup ,
4+ fireEvent ,
5+ render ,
6+ renderHook ,
7+ screen ,
8+ } from "@testing-library/react" ;
29import { isValidElement } from "react" ;
310import { beforeEach , describe , expect , it , vi } from "vitest" ;
411
@@ -25,6 +32,7 @@ const hoisted = vi.hoisted(() => ({
2532 summary : string | null ;
2633 isGenerating : boolean ;
2734 } > ,
35+ batch : { } as Record < string , { error : string | null } > ,
2836 generateMissingPastNotes : vi . fn ( ) ,
2937 regeneratePastNote : vi . fn ( ) ,
3038} ) ) ;
@@ -69,10 +77,12 @@ vi.mock("~/stt/contexts", () => ({
6977 requestedLiveTranscription : boolean | null ;
7078 liveTranscriptionActive : boolean | null ;
7179 } ;
80+ batch : Record < string , { error : string | null } > ;
7281 } ) => unknown ,
7382 ) =>
7483 selector ( {
7584 live : hoisted . live ,
85+ batch : hoisted . batch ,
7686 } ) ,
7787} ) ) ;
7888
@@ -88,12 +98,14 @@ import { useSessionBottomAccessory } from "./index";
8898
8999describe ( "useSessionBottomAccessory" , ( ) => {
90100 beforeEach ( ( ) => {
101+ cleanup ( ) ;
91102 hoisted . hotkeys . clear ( ) ;
92103 hoisted . live . status = "inactive" ;
93104 hoisted . live . sessionId = null ;
94105 hoisted . live . requestedLiveTranscription = true ;
95106 hoisted . live . liveTranscriptionActive = true ;
96107 hoisted . pastNotes = [ ] ;
108+ hoisted . batch = { } ;
97109 hoisted . generateMissingPastNotes . mockClear ( ) ;
98110 hoisted . regeneratePastNote . mockClear ( ) ;
99111 useShellMock . mockReturnValue ( {
@@ -113,7 +125,7 @@ describe("useSessionBottomAccessory", () => {
113125 useSessionBottomAccessory ( {
114126 sessionId : "session-1" ,
115127 sessionMode : "inactive" ,
116- audioUrl : "file:///session.wav" ,
128+ audioExists : true ,
117129 hasTranscript : true ,
118130 } ) ,
119131 ) ;
@@ -166,7 +178,7 @@ describe("useSessionBottomAccessory", () => {
166178 useSessionBottomAccessory ( {
167179 sessionId : "session-1" ,
168180 sessionMode : "inactive" ,
169- audioUrl : "file:///session.wav" ,
181+ audioExists : true ,
170182 hasTranscript : true ,
171183 } ) ,
172184 ) ;
@@ -199,7 +211,7 @@ describe("useSessionBottomAccessory", () => {
199211 useSessionBottomAccessory ( {
200212 sessionId : "session-1" ,
201213 sessionMode : "inactive" ,
202- audioUrl : "file:///session.wav" ,
214+ audioExists : true ,
203215 hasTranscript : true ,
204216 } ) ,
205217 ) ;
@@ -222,7 +234,7 @@ describe("useSessionBottomAccessory", () => {
222234 useSessionBottomAccessory ( {
223235 sessionId : "session-1" ,
224236 sessionMode : "inactive" ,
225- audioUrl : "file:///session.wav" ,
237+ audioExists : true ,
226238 hasTranscript : true ,
227239 } ) ,
228240 ) ;
@@ -249,7 +261,7 @@ describe("useSessionBottomAccessory", () => {
249261 useSessionBottomAccessory ( {
250262 sessionId : "session-1" ,
251263 sessionMode : "inactive" ,
252- audioUrl : "file:///session.wav" ,
264+ audioExists : true ,
253265 hasTranscript : true ,
254266 } ) ,
255267 ) ;
@@ -273,6 +285,165 @@ describe("useSessionBottomAccessory", () => {
273285 } ) ;
274286 } ) ;
275287
288+ it ( "uses related meetings as the only tab when there is no transcript content" , ( ) => {
289+ hoisted . pastNotes = [
290+ {
291+ sessionId : "past-session" ,
292+ title : "Weekly sync" ,
293+ dateLabel : "May 28, 2026" ,
294+ summary : null ,
295+ isGenerating : false ,
296+ } ,
297+ ] ;
298+
299+ const { result } = renderHook ( ( ) =>
300+ useSessionBottomAccessory ( {
301+ sessionId : "session-1" ,
302+ sessionMode : "inactive" ,
303+ audioExists : false ,
304+ hasTranscript : false ,
305+ } ) ,
306+ ) ;
307+
308+ expect ( result . current . bottomAccessoryState ) . toEqual ( {
309+ mode : "transcript_only" ,
310+ expanded : false ,
311+ } ) ;
312+
313+ render ( result . current . bottomBorderHandle ) ;
314+
315+ expect ( screen . queryByRole ( "button" , { name : / T r a n s c r i p t / } ) ) . toBeNull ( ) ;
316+
317+ fireEvent . click (
318+ screen . getByRole ( "button" , { name : "Expand Related meetings" } ) ,
319+ ) ;
320+
321+ expect ( hoisted . generateMissingPastNotes ) . toHaveBeenCalledTimes ( 1 ) ;
322+ expect ( result . current . bottomAccessoryState ) . toEqual ( {
323+ mode : "transcript_only" ,
324+ expanded : true ,
325+ } ) ;
326+ } ) ;
327+
328+ it ( "keeps the transcript panel available after batch transcription fails without words" , ( ) => {
329+ hoisted . batch = {
330+ "session-1" : {
331+ error : "batch start failed: connection refused" ,
332+ } ,
333+ } ;
334+
335+ const { result } = renderHook ( ( ) =>
336+ useSessionBottomAccessory ( {
337+ sessionId : "session-1" ,
338+ sessionMode : "inactive" ,
339+ audioExists : false ,
340+ hasTranscript : false ,
341+ } ) ,
342+ ) ;
343+
344+ expect ( result . current . bottomAccessoryState ) . toEqual ( {
345+ mode : "transcript_only" ,
346+ expanded : false ,
347+ } ) ;
348+ expect ( result . current . bottomBorderHandle ) . not . toBeNull ( ) ;
349+ } ) ;
350+
351+ it ( "keeps the transcript tab visible for batch errors next to related meetings" , ( ) => {
352+ hoisted . batch = {
353+ "session-1" : {
354+ error : "batch start failed: connection refused" ,
355+ } ,
356+ } ;
357+ hoisted . pastNotes = [
358+ {
359+ sessionId : "past-session" ,
360+ title : "Weekly sync" ,
361+ dateLabel : "May 28, 2026" ,
362+ summary : null ,
363+ isGenerating : false ,
364+ } ,
365+ ] ;
366+
367+ const { result } = renderHook ( ( ) =>
368+ useSessionBottomAccessory ( {
369+ sessionId : "session-1" ,
370+ sessionMode : "inactive" ,
371+ audioExists : false ,
372+ hasTranscript : false ,
373+ } ) ,
374+ ) ;
375+
376+ render ( result . current . bottomBorderHandle ) ;
377+
378+ expect (
379+ screen . getByRole ( "button" , { name : "Expand Transcript" } ) ,
380+ ) . not . toBeNull ( ) ;
381+ expect (
382+ screen . getByRole ( "button" , { name : "Expand Related meetings" } ) ,
383+ ) . not . toBeNull ( ) ;
384+ } ) ;
385+
386+ it ( "keeps playback disabled until the audio URL is ready" , ( ) => {
387+ const { result, rerender } = renderHook (
388+ ( { audioUrlReady } : { audioUrlReady : boolean } ) =>
389+ useSessionBottomAccessory ( {
390+ sessionId : "session-1" ,
391+ sessionMode : "inactive" ,
392+ audioExists : true ,
393+ audioUrlReady,
394+ hasTranscript : false ,
395+ } ) ,
396+ {
397+ initialProps : {
398+ audioUrlReady : false ,
399+ } ,
400+ } ,
401+ ) ;
402+
403+ expect ( result . current . bottomAccessoryState ) . toEqual ( {
404+ mode : "transcript_only" ,
405+ expanded : false ,
406+ } ) ;
407+ expect ( result . current . bottomBorderHandle ) . not . toBeNull ( ) ;
408+
409+ rerender ( { audioUrlReady : true } ) ;
410+
411+ expect ( result . current . bottomAccessoryState ) . toEqual ( {
412+ mode : "playback" ,
413+ expanded : false ,
414+ } ) ;
415+ } ) ;
416+
417+ it ( "keeps the post-session handle visible while audio lookup is loading" , ( ) => {
418+ const { result, rerender } = renderHook (
419+ ( { isAudioLoading } : { isAudioLoading : boolean } ) =>
420+ useSessionBottomAccessory ( {
421+ sessionId : "session-1" ,
422+ sessionMode : "inactive" ,
423+ audioExists : false ,
424+ audioUrlReady : false ,
425+ isAudioLoading,
426+ hasTranscript : false ,
427+ } ) ,
428+ {
429+ initialProps : {
430+ isAudioLoading : true ,
431+ } ,
432+ } ,
433+ ) ;
434+
435+ expect ( result . current . bottomAccessoryState ) . toEqual ( {
436+ mode : "transcript_only" ,
437+ expanded : false ,
438+ } ) ;
439+ expect ( result . current . bottomBorderHandle ) . not . toBeNull ( ) ;
440+
441+ rerender ( { isAudioLoading : false } ) ;
442+
443+ expect ( result . current . bottomAccessoryState ) . toBeNull ( ) ;
444+ expect ( result . current . bottomBorderHandle ) . toBeNull ( ) ;
445+ } ) ;
446+
276447 it ( "hides the bottom accessory while recording for batch transcription" , ( ) => {
277448 hoisted . live . requestedLiveTranscription = false ;
278449 hoisted . live . liveTranscriptionActive = false ;
@@ -281,7 +452,7 @@ describe("useSessionBottomAccessory", () => {
281452 useSessionBottomAccessory ( {
282453 sessionId : "session-1" ,
283454 sessionMode : "active" ,
284- audioUrl : null ,
455+ audioExists : false ,
285456 hasTranscript : false ,
286457 } ) ,
287458 ) ;
@@ -296,7 +467,7 @@ describe("useSessionBottomAccessory", () => {
296467 useSessionBottomAccessory ( {
297468 sessionId : "session-1" ,
298469 sessionMode : "finalizing" ,
299- audioUrl : null ,
470+ audioExists : false ,
300471 hasTranscript : false ,
301472 } ) ,
302473 ) ;
@@ -314,7 +485,7 @@ describe("useSessionBottomAccessory", () => {
314485 useSessionBottomAccessory ( {
315486 sessionId : "session-1" ,
316487 sessionMode : "inactive" ,
317- audioUrl : "file:///session.wav" ,
488+ audioExists : true ,
318489 hasTranscript : true ,
319490 } ) ,
320491 ) ;
@@ -332,7 +503,7 @@ describe("useSessionBottomAccessory", () => {
332503 useSessionBottomAccessory ( {
333504 sessionId : "session-1" ,
334505 sessionMode : "running_batch" ,
335- audioUrl : "file:///session.wav" ,
506+ audioExists : true ,
336507 hasTranscript : true ,
337508 } ) ,
338509 ) ;
@@ -350,7 +521,7 @@ describe("useSessionBottomAccessory", () => {
350521 useSessionBottomAccessory ( {
351522 sessionId : "session-1" ,
352523 sessionMode : "running_batch" ,
353- audioUrl : "file:///session.wav" ,
524+ audioExists : true ,
354525 hasTranscript : true ,
355526 } ) ,
356527 ) ;
@@ -369,7 +540,7 @@ describe("useSessionBottomAccessory", () => {
369540 useSessionBottomAccessory ( {
370541 sessionId : "session-1" ,
371542 sessionMode,
372- audioUrl : "file:///session.wav" ,
543+ audioExists : true ,
373544 hasTranscript : true ,
374545 } ) ,
375546 {
@@ -416,7 +587,7 @@ describe("useSessionBottomAccessory", () => {
416587 useSessionBottomAccessory ( {
417588 sessionId : "session-1" ,
418589 sessionMode : "active" ,
419- audioUrl : null ,
590+ audioExists : false ,
420591 hasTranscript : false ,
421592 } ) ,
422593 ) ;
0 commit comments