@@ -485,6 +485,197 @@ describe('EffectComposer', () => {
485485 } )
486486 } )
487487
488+ describe ( 'renderer state restoration' , ( ) => {
489+ it ( 'restores autoClear to its previous value once the composer is fully unmounted' , async ( ) => {
490+ const gl = root . render ( null ) . getState ( ) . gl
491+ gl . autoClear = true // known baseline, independent of whatever earlier tests in this file left behind
492+
493+ const ref = React . createRef < EffectComposerImpl > ( )
494+
495+ await React . act ( async ( ) =>
496+ root . render (
497+ < EffectComposer ref = { ref } >
498+ < WrappedEffectA />
499+ </ EffectComposer >
500+ )
501+ )
502+
503+ await waitForComposer ( ref )
504+ expect ( gl . autoClear ) . toBe ( false )
505+
506+ await React . act ( async ( ) => root . render ( null ) )
507+
508+ expect ( gl . autoClear ) . toBe ( true )
509+ } )
510+
511+ it ( 'keeps autoClear disabled while a sibling composer on the same renderer is still mounted, regardless of unmount order' , async ( ) => {
512+ const gl = root . render ( null ) . getState ( ) . gl
513+ gl . autoClear = true
514+
515+ const refA = React . createRef < EffectComposerImpl > ( )
516+ const refB = React . createRef < EffectComposerImpl > ( )
517+
518+ await React . act ( async ( ) =>
519+ root . render (
520+ < >
521+ < EffectComposer key = "a" ref = { refA } >
522+ < WrappedEffectA />
523+ </ EffectComposer >
524+ < EffectComposer key = "b" ref = { refB } >
525+ < WrappedEffectB />
526+ </ EffectComposer >
527+ </ >
528+ )
529+ )
530+
531+ await waitForComposer ( refA )
532+ await waitForComposer ( refB )
533+ expect ( gl . autoClear ) . toBe ( false )
534+
535+ // Unmount the first composer only (key "a" drops out of the tree) -
536+ // the second is still relying on autoClear staying off, so this must
537+ // not restore it yet. Keying both is essential here: without it,
538+ // React would match by position and reuse "a"'s instance in place
539+ // (just updating its props to "b"'s), unmounting the wrong one.
540+ await React . act ( async ( ) =>
541+ root . render (
542+ < EffectComposer key = "b" ref = { refB } >
543+ < WrappedEffectB />
544+ </ EffectComposer >
545+ )
546+ )
547+
548+ expect ( gl . autoClear ) . toBe ( false )
549+
550+ // Now the last one goes too - only now should it actually restore.
551+ await React . act ( async ( ) => root . render ( null ) )
552+
553+ expect ( gl . autoClear ) . toBe ( true )
554+ } )
555+
556+ it ( 'restores toneMapping to its previous value once the composer is fully unmounted' , async ( ) => {
557+ const gl = root . render ( null ) . getState ( ) . gl
558+ gl . toneMapping = THREE . ACESFilmicToneMapping // known baseline, distinct from NoToneMapping
559+
560+ const ref = React . createRef < EffectComposerImpl > ( )
561+
562+ await React . act ( async ( ) =>
563+ root . render (
564+ < EffectComposer ref = { ref } >
565+ < WrappedEffectA />
566+ </ EffectComposer >
567+ )
568+ )
569+
570+ await waitForComposer ( ref )
571+ expect ( gl . toneMapping ) . toBe ( THREE . NoToneMapping )
572+
573+ await React . act ( async ( ) => root . render ( null ) )
574+
575+ expect ( gl . toneMapping ) . toBe ( THREE . ACESFilmicToneMapping )
576+ } )
577+
578+ it ( 'keeps toneMapping disabled while a sibling composer on the same renderer is still mounted, regardless of unmount order' , async ( ) => {
579+ const gl = root . render ( null ) . getState ( ) . gl
580+ gl . toneMapping = THREE . ACESFilmicToneMapping
581+
582+ const refA = React . createRef < EffectComposerImpl > ( )
583+ const refB = React . createRef < EffectComposerImpl > ( )
584+
585+ await React . act ( async ( ) =>
586+ root . render (
587+ < >
588+ < EffectComposer key = "a" ref = { refA } >
589+ < WrappedEffectA />
590+ </ EffectComposer >
591+ < EffectComposer key = "b" ref = { refB } >
592+ < WrappedEffectB />
593+ </ EffectComposer >
594+ </ >
595+ )
596+ )
597+
598+ await waitForComposer ( refA )
599+ await waitForComposer ( refB )
600+ expect ( gl . toneMapping ) . toBe ( THREE . NoToneMapping )
601+
602+ // Unmount the first composer only - the second still relies on
603+ // toneMapping staying off, so this must not restore it yet.
604+ await React . act ( async ( ) =>
605+ root . render (
606+ < EffectComposer key = "b" ref = { refB } >
607+ < WrappedEffectB />
608+ </ EffectComposer >
609+ )
610+ )
611+
612+ expect ( gl . toneMapping ) . toBe ( THREE . NoToneMapping )
613+
614+ // Now the last one goes too - only now should it actually restore.
615+ await React . act ( async ( ) => root . render ( null ) )
616+
617+ expect ( gl . toneMapping ) . toBe ( THREE . ACESFilmicToneMapping )
618+ } )
619+
620+ it ( 'does not clobber a manual autoClear change made while the composer was mounted' , async ( ) => {
621+ const gl = root . render ( null ) . getState ( ) . gl
622+ // Pre-mount baseline is false (not the usual true) specifically so
623+ // it differs from the manual override below - autoClear only has two
624+ // states, so this is the only way to make an unconditional restore-
625+ // to-original and a "preserve the manual change" outcome observably
626+ // different from each other.
627+ gl . autoClear = false
628+
629+ const ref = React . createRef < EffectComposerImpl > ( )
630+
631+ await React . act ( async ( ) =>
632+ root . render (
633+ < EffectComposer ref = { ref } >
634+ < WrappedEffectA />
635+ </ EffectComposer >
636+ )
637+ )
638+
639+ await waitForComposer ( ref )
640+ expect ( gl . autoClear ) . toBe ( false )
641+
642+ // Something outside this component takes manual control while the
643+ // composer happens to still be mounted - a deliberate, more current
644+ // intent than whatever we captured before mount.
645+ gl . autoClear = true
646+
647+ await React . act ( async ( ) => root . render ( null ) )
648+
649+ // Must stay what the manual override set it to, not get silently
650+ // reset back to the pre-mount value we originally captured (false).
651+ expect ( gl . autoClear ) . toBe ( true )
652+ } )
653+
654+ it ( 'does not clobber a manual toneMapping change made while the composer was mounted' , async ( ) => {
655+ const gl = root . render ( null ) . getState ( ) . gl
656+ gl . toneMapping = THREE . ACESFilmicToneMapping
657+
658+ const ref = React . createRef < EffectComposerImpl > ( )
659+
660+ await React . act ( async ( ) =>
661+ root . render (
662+ < EffectComposer ref = { ref } >
663+ < WrappedEffectA />
664+ </ EffectComposer >
665+ )
666+ )
667+
668+ await waitForComposer ( ref )
669+ expect ( gl . toneMapping ) . toBe ( THREE . NoToneMapping )
670+
671+ gl . toneMapping = THREE . ReinhardToneMapping
672+
673+ await React . act ( async ( ) => root . render ( null ) )
674+
675+ expect ( gl . toneMapping ) . toBe ( THREE . ReinhardToneMapping )
676+ } )
677+ } )
678+
488679 describe ( 'StrictMode' , ( ) => {
489680 it ( 'preserves effects after the StrictMode fake-unmount/remount cycle' , async ( ) => {
490681 const ref = React . createRef < EffectComposerImpl > ( )
0 commit comments