@@ -866,4 +866,156 @@ describe('renderer', () => {
866866 await act ( async ( ) => root . render ( < TestComponent /> ) )
867867 await act ( async ( ) => updateSynchronously ( 1 ) )
868868 } )
869+
870+ //* Props vs Setters Conflict Resolution Tests ==============================
871+ // These tests verify that imperative setter changes (setDpr, setFrameloop, etc.)
872+ // are preserved when Canvas re-configures (e.g., on resize)
873+
874+ it ( 'should preserve setFrameloop changes across re-configure (no prop passed)' , async ( ) => {
875+ // Test scenario: Canvas has no frameloop prop (defaults to 'always')
876+ // User calls setFrameloop('demand'), then Canvas re-configures (resize)
877+ // Frameloop should stay 'demand', not reset to default
878+
879+ // Initial configure with default frameloop (no prop passed explicitly uses 'always')
880+ const store = await act ( async ( ) =>
881+ ( await root . configure ( { size : { width : 100 , height : 100 , top : 0 , left : 0 } } ) ) . render ( < group /> ) ,
882+ )
883+ const state = store . getState ( )
884+
885+ // Verify initial state - frameloop should be 'always' (the default)
886+ expect ( state . frameloop ) . toBe ( 'always' )
887+
888+ // User imperatively changes frameloop to 'demand'
889+ await act ( async ( ) => state . setFrameloop ( 'demand' ) )
890+ expect ( store . getState ( ) . frameloop ) . toBe ( 'demand' )
891+
892+ // Simulate resize by re-configuring with new size (but same frameloop prop)
893+ // This is what happens when the Canvas container resizes
894+ await act ( async ( ) => root . configure ( { size : { width : 200 , height : 200 , top : 0 , left : 0 } } ) )
895+
896+ // Size should have updated
897+ expect ( store . getState ( ) . size . width ) . toBe ( 200 )
898+ expect ( store . getState ( ) . size . height ) . toBe ( 200 )
899+
900+ // Frameloop should STILL be 'demand' - not reset to 'always'
901+ expect ( store . getState ( ) . frameloop ) . toBe ( 'demand' )
902+ } )
903+
904+ it ( 'should preserve setDpr changes from child component across re-configure' , async ( ) => {
905+ // Test scenario: Canvas has dpr={[1, 2]} prop
906+ // A child component calls setDpr(1) after mount
907+ // Canvas re-configures (resize) - dpr should stay at 1, not reset to [1, 2]
908+
909+ let setDprFromComponent : ( dpr : number ) => void = ( ) => { }
910+
911+ function DprSetter ( ) {
912+ const state = useThree ( )
913+ React . useEffect ( ( ) => {
914+ // Expose setDpr for test to call
915+ setDprFromComponent = ( dpr : number ) => state . setDpr ( dpr )
916+ } , [ state ] )
917+ return null
918+ }
919+
920+ // Initial configure with dpr={[1, 2]}
921+ const store = await act ( async ( ) =>
922+ (
923+ await root . configure ( {
924+ dpr : [ 1 , 2 ] ,
925+ size : { width : 100 , height : 100 , top : 0 , left : 0 } ,
926+ } )
927+ ) . render ( < DprSetter /> ) ,
928+ )
929+
930+ // Verify initial dpr was applied (calculateDpr picks based on device pixel ratio)
931+ const initialDpr = store . getState ( ) . viewport . dpr
932+ expect ( typeof initialDpr ) . toBe ( 'number' )
933+
934+ // Child component imperatively changes dpr to 1
935+ await act ( async ( ) => setDprFromComponent ( 1 ) )
936+ expect ( store . getState ( ) . viewport . dpr ) . toBe ( 1 )
937+
938+ // Simulate resize by re-configuring with new size (but same dpr prop)
939+ await act ( async ( ) =>
940+ root . configure ( {
941+ dpr : [ 1 , 2 ] ,
942+ size : { width : 200 , height : 200 , top : 0 , left : 0 } ,
943+ } ) ,
944+ )
945+
946+ // Size should have updated
947+ expect ( store . getState ( ) . size . width ) . toBe ( 200 )
948+ expect ( store . getState ( ) . size . height ) . toBe ( 200 )
949+
950+ // DPR should STILL be 1 - not reset based on [1, 2] range
951+ expect ( store . getState ( ) . viewport . dpr ) . toBe ( 1 )
952+ } )
953+
954+ it ( 'should update dpr when the PROP changes (controlled mode)' , async ( ) => {
955+ // Test scenario: Canvas dpr prop actually changes from [1, 2] to 1
956+ // In this case, the new prop value should be applied
957+
958+ const store = await act ( async ( ) =>
959+ (
960+ await root . configure ( {
961+ dpr : [ 1 , 2 ] ,
962+ size : { width : 100 , height : 100 , top : 0 , left : 0 } ,
963+ } )
964+ ) . render ( < group /> ) ,
965+ )
966+
967+ const initialDpr = store . getState ( ) . viewport . dpr
968+
969+ // Re-configure with a DIFFERENT dpr prop value
970+ await act ( async ( ) =>
971+ root . configure ( {
972+ dpr : 0.5 , // Changed from [1, 2] to 0.5
973+ size : { width : 100 , height : 100 , top : 0 , left : 0 } ,
974+ } ) ,
975+ )
976+
977+ // DPR should now be 0.5 (the new prop value was applied)
978+ expect ( store . getState ( ) . viewport . dpr ) . toBe ( 0.5 )
979+ } )
980+
981+ it ( 'should preserve multiple setter changes across re-configure' , async ( ) => {
982+ // Test scenario: Both dpr and frameloop are changed imperatively
983+ // Both should be preserved after re-configure
984+
985+ const store = await act ( async ( ) =>
986+ (
987+ await root . configure ( {
988+ dpr : [ 1 , 2 ] ,
989+ frameloop : 'always' ,
990+ size : { width : 100 , height : 100 , top : 0 , left : 0 } ,
991+ } )
992+ ) . render ( < group /> ) ,
993+ )
994+
995+ const state = store . getState ( )
996+
997+ // User imperatively changes both dpr and frameloop
998+ await act ( async ( ) => {
999+ state . setDpr ( 1 )
1000+ state . setFrameloop ( 'demand' )
1001+ } )
1002+
1003+ expect ( store . getState ( ) . viewport . dpr ) . toBe ( 1 )
1004+ expect ( store . getState ( ) . frameloop ) . toBe ( 'demand' )
1005+
1006+ // Simulate resize by re-configuring with same props
1007+ await act ( async ( ) =>
1008+ root . configure ( {
1009+ dpr : [ 1 , 2 ] ,
1010+ frameloop : 'always' ,
1011+ size : { width : 200 , height : 200 , top : 0 , left : 0 } ,
1012+ } ) ,
1013+ )
1014+
1015+ // Both values should be preserved
1016+ expect ( store . getState ( ) . viewport . dpr ) . toBe ( 1 )
1017+ expect ( store . getState ( ) . frameloop ) . toBe ( 'demand' )
1018+ // Size should have updated
1019+ expect ( store . getState ( ) . size . width ) . toBe ( 200 )
1020+ } )
8691021} )
0 commit comments