@@ -357,6 +357,144 @@ describe('Markdown file links', () => {
357357 expect ( mocks . getCurrentWorkspacePath ) . not . toHaveBeenCalled ( ) ;
358358 } ) ;
359359
360+ it ( 'preserves existing markdown nodes while streaming content is appended' , async ( ) => {
361+ const initialContent = [
362+ 'Before image' ,
363+ '' ,
364+ '' ,
365+ '' ,
366+ 'After image' ,
367+ ] . join ( '\n' ) ;
368+
369+ await act ( async ( ) => {
370+ root . render (
371+ < Markdown
372+ content = { initialContent }
373+ basePath = { EXAMPLE_WORKSPACE }
374+ isStreaming
375+ onFileViewRequest = { onFileViewRequest }
376+ /> ,
377+ ) ;
378+ await Promise . resolve ( ) ;
379+ await Promise . resolve ( ) ;
380+ } ) ;
381+
382+ const imageBefore = container . querySelector < HTMLImageElement > ( 'img[alt="Stable diagram"]' ) ;
383+ const paragraphsBefore = Array . from ( container . querySelectorAll ( 'p' ) ) ;
384+ expect ( imageBefore ) . not . toBeNull ( ) ;
385+ expect ( paragraphsBefore ) . toHaveLength ( 3 ) ;
386+ expect ( mocks . readFileContent ) . toHaveBeenCalledTimes ( 1 ) ;
387+
388+ await act ( async ( ) => {
389+ root . render (
390+ < Markdown
391+ content = { `${ initialContent } \n\nNew streamed paragraph` }
392+ basePath = { EXAMPLE_WORKSPACE }
393+ isStreaming
394+ onFileViewRequest = { onFileViewRequest }
395+ /> ,
396+ ) ;
397+ await Promise . resolve ( ) ;
398+ } ) ;
399+
400+ const imageAfter = container . querySelector < HTMLImageElement > ( 'img[alt="Stable diagram"]' ) ;
401+ const paragraphsAfter = Array . from ( container . querySelectorAll ( 'p' ) ) ;
402+ expect ( imageAfter ) . toBe ( imageBefore ) ;
403+ expect ( paragraphsAfter ) . toHaveLength ( 4 ) ;
404+ expect ( paragraphsAfter . slice ( 0 , 3 ) ) . toEqual ( paragraphsBefore ) ;
405+ expect ( mocks . readFileContent ) . toHaveBeenCalledTimes ( 1 ) ;
406+ } ) ;
407+
408+ it ( 'renders alt text instead of a broken image when a local image read fails' , async ( ) => {
409+ mocks . readFileContent . mockRejectedValueOnce ( new Error ( 'missing image' ) ) ;
410+
411+ await act ( async ( ) => {
412+ root . render (
413+ < Markdown
414+ content = { '' }
415+ basePath = { EXAMPLE_WORKSPACE }
416+ isStreaming
417+ /> ,
418+ ) ;
419+ await Promise . resolve ( ) ;
420+ await Promise . resolve ( ) ;
421+ await Promise . resolve ( ) ;
422+ } ) ;
423+
424+ expect ( container . querySelector ( 'img[alt="Missing diagram"]' ) ) . toBeNull ( ) ;
425+ const fallback = container . querySelector ( '[data-bf-part="imageFallback"]' ) ;
426+ expect ( fallback ?. textContent ) . toBe ( 'Missing diagram' ) ;
427+
428+ await act ( async ( ) => {
429+ root . render (
430+ < Markdown
431+ content = { '\n\nLater streamed text' }
432+ basePath = { EXAMPLE_WORKSPACE }
433+ isStreaming
434+ /> ,
435+ ) ;
436+ await Promise . resolve ( ) ;
437+ } ) ;
438+
439+ expect ( container . querySelector ( '[data-bf-part="imageFallback"]' ) ) . toBe ( fallback ) ;
440+ expect ( mocks . readFileContent ) . toHaveBeenCalledTimes ( 1 ) ;
441+ } ) ;
442+
443+ it ( 'replaces an externally hosted image after the browser reports an error' , async ( ) => {
444+ await act ( async ( ) => {
445+ root . render ( < Markdown content = { '' } /> ) ;
446+ await Promise . resolve ( ) ;
447+ } ) ;
448+
449+ const image = container . querySelector < HTMLImageElement > ( 'img[alt="Unavailable chart"]' ) ;
450+ expect ( image ) . not . toBeNull ( ) ;
451+
452+ act ( ( ) => {
453+ image ?. dispatchEvent ( new Event ( 'error' ) ) ;
454+ } ) ;
455+
456+ expect ( container . querySelector ( 'img[alt="Unavailable chart"]' ) ) . toBeNull ( ) ;
457+ expect ( container . querySelector ( '[data-bf-part="imageFallback"]' ) ?. textContent )
458+ . toBe ( 'Unavailable chart' ) ;
459+ } ) ;
460+
461+ it ( 'uses the latest source text and callback without remounting a file link' , async ( ) => {
462+ const firstHandler = vi . fn ( ) ;
463+ const secondHandler = vi . fn ( ) ;
464+ const firstPath = 'D:\\First\\README.md' ;
465+ const secondPath = 'E:\\Second\\README.md' ;
466+
467+ await act ( async ( ) => {
468+ root . render (
469+ < Markdown
470+ content = { `[README.md](${ firstPath } )` }
471+ onFileViewRequest = { firstHandler }
472+ /> ,
473+ ) ;
474+ await Promise . resolve ( ) ;
475+ await Promise . resolve ( ) ;
476+ } ) ;
477+
478+ const linkBefore = container . querySelector < HTMLButtonElement > ( 'button.file-link' ) ;
479+ expect ( linkBefore ) . not . toBeNull ( ) ;
480+
481+ await act ( async ( ) => {
482+ root . render (
483+ < Markdown
484+ content = { `[README.md](${ secondPath } )` }
485+ onFileViewRequest = { secondHandler }
486+ /> ,
487+ ) ;
488+ await Promise . resolve ( ) ;
489+ } ) ;
490+
491+ const linkAfter = container . querySelector < HTMLButtonElement > ( 'button.file-link' ) ;
492+ expect ( linkAfter ) . toBe ( linkBefore ) ;
493+ act ( ( ) => linkAfter ?. click ( ) ) ;
494+ expect ( firstHandler ) . not . toHaveBeenCalled ( ) ;
495+ expect ( secondHandler ) . toHaveBeenCalledWith ( secondPath , 'README.md' , undefined ) ;
496+ } ) ;
497+
360498 it ( 'routes remote markdown image reads through the session connection' , async ( ) => {
361499 await act ( async ( ) => {
362500 root . render (
0 commit comments