@@ -17,7 +17,11 @@ function emptyFunction() {}
1717describe ( 'ReactDOMInput' , ( ) => {
1818 let React ;
1919 let ReactDOM ;
20+ let ReactDOMClient ;
2021 let ReactDOMServer ;
22+ let Scheduler ;
23+ let act ;
24+ let assertLog ;
2125 let setUntrackedValue ;
2226 let setUntrackedChecked ;
2327 let container ;
@@ -87,7 +91,11 @@ describe('ReactDOMInput', () => {
8791
8892 React = require ( 'react' ) ;
8993 ReactDOM = require ( 'react-dom' ) ;
94+ ReactDOMClient = require ( 'react-dom/client' ) ;
9095 ReactDOMServer = require ( 'react-dom/server' ) ;
96+ Scheduler = require ( 'scheduler' ) ;
97+ act = require ( 'internal-test-utils' ) . act ;
98+ assertLog = require ( 'internal-test-utils' ) . assertLog ;
9199
92100 container = document . createElement ( 'div' ) ;
93101 document . body . appendChild ( container ) ;
@@ -1235,6 +1243,161 @@ describe('ReactDOMInput', () => {
12351243 assertInputTrackingIsCurrent ( container ) ;
12361244 } ) ;
12371245
1246+ it ( 'should hydrate controlled radio buttons' , async ( ) => {
1247+ function App ( ) {
1248+ let [ current , setCurrent ] = React . useState ( 'a' ) ;
1249+ return (
1250+ < >
1251+ < input
1252+ type = "radio"
1253+ name = "fruit"
1254+ checked = { current === 'a' }
1255+ onChange = { ( ) => {
1256+ Scheduler . log ( 'click a' ) ;
1257+ setCurrent ( 'a' ) ;
1258+ } }
1259+ />
1260+ < input
1261+ type = "radio"
1262+ name = "fruit"
1263+ checked = { current === 'b' }
1264+ onChange = { ( ) => {
1265+ Scheduler . log ( 'click b' ) ;
1266+ setCurrent ( 'b' ) ;
1267+ } }
1268+ />
1269+ < input
1270+ type = "radio"
1271+ name = "fruit"
1272+ checked = { current === 'c' }
1273+ onChange = { ( ) => {
1274+ Scheduler . log ( 'click c' ) ;
1275+ // Let's say the user can't pick C
1276+ } }
1277+ />
1278+ </ >
1279+ ) ;
1280+ }
1281+ const html = ReactDOMServer . renderToString ( < App /> ) ;
1282+ container . innerHTML = html ;
1283+ const [ a , b , c ] = container . querySelectorAll ( 'input' ) ;
1284+ expect ( a . checked ) . toBe ( true ) ;
1285+ expect ( b . checked ) . toBe ( false ) ;
1286+ expect ( c . checked ) . toBe ( false ) ;
1287+ expect ( isCheckedDirty ( a ) ) . toBe ( false ) ;
1288+ expect ( isCheckedDirty ( b ) ) . toBe ( false ) ;
1289+ expect ( isCheckedDirty ( c ) ) . toBe ( false ) ;
1290+
1291+ // Click on B before hydrating
1292+ b . checked = true ;
1293+ expect ( isCheckedDirty ( a ) ) . toBe ( true ) ;
1294+ expect ( isCheckedDirty ( b ) ) . toBe ( true ) ;
1295+ expect ( isCheckedDirty ( c ) ) . toBe ( false ) ;
1296+
1297+ await act ( async ( ) => {
1298+ ReactDOMClient . hydrateRoot ( container , < App /> ) ;
1299+ } ) ;
1300+
1301+ // Currently, we don't fire onChange when hydrating
1302+ assertLog ( [ ] ) ;
1303+ // Strangely, we leave `b` checked even though we rendered A with
1304+ // checked={true} and B with checked={false}. Arguably this is a bug.
1305+ expect ( a . checked ) . toBe ( false ) ;
1306+ expect ( b . checked ) . toBe ( true ) ;
1307+ expect ( c . checked ) . toBe ( false ) ;
1308+ expect ( isCheckedDirty ( a ) ) . toBe ( true ) ;
1309+ expect ( isCheckedDirty ( b ) ) . toBe ( true ) ;
1310+ expect ( isCheckedDirty ( c ) ) . toBe ( true ) ;
1311+ assertInputTrackingIsCurrent ( container ) ;
1312+
1313+ // If we click on C now though...
1314+ await act ( async ( ) => {
1315+ setUntrackedChecked . call ( c , true ) ;
1316+ dispatchEventOnNode ( c , 'click' ) ;
1317+ } ) ;
1318+
1319+ // then since C's onClick doesn't set state, A becomes rechecked.
1320+ assertLog ( [ 'click c' ] ) ;
1321+ expect ( a . checked ) . toBe ( true ) ;
1322+ expect ( b . checked ) . toBe ( false ) ;
1323+ expect ( c . checked ) . toBe ( false ) ;
1324+ expect ( isCheckedDirty ( a ) ) . toBe ( true ) ;
1325+ expect ( isCheckedDirty ( b ) ) . toBe ( true ) ;
1326+ expect ( isCheckedDirty ( c ) ) . toBe ( true ) ;
1327+ assertInputTrackingIsCurrent ( container ) ;
1328+ } ) ;
1329+
1330+ it ( 'should hydrate uncontrolled radio buttons' , async ( ) => {
1331+ function App ( ) {
1332+ return (
1333+ < >
1334+ < input
1335+ type = "radio"
1336+ name = "fruit"
1337+ defaultChecked = { true }
1338+ onChange = { ( ) => Scheduler . log ( 'click a' ) }
1339+ />
1340+ < input
1341+ type = "radio"
1342+ name = "fruit"
1343+ defaultChecked = { false }
1344+ onChange = { ( ) => Scheduler . log ( 'click b' ) }
1345+ />
1346+ < input
1347+ type = "radio"
1348+ name = "fruit"
1349+ defaultChecked = { false }
1350+ onChange = { ( ) => Scheduler . log ( 'click c' ) }
1351+ />
1352+ </ >
1353+ ) ;
1354+ }
1355+ const html = ReactDOMServer . renderToString ( < App /> ) ;
1356+ container . innerHTML = html ;
1357+ const [ a , b , c ] = container . querySelectorAll ( 'input' ) ;
1358+ expect ( a . checked ) . toBe ( true ) ;
1359+ expect ( b . checked ) . toBe ( false ) ;
1360+ expect ( c . checked ) . toBe ( false ) ;
1361+ expect ( isCheckedDirty ( a ) ) . toBe ( false ) ;
1362+ expect ( isCheckedDirty ( b ) ) . toBe ( false ) ;
1363+ expect ( isCheckedDirty ( c ) ) . toBe ( false ) ;
1364+
1365+ // Click on B before hydrating
1366+ b . checked = true ;
1367+ expect ( isCheckedDirty ( a ) ) . toBe ( true ) ;
1368+ expect ( isCheckedDirty ( b ) ) . toBe ( true ) ;
1369+ expect ( isCheckedDirty ( c ) ) . toBe ( false ) ;
1370+
1371+ await act ( async ( ) => {
1372+ ReactDOMClient . hydrateRoot ( container , < App /> ) ;
1373+ } ) ;
1374+
1375+ // Currently, we don't fire onChange when hydrating
1376+ assertLog ( [ ] ) ;
1377+ expect ( a . checked ) . toBe ( false ) ;
1378+ expect ( b . checked ) . toBe ( true ) ;
1379+ expect ( c . checked ) . toBe ( false ) ;
1380+ expect ( isCheckedDirty ( a ) ) . toBe ( true ) ;
1381+ expect ( isCheckedDirty ( b ) ) . toBe ( true ) ;
1382+ expect ( isCheckedDirty ( c ) ) . toBe ( true ) ;
1383+ assertInputTrackingIsCurrent ( container ) ;
1384+
1385+ // Click back to A
1386+ await act ( async ( ) => {
1387+ setUntrackedChecked . call ( a , true ) ;
1388+ dispatchEventOnNode ( a , 'click' ) ;
1389+ } ) ;
1390+
1391+ assertLog ( [ 'click a' ] ) ;
1392+ expect ( a . checked ) . toBe ( true ) ;
1393+ expect ( b . checked ) . toBe ( false ) ;
1394+ expect ( c . checked ) . toBe ( false ) ;
1395+ expect ( isCheckedDirty ( a ) ) . toBe ( true ) ;
1396+ expect ( isCheckedDirty ( b ) ) . toBe ( true ) ;
1397+ expect ( isCheckedDirty ( c ) ) . toBe ( true ) ;
1398+ assertInputTrackingIsCurrent ( container ) ;
1399+ } ) ;
1400+
12381401 it ( 'should check the correct radio when the selected name moves' , ( ) => {
12391402 class App extends React . Component {
12401403 state = {
0 commit comments