@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
33import { defineComponent , h , nextTick } from 'vue' ;
44
55import { FlareErrorBoundary } from '../src/FlareErrorBoundary' ;
6+ import { FlareVueContext } from '../src/types' ;
67
78const mockReport = vi . fn ( ) ;
89
@@ -29,7 +30,7 @@ describe('FlareErrorBoundary', () => {
2930
3031 beforeEach ( ( ) => {
3132 testError = new Error ( 'test error' ) ;
32- mockReport . mockClear ( ) ;
33+ mockReport . mockReset ( ) ;
3334 consoleWarnSpy = vi . spyOn ( console , 'warn' ) . mockImplementation ( ( ) => { } ) ;
3435 } ) ;
3536
@@ -293,4 +294,293 @@ describe('FlareErrorBoundary', () => {
293294 } ) ;
294295 } ) . toThrow ( 'report failed' ) ;
295296 } ) ;
297+
298+ test ( 'calls beforeEvaluate before reporting' , async ( ) => {
299+ const callOrder : string [ ] = [ ] ;
300+
301+ const beforeEvaluate = vi . fn ( ( ) => callOrder . push ( 'beforeEvaluate' ) ) ;
302+ mockReport . mockImplementationOnce ( ( ) => callOrder . push ( 'report' ) ) ;
303+
304+ mount ( FlareErrorBoundary , {
305+ props : { beforeEvaluate } ,
306+ slots : {
307+ default : ( ) => h ( ThrowingComponent ) ,
308+ fallback : ( ) => h ( 'div' , 'Error' ) ,
309+ } ,
310+ } ) ;
311+
312+ await nextTick ( ) ;
313+
314+ expect ( beforeEvaluate ) . toHaveBeenCalledOnce ( ) ;
315+ expect ( callOrder ) . toEqual ( [ 'beforeEvaluate' , 'report' ] ) ;
316+ } ) ;
317+
318+ test ( 'calls beforeEvaluate with error, instance, and info' , async ( ) => {
319+ const beforeEvaluate = vi . fn ( ) ;
320+
321+ mount ( FlareErrorBoundary , {
322+ props : { beforeEvaluate } ,
323+ slots : {
324+ default : ( ) => h ( ThrowingComponent ) ,
325+ fallback : ( ) => h ( 'div' , 'Error' ) ,
326+ } ,
327+ } ) ;
328+
329+ await nextTick ( ) ;
330+
331+ expect ( beforeEvaluate . mock . calls [ 0 ] [ 0 ] . error ) . toBe ( testError ) ;
332+ expect ( beforeEvaluate . mock . calls [ 0 ] [ 0 ] . instance ) . toBeDefined ( ) ;
333+ expect ( beforeEvaluate . mock . calls [ 0 ] [ 0 ] . info ) . toEqual ( expect . any ( String ) ) ;
334+ } ) ;
335+
336+ test ( 'calls beforeSubmit after beforeEvaluate and before reporting' , async ( ) => {
337+ const callOrder : string [ ] = [ ] ;
338+
339+ const beforeEvaluate = vi . fn ( ( ) => callOrder . push ( 'beforeEvaluate' ) ) ;
340+ const beforeSubmit = vi . fn ( ( params : { context : FlareVueContext } ) => {
341+ callOrder . push ( 'beforeSubmit' ) ;
342+ return params . context ;
343+ } ) ;
344+ mockReport . mockImplementationOnce ( ( ) => callOrder . push ( 'report' ) ) ;
345+
346+ mount ( FlareErrorBoundary , {
347+ props : { beforeEvaluate, beforeSubmit } ,
348+ slots : {
349+ default : ( ) => h ( ThrowingComponent ) ,
350+ fallback : ( ) => h ( 'div' , 'Error' ) ,
351+ } ,
352+ } ) ;
353+
354+ await nextTick ( ) ;
355+
356+ expect ( beforeSubmit ) . toHaveBeenCalledOnce ( ) ;
357+ expect ( callOrder ) . toEqual ( [ 'beforeEvaluate' , 'beforeSubmit' , 'report' ] ) ;
358+ } ) ;
359+
360+ test ( 'calls beforeSubmit with error, instance, info, and context' , async ( ) => {
361+ const beforeSubmit = vi . fn (
362+ ( params : { error : Error ; instance : unknown ; info : string ; context : FlareVueContext } ) => params . context
363+ ) ;
364+
365+ mount ( FlareErrorBoundary , {
366+ props : { beforeSubmit } ,
367+ slots : {
368+ default : ( ) => h ( ThrowingComponent ) ,
369+ fallback : ( ) => h ( 'div' , 'Error' ) ,
370+ } ,
371+ } ) ;
372+
373+ await nextTick ( ) ;
374+
375+ expect ( beforeSubmit . mock . calls [ 0 ] [ 0 ] . error ) . toBe ( testError ) ;
376+ expect ( beforeSubmit . mock . calls [ 0 ] [ 0 ] . instance ) . toBeDefined ( ) ;
377+ expect ( beforeSubmit . mock . calls [ 0 ] [ 0 ] . info ) . toEqual ( expect . any ( String ) ) ;
378+ expect ( beforeSubmit . mock . calls [ 0 ] [ 0 ] . context . vue . componentHierarchy ) . toBeInstanceOf ( Array ) ;
379+ expect ( beforeSubmit . mock . calls [ 0 ] [ 0 ] . context . vue . componentName ) . toBe ( 'ThrowingComponent' ) ;
380+ } ) ;
381+
382+ test ( 'beforeSubmit can modify the context before reporting' , async ( ) => {
383+ const customHierarchy = [ 'Custom' , 'Modified' ] ;
384+ const beforeSubmit = vi . fn ( ( { context } : { context : FlareVueContext } ) => ( {
385+ ...context ,
386+ vue : {
387+ ...context . vue ,
388+ componentHierarchy : customHierarchy ,
389+ } ,
390+ } ) ) ;
391+
392+ mount ( FlareErrorBoundary , {
393+ props : { beforeSubmit } ,
394+ slots : {
395+ default : ( ) => h ( ThrowingComponent ) ,
396+ fallback : ( ) => h ( 'div' , 'Error' ) ,
397+ } ,
398+ } ) ;
399+
400+ await nextTick ( ) ;
401+
402+ const reportedContext = mockReport . mock . calls [ 0 ] [ 1 ] ;
403+ expect ( reportedContext . vue . componentHierarchy ) . toBe ( customHierarchy ) ;
404+ } ) ;
405+
406+ test ( 'beforeSubmit modified context is passed to afterSubmit' , async ( ) => {
407+ const customHierarchy = [ 'Custom' , 'Modified' ] ;
408+ const beforeSubmit = vi . fn ( ( { context } : { context : FlareVueContext } ) => ( {
409+ ...context ,
410+ vue : {
411+ ...context . vue ,
412+ componentHierarchy : customHierarchy ,
413+ } ,
414+ } ) ) ;
415+ const afterSubmit = vi . fn ( ) ;
416+
417+ mount ( FlareErrorBoundary , {
418+ props : { beforeSubmit, afterSubmit } ,
419+ slots : {
420+ default : ( ) => h ( ThrowingComponent ) ,
421+ fallback : ( ) => h ( 'div' , 'Error' ) ,
422+ } ,
423+ } ) ;
424+
425+ await nextTick ( ) ;
426+
427+ expect ( afterSubmit . mock . calls [ 0 ] [ 0 ] . context . vue . componentHierarchy ) . toBe ( customHierarchy ) ;
428+ } ) ;
429+
430+ test ( 'calls afterSubmit after reporting' , async ( ) => {
431+ const callOrder : string [ ] = [ ] ;
432+
433+ mockReport . mockImplementationOnce ( ( ) => callOrder . push ( 'report' ) ) ;
434+ const afterSubmit = vi . fn ( ( ) => {
435+ callOrder . push ( 'afterSubmit' ) ;
436+ } ) ;
437+
438+ mount ( FlareErrorBoundary , {
439+ props : { afterSubmit } ,
440+ slots : {
441+ default : ( ) => h ( ThrowingComponent ) ,
442+ fallback : ( ) => h ( 'div' , 'Error' ) ,
443+ } ,
444+ } ) ;
445+
446+ await nextTick ( ) ;
447+
448+ expect ( afterSubmit ) . toHaveBeenCalledOnce ( ) ;
449+ expect ( afterSubmit . mock . calls [ 0 ] [ 0 ] . error ) . toBe ( testError ) ;
450+ expect ( afterSubmit . mock . calls [ 0 ] [ 0 ] . instance ) . toBeDefined ( ) ;
451+ expect ( afterSubmit . mock . calls [ 0 ] [ 0 ] . info ) . toEqual ( expect . any ( String ) ) ;
452+ expect ( afterSubmit . mock . calls [ 0 ] [ 0 ] . context . vue . componentHierarchy ) . toBeInstanceOf ( Array ) ;
453+ expect ( callOrder ) . toEqual ( [ 'report' , 'afterSubmit' ] ) ;
454+ } ) ;
455+
456+ test ( 'uses original context when beforeSubmit does not return' , async ( ) => {
457+ const beforeSubmit = vi . fn ( ( ) => {
458+ // user forgot to return context
459+ } ) ;
460+
461+ mount ( FlareErrorBoundary , {
462+ // @ts -expect-error - intentionally testing a user mistake where beforeSubmit does not return
463+ props : { beforeSubmit } ,
464+ slots : {
465+ default : ( ) => h ( ThrowingComponent ) ,
466+ fallback : ( ) => h ( 'div' , 'Error' ) ,
467+ } ,
468+ } ) ;
469+
470+ await nextTick ( ) ;
471+
472+ expect ( beforeSubmit ) . toHaveBeenCalledOnce ( ) ;
473+ const reportedContext = mockReport . mock . calls [ 0 ] [ 1 ] ;
474+ expect ( reportedContext . vue . componentName ) . toBe ( 'ThrowingComponent' ) ;
475+ expect ( reportedContext . vue . componentHierarchy ) . toBeInstanceOf ( Array ) ;
476+ } ) ;
477+
478+ test ( 'beforeSubmit modified componentHierarchy is reflected in the fallback render' , async ( ) => {
479+ const customHierarchy = [ 'Custom' , 'Modified' ] ;
480+ const beforeSubmit = vi . fn ( ( { context } : { context : FlareVueContext } ) => ( {
481+ ...context ,
482+ vue : {
483+ ...context . vue ,
484+ componentHierarchy : customHierarchy ,
485+ } ,
486+ } ) ) ;
487+
488+ const wrapper = mount ( FlareErrorBoundary , {
489+ props : { beforeSubmit } ,
490+ slots : {
491+ default : ( ) => h ( ThrowingComponent ) ,
492+ fallback : ( props : { componentHierarchy : string [ ] } ) =>
493+ h ( 'span' , { class : 'hierarchy' } , props . componentHierarchy . join ( ',' ) ) ,
494+ } ,
495+ } ) ;
496+
497+ await nextTick ( ) ;
498+
499+ expect ( wrapper . find ( '.hierarchy' ) . text ( ) ) . toBe ( 'Custom,Modified' ) ;
500+ } ) ;
501+
502+ test ( 'beforeEvaluate throwing prevents reporting and propagates' , ( ) => {
503+ const beforeEvaluate = vi . fn ( ( ) => {
504+ throw new Error ( 'beforeEvaluate error' ) ;
505+ } ) ;
506+
507+ expect ( ( ) => {
508+ mount ( FlareErrorBoundary , {
509+ props : { beforeEvaluate } ,
510+ slots : {
511+ default : ( ) => h ( ThrowingComponent ) ,
512+ fallback : ( ) => h ( 'div' , 'Error' ) ,
513+ } ,
514+ } ) ;
515+ } ) . toThrow ( 'beforeEvaluate error' ) ;
516+
517+ expect ( beforeEvaluate ) . toHaveBeenCalledOnce ( ) ;
518+ expect ( mockReport ) . not . toHaveBeenCalled ( ) ;
519+ } ) ;
520+
521+ test ( 'beforeSubmit throwing prevents reporting and propagates' , ( ) => {
522+ const beforeSubmit = vi . fn ( ( ) => {
523+ throw new Error ( 'beforeSubmit error' ) ;
524+ } ) ;
525+
526+ expect ( ( ) => {
527+ mount ( FlareErrorBoundary , {
528+ props : { beforeSubmit } ,
529+ slots : {
530+ default : ( ) => h ( ThrowingComponent ) ,
531+ fallback : ( ) => h ( 'div' , 'Error' ) ,
532+ } ,
533+ } ) ;
534+ } ) . toThrow ( 'beforeSubmit error' ) ;
535+
536+ expect ( beforeSubmit ) . toHaveBeenCalledOnce ( ) ;
537+ expect ( mockReport ) . not . toHaveBeenCalled ( ) ;
538+ } ) ;
539+
540+ test ( 'afterSubmit throwing propagates after reporting' , ( ) => {
541+ const afterSubmit = vi . fn ( ( ) => {
542+ throw new Error ( 'afterSubmit error' ) ;
543+ } ) ;
544+
545+ expect ( ( ) => {
546+ mount ( FlareErrorBoundary , {
547+ props : { afterSubmit } ,
548+ slots : {
549+ default : ( ) => h ( ThrowingComponent ) ,
550+ fallback : ( ) => h ( 'div' , 'Error' ) ,
551+ } ,
552+ } ) ;
553+ } ) . toThrow ( 'afterSubmit error' ) ;
554+
555+ expect ( afterSubmit ) . toHaveBeenCalledOnce ( ) ;
556+ expect ( mockReport ) . toHaveBeenCalledOnce ( ) ;
557+ } ) ;
558+
559+ test ( 'callbacks fire again after reset and re-throw' , async ( ) => {
560+ const beforeEvaluate = vi . fn ( ) ;
561+ const beforeSubmit = vi . fn ( ( params : { context : FlareVueContext } ) => params . context ) ;
562+ const afterSubmit = vi . fn ( ) ;
563+
564+ const wrapper = mount ( FlareErrorBoundary , {
565+ props : { beforeEvaluate, beforeSubmit, afterSubmit } ,
566+ slots : {
567+ default : ( ) => h ( ThrowingComponent ) ,
568+ fallback : ( props : { resetErrorBoundary : ( ) => void } ) =>
569+ h ( 'button' , { onClick : props . resetErrorBoundary } , 'Reset' ) ,
570+ } ,
571+ } ) ;
572+
573+ await nextTick ( ) ;
574+
575+ expect ( beforeEvaluate ) . toHaveBeenCalledOnce ( ) ;
576+ expect ( beforeSubmit ) . toHaveBeenCalledOnce ( ) ;
577+ expect ( afterSubmit ) . toHaveBeenCalledOnce ( ) ;
578+
579+ await wrapper . find ( 'button' ) . trigger ( 'click' ) ;
580+ await nextTick ( ) ;
581+
582+ expect ( beforeEvaluate ) . toHaveBeenCalledTimes ( 2 ) ;
583+ expect ( beforeSubmit ) . toHaveBeenCalledTimes ( 2 ) ;
584+ expect ( afterSubmit ) . toHaveBeenCalledTimes ( 2 ) ;
585+ } ) ;
296586} ) ;
0 commit comments