@@ -694,17 +694,22 @@ describe('enhanced report tables: stateful widgets survive interactions', () =>
694694 hideAllPopups ( ) ;
695695 } ) ;
696696
697- test ( 'a payload change destroys the previous charts before the markup swap and re-creates them' , async ( ) => {
697+ test ( 'a payload change destroys the previous charts before the loading markup and re-creates them' , async ( ) => {
698698 // Every Chart.js construction/teardown, in order, so the test can pin
699- // that the old instances are released before the replacement render
699+ // that the old instances are released before the fetch effect clears
700+ // the payload (loading placeholder) and before the replacement render
700701 // builds fresh charts on the new canvases.
701702 const events = [ ] ;
702703 const instances = [ ] ;
703704 window . Chart = function ( ) {
704705 const chart = {
705706 destroyed : false ,
706707 update ( ) { } ,
707- destroy ( ) { chart . destroyed = true ; events . push ( 'destroy' ) ; }
708+ destroy ( ) {
709+ chart . destroyed = true ;
710+ const canvas = document . querySelector ( 'canvas#chart_GOD' ) ;
711+ events . push ( canvas && canvas . isConnected ? 'destroy:attached' : 'destroy:detached' ) ;
712+ }
708713 } ;
709714 instances . push ( chart ) ;
710715 events . push ( 'create' ) ;
@@ -717,29 +722,93 @@ describe('enhanced report tables: stateful widgets survive interactions', () =>
717722 const firstRenderCreates = instances . length ;
718723 expect ( firstRenderCreates ) . toBeGreaterThan ( 0 ) ;
719724
720- // Switching branches fetches a new payload: the replacement render must
721- // tear the old Chart.js instances down (they would otherwise outlive
722- // their removed canvases) and then re-create charts for the fresh DOM.
725+ // Switching branches re-runs the fetch effect: it must release the old
726+ // Chart.js instances (they would otherwise outlive their removed
727+ // canvases) before the loading placeholder replaces the report markup,
728+ // and the fresh render then builds new charts for the new canvases.
723729 utils . rerender ( _jsx ( ReportView , {
724730 username : 'junit-team' ,
725731 repository : 'junit4' ,
726732 branch : 'develop' ,
727733 widgetSettleMs : 25
728734 } ) ) ;
729735 // The rerender passes through loading/graft phases that reuse or drop
730- // the old canvas node, so wait on the destruction itself settling.
736+ // the old canvas node; the destroys happen up front at the fetch
737+ // cycle's start, so wait on them and then on the fresh render.
738+ await waitFor ( ( ) => {
739+ expect ( events . filter ( e => e !== 'create' ) . length ) . toBe ( firstRenderCreates ) ;
740+ } ) ;
731741 await waitFor ( ( ) => {
732- expect ( events . filter ( e => e === 'destroy' ) . length ) . toBe ( firstRenderCreates ) ;
742+ expect ( instances . length ) . toBe ( 2 * firstRenderCreates ) ;
733743 } ) ;
734744 expect ( utils . container . querySelector ( 'canvas#chart_GOD' ) ) . not . toBe ( firstCanvas ) ;
735745
736746 expect ( instances . slice ( 0 , firstRenderCreates ) . every ( c => c . destroyed ) ) . toBe ( true ) ;
737- // All destroys precede every replacement construction.
738- const destroys = events . slice ( firstRenderCreates , instances . length ) ;
739- expect ( destroys . every ( e => e === 'destroy' ) ) . toBe ( true ) ;
747+ // Destroyed while the canvases were still attached: before the loading
748+ // markup swap, not after the report markup was thrown away.
749+ const destroys = events . filter ( e => e !== 'create' ) ;
750+ expect ( destroys . every ( e => e === 'destroy:attached' ) ) . toBe ( true ) ;
751+ expect ( events ) . not . toContain ( 'destroy:detached' ) ;
752+ // ...and all destroys precede every replacement construction.
753+ expect ( events . findIndex ( e => e !== 'create' ) ) . toBeLessThan ( events . lastIndexOf ( 'create' ) ) ;
740754 // enhanceReport still builds charts for the replacement canvases.
741755 expect ( instances . length ) . toBe ( 2 * firstRenderCreates ) ;
742756 } , SLOW_TEST_MS ) ;
757+
758+ test ( 'unmounting the report destroys its live charts' , async ( ) => {
759+ const instances = [ ] ;
760+ window . Chart = function ( ) {
761+ const chart = {
762+ destroyed : false ,
763+ update ( ) { } ,
764+ destroy ( ) { chart . destroyed = true ; }
765+ } ;
766+ instances . push ( chart ) ;
767+ return chart ;
768+ } ;
769+ markWidgetReady ( 'chart' ) ;
770+ respondJsonFor ( sampleJson ) ;
771+ const utils = await renderReport ( ) ;
772+ expect ( instances . length ) . toBeGreaterThan ( 0 ) ;
773+ expect ( instances . every ( c => c . destroyed ) ) . toBe ( false ) ;
774+
775+ // The fetch effect's cleanup aborts the in-flight request; it must
776+ // release the report's Chart.js instances as well, since the canvases
777+ // go away with the unmounted container.
778+ utils . unmount ( ) ;
779+ expect ( instances . every ( c => c . destroyed ) ) . toBe ( true ) ;
780+ } ) ;
781+
782+ test ( 'a refetch destroys the previous charts up front, even if it never delivers a payload' , async ( ) => {
783+ const instances = [ ] ;
784+ window . Chart = function ( ) {
785+ const chart = {
786+ destroyed : false ,
787+ update ( ) { } ,
788+ destroy ( ) { chart . destroyed = true ; }
789+ } ;
790+ instances . push ( chart ) ;
791+ return chart ;
792+ } ;
793+ markWidgetReady ( 'chart' ) ;
794+ respondJsonFor ( sampleJson ) ;
795+ const utils = await renderReport ( ) ;
796+ expect ( instances . length ) . toBeGreaterThan ( 0 ) ;
797+ expect ( instances . every ( c => c . destroyed ) ) . toBe ( false ) ;
798+
799+ // A branch switch whose fetch never resolves: the old Chart.js
800+ // instances must be released when the fetch cycle starts — before the
801+ // payload is cleared and the loading markup replaces the report — not
802+ // wait for a replacement payload that may never arrive.
803+ mockFetch . mockImplementation ( ( ) => new Promise ( ( ) => { } ) ) ;
804+ utils . rerender ( _jsx ( ReportView , {
805+ username : 'junit-team' ,
806+ repository : 'junit4' ,
807+ branch : 'develop' ,
808+ widgetSettleMs : 25
809+ } ) ) ;
810+ expect ( instances . every ( c => c . destroyed ) ) . toBe ( true ) ;
811+ } ) ;
743812} ) ;
744813
745814async function hideAllPopups ( ) {
0 commit comments