18
let ReactDOM;
19
let ReactDOMServer;
20
let ReactTestUtils;
21
+let Scheduler;
22
let useState;
23
let useReducer;
24
let useEffect;
29
let useImperativeHandle;
30
let useLayoutEffect;
31
let useDebugValue;
32
+let useOpaqueIdentifier;
33
let forwardRef;
34
let yieldedValues;
35
let yieldValue;
41
42
ReactFeatureFlags = require('shared/ReactFeatureFlags');
43
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
44
+ ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
45
React = require('react');
46
ReactDOM = require('react-dom');
47
ReactDOMServer = require('react-dom/server');
48
ReactTestUtils = require('react-dom/test-utils');
49
+ Scheduler = require('scheduler');
50
useState = React.useState;
51
useReducer = React.useReducer;
52
useEffect = React.useEffect;
57
useDebugValue = React.useDebugValue;
58
useImperativeHandle = React.useImperativeHandle;
59
useLayoutEffect = React.useLayoutEffect;
60
+ useOpaqueIdentifier = React.unstable_useOpaqueIdentifier;
61
forwardRef = React.forwardRef;
62
63
yieldedValues = [];
83
itRenders,
84
itThrowsWhenRendering,
85
serverRender,
86
+ streamRender,
87
+ clientCleanRender,
88
+ clientRenderOnServerString,
89
} = ReactDOMServerIntegrationUtils(initModules);
90
91
describe('ReactDOMServerHooks', () => {
842
expect(domNode2.textContent).toEqual('42');
843
});
844
});
845
+
846
+ if (__EXPERIMENTAL__) {
847
+ describe('useOpaqueIdentifier', () => {
848
+ it('generates unique ids for server string render', async () => {
849
+ function App(props) {
850
+ const idOne = useOpaqueIdentifier();
851
+ const idTwo = useOpaqueIdentifier();
852
+ return (
853
+ <div>
854
+ <div aria-labelledby={idOne} />
855
+ <div id={idOne} />
856
+ <span aria-labelledby={idTwo} />
857
+ <span id={idTwo} />
858
+ </div>
859
+ );
860
+ }
861
+
862
+ const domNode = await serverRender(<App />);
863
+ expect(domNode.children.length).toEqual(4);
864
+ expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual(
865
+ domNode.children[1].getAttribute('id'),
866
+ );
867
+ expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual(
868
+ domNode.children[3].getAttribute('id'),
869
+ );
870
+ expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual(
871
+ domNode.children[2].getAttribute('aria-labelledby'),
872
+ );
873
+ expect(
874
+ domNode.children[0].getAttribute('aria-labelledby'),
875
+ ).not.toBeNull();
876
+ expect(
877
+ domNode.children[2].getAttribute('aria-labelledby'),
878
+ ).not.toBeNull();
879
+ });
880
+
881
+ it('generates unique ids for server stream render', async () => {
882
+ function App(props) {
883
+ const idOne = useOpaqueIdentifier();
884
+ const idTwo = useOpaqueIdentifier();
885
+ return (
886
+ <div>
887
+ <div aria-labelledby={idOne} />
888
+ <div id={idOne} />
889
+ <span aria-labelledby={idTwo} />
890
+ <span id={idTwo} />
891
+ </div>
892
+ );
893
+ }
894
+
895
+ const domNode = await streamRender(<App />);
896
+ expect(domNode.children.length).toEqual(4);
897
+ expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual(
898
+ domNode.children[1].getAttribute('id'),
899
+ );
900
+ expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual(
901
+ domNode.children[3].getAttribute('id'),
902
+ );
903
+ expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual(
904
+ domNode.children[2].getAttribute('aria-labelledby'),
905
+ );
906
+ expect(
907
+ domNode.children[0].getAttribute('aria-labelledby'),
908
+ ).not.toBeNull();
909
+ expect(
910
+ domNode.children[2].getAttribute('aria-labelledby'),
911
+ ).not.toBeNull();
912
+ });
913
+
914
+ it('generates unique ids for client render', async () => {
915
+ function App(props) {
916
+ const idOne = useOpaqueIdentifier();
917
+ const idTwo = useOpaqueIdentifier();
918
+ return (
919
+ <div>
920
+ <div aria-labelledby={idOne} />
921
+ <div id={idOne} />
922
+ <span aria-labelledby={idTwo} />
923
+ <span id={idTwo} />
924
+ </div>
925
+ );
926
+ }
927
+
928
+ const domNode = await clientCleanRender(<App />);
929
+ expect(domNode.children.length).toEqual(4);
930
+ expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual(
931
+ domNode.children[1].getAttribute('id'),
932
+ );
933
+ expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual(
934
+ domNode.children[3].getAttribute('id'),
935
+ );
936
+ expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual(
937
+ domNode.children[2].getAttribute('aria-labelledby'),
938
+ );
939
+ expect(
940
+ domNode.children[0].getAttribute('aria-labelledby'),
941
+ ).not.toBeNull();
942
+ expect(
943
+ domNode.children[2].getAttribute('aria-labelledby'),
944
+ ).not.toBeNull();
945
+ });
946
+
947
+ it('generates unique ids for client render on good server markup', async () => {
948
+ function App(props) {
949
+ const idOne = useOpaqueIdentifier();
950
+ const idTwo = useOpaqueIdentifier();
951
+ return (
952
+ <div>
953
+ <div aria-labelledby={idOne} />
954
+ <div id={idOne} />
955
+ <span aria-labelledby={idTwo} />
956
+ <span id={idTwo} />
957
+ </div>
958
+ );
959
+ }
960
+
961
+ const domNode = await clientRenderOnServerString(<App />);
962
+ expect(domNode.children.length).toEqual(4);
963
+ expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual(
964
+ domNode.children[1].getAttribute('id'),
965
+ );
966
+ expect(domNode.children[2].getAttribute('aria-labelledby')).toEqual(
967
+ domNode.children[3].getAttribute('id'),
968
+ );
969
+ expect(domNode.children[0].getAttribute('aria-labelledby')).not.toEqual(
970
+ domNode.children[2].getAttribute('aria-labelledby'),
971
+ );
972
+ expect(
973
+ domNode.children[0].getAttribute('aria-labelledby'),
974
+ ).not.toBeNull();
975
+ expect(
976
+ domNode.children[2].getAttribute('aria-labelledby'),
977
+ ).not.toBeNull();
978
+ });
979
+
980
+ it('useOpaqueIdentifier does not change id even if the component updates during client render', async () => {
981
+ let _setShowId;
982
+ function App() {
983
+ const id = useOpaqueIdentifier();
984
+ const [showId, setShowId] = useState(false);
985
+ _setShowId = setShowId;
986
+ return (
987
+ <div>
988
+ <div aria-labelledby={id} />
989
+ {showId && <div id={id} />}
990
+ </div>
991
+ );
992
+ }
993
+
994
+ const domNode = await clientCleanRender(<App />);
995
+ const oldClientId = domNode.children[0].getAttribute('aria-labelledby');
996
+
997
+ expect(domNode.children.length).toEqual(1);
998
+ expect(oldClientId).not.toBeNull();
999
+
1000
+ await ReactTestUtils.act(async () => _setShowId(true));
1001
+
1002
+ expect(domNode.children.length).toEqual(2);
1003
+ expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual(
1004
+ domNode.children[1].getAttribute('id'),
1005
+ );
1006
+ expect(domNode.children[0].getAttribute('aria-labelledby')).toEqual(
1007
+ oldClientId,
1008
+ );
1009
+ });
1010
+
1011
+ it('useOpaqueIdentifier: IDs match when, after hydration, a new component that uses the ID is rendered', async () => {
1012
+ let _setShowDiv;
1013
+ function App() {
1014
+ const id = useOpaqueIdentifier();
1015
+ const [showDiv, setShowDiv] = useState(false);
1016
+ _setShowDiv = setShowDiv;
1017
+
1018
+ return (
1019
+ <div>
1020
+ <div id={id}>Child One</div>
1021
+ {showDiv && <div id={id}>Child Two</div>}
1022
+ </div>
1023
+ );
1024
+ }
1025
+
1026
+ const container = document.createElement('div');
1027
+ document.body.append(container);
1028
+
1029
+ container.innerHTML = ReactDOMServer.renderToString(<App />);
1030
+ const root = ReactDOM.createRoot(container, {hydrate: true});
1031
+ root.render(<App />);
1032
+ Scheduler.unstable_flushAll();
1033
+ jest.runAllTimers();
1034
+
1035
+ expect(container.children[0].children.length).toEqual(1);
1036
+ const oldServerId = container.children[0].children[0].getAttribute(
1037
+ 'id',
1038
+ );
1039
+ expect(oldServerId).not.toBeNull();
1040
+
1041
+ await ReactTestUtils.act(async () => {
1042
+ _setShowDiv(true);
1043
+ });
1044
+ expect(container.children[0].children.length).toEqual(2);
1045
+ expect(container.children[0].children[0].getAttribute('id')).toEqual(
1046
+ container.children[0].children[1].getAttribute('id'),
1047
+ );
1048
+ expect(
1049
+ container.children[0].children[0].getAttribute('id'),
1050
+ ).not.toEqual(oldServerId);
1051
+ expect(
1052
+ container.children[0].children[0].getAttribute('id'),
1053
+ ).not.toBeNull();
1054
+ });
1055
+
1056
+ it('useOpaqueIdentifier: IDs match when, after hydration, a new component that uses the ID is rendered for legacy', async () => {
1057
+ let _setShowDiv;
1058
+ function App() {
1059
+ const id = useOpaqueIdentifier();
1060
+ const [showDiv, setShowDiv] = useState(false);
1061
+ _setShowDiv = setShowDiv;
1062
+
1063
+ return (
1064
+ <div>
1065
+ <div id={id}>Child One</div>
1066
+ {showDiv && <div id={id}>Child Two</div>}
1067
+ </div>
1068
+ );
1069
+ }
1070
+
1071
+ const container = document.createElement('div');
1072
+ document.body.append(container);
1073
+
1074
+ container.innerHTML = ReactDOMServer.renderToString(<App />);
1075
+ ReactDOM.hydrate(<App />, container);
1076
+
1077
+ expect(container.children[0].children.length).toEqual(1);
1078
+ const oldServerId = container.children[0].children[0].getAttribute(
1079
+ 'id',
1080
+ );
1081
+ expect(oldServerId).not.toBeNull();
1082
+
1083
+ await ReactTestUtils.act(async () => {
1084
+ _setShowDiv(true);
1085
+ });
1086
+ expect(container.children[0].children.length).toEqual(2);
1087
+ expect(container.children[0].children[0].getAttribute('id')).toEqual(
1088
+ container.children[0].children[1].getAttribute('id'),
1089
+ );
1090
+ expect(
1091
+ container.children[0].children[0].getAttribute('id'),
1092
+ ).not.toEqual(oldServerId);
1093
+ expect(
1094
+ container.children[0].children[0].getAttribute('id'),
1095
+ ).not.toBeNull();
1096
+ });
1097
+
1098
+ it('useOpaqueIdentifier: ID is not used during hydration but is used in an update', async () => {
1099
+ let _setShow;
1100
+ function App() {
1101
+ Scheduler.unstable_yieldValue('App');
1102
+ const id = useOpaqueIdentifier();
1103
+ const [show, setShow] = useState(false);
1104
+ _setShow = setShow;
1105
+ return (
1106
+ <div>
1107
+ <span id={show ? id : null}>{'Child One'}</span>
1108
+ </div>
1109
+ );
1110
+ }
1111
+
1112
+ const container = document.createElement('div');
1113
+ document.body.append(container);
1114
+ container.innerHTML = ReactDOMServer.renderToString(<App />);
1115
+ const root = ReactDOM.createRoot(container, {hydrate: true});
1116
+ ReactTestUtils.act(() => {
1117
+ root.render(<App />);
1118
+ });
1119
+ expect(Scheduler).toHaveYielded(['App', 'App']);
1120
+ // The ID goes from not being used to being added to the page
1121
+ ReactTestUtils.act(() => {
1122
+ _setShow(true);
1123
+ });
1124
+ expect(Scheduler).toHaveYielded(['App', 'App']);
1125
+ expect(
1126
+ container.getElementsByTagName('span')[0].getAttribute('id'),
1127
+ ).not.toBeNull();
1128
+ });
1129
+
1130
+ it('useOpaqueIdentifier: ID is not used during hydration but is used in an update in legacy', async () => {
1131
+ let _setShow;
1132
+ function App() {
1133
+ Scheduler.unstable_yieldValue('App');
1134
+ const id = useOpaqueIdentifier();
1135
+ const [show, setShow] = useState(false);
1136
+ _setShow = setShow;
1137
+ return (
1138
+ <div>
1139
+ <span id={show ? id : null}>{'Child One'}</span>
1140
+ </div>
1141
+ );
1142
+ }
1143
+
1144
+ const container = document.createElement('div');
1145
+ document.body.append(container);
1146
+ container.innerHTML = ReactDOMServer.renderToString(<App />);
1147
+ ReactDOM.hydrate(<App />, container);
1148
+ expect(Scheduler).toHaveYielded(['App', 'App']);
1149
+ // The ID goes from not being used to being added to the page
1150
+ ReactTestUtils.act(() => {
1151
+ _setShow(true);
1152
+ });
1153
+ expect(Scheduler).toHaveYielded(['App']);
1154
+ expect(
1155
+ container.getElementsByTagName('span')[0].getAttribute('id'),
1156
+ ).not.toBeNull();
1157
+ });
1158
+
1159
+ it('useOpaqueIdentifierr: flushSync', async () => {
1160
+ let _setShow;
1161
+ function App() {
1162
+ const id = useOpaqueIdentifier();
1163
+ const [show, setShow] = useState(false);
1164
+ _setShow = setShow;
1165
+ return (
1166
+ <div>
1167
+ <span id={show ? id : null}>{'Child One'}</span>
1168
+ </div>
1169
+ );
1170
+ }
1171
+
1172
+ const container = document.createElement('div');
1173
+ document.body.append(container);
1174
+ container.innerHTML = ReactDOMServer.renderToString(<App />);
1175
+ const root = ReactDOM.createRoot(container, {hydrate: true});
1176
+ ReactTestUtils.act(() => {
1177
+ root.render(<App />);
1178
+ });
1179
+
1180
+ // The ID goes from not being used to being added to the page
1181
+ ReactTestUtils.act(() => {
1182
+ ReactDOM.flushSync(() => {
1183
+ _setShow(true);
1184
+ });
1185
+ });
1186
+ expect(
1187
+ container.getElementsByTagName('span')[0].getAttribute('id'),
1188
+ ).not.toBeNull();
1189
+ });
1190
+
1191
+ it('useOpaqueIdentifier: children with id hydrates before other children if ID updates', async () => {
1192
+ let _setShow;
1193
+
1194
+ const child1Ref = React.createRef();
1195
+ const childWithIDRef = React.createRef();
1196
+ const setShowRef = React.createRef();
1197
+
1198
+ // RENAME THESE
1199
+ function Child1() {
1200
+ Scheduler.unstable_yieldValue('Child One');
1201
+ return <span ref={child1Ref}>{'Child One'}</span>;
1202
+ }
1203
+
1204
+ function Child2() {
1205
+ Scheduler.unstable_yieldValue('Child Two');
1206
+ return <span>{'Child Two'}</span>;
1207
+ }
1208
+
1209
+ const Children = React.memo(function Children() {
1210
+ return (
1211
+ <React.Suspense fallback="Loading 1...">
1212
+ <Child1 />
1213
+ <Child2 />
1214
+ </React.Suspense>
1215
+ );
1216
+ });
1217
+
1218
+ function ChildWithID({parentID}) {
1219
+ Scheduler.unstable_yieldValue('Child with ID');
1220
+ return (
1221
+ <span id={parentID} ref={childWithIDRef}>
1222
+ {'Child with ID'}
1223
+ </span>
1224
+ );
1225
+ }
1226
+
1227
+ const ChildrenWithID = React.memo(function ChildrenWithID({parentID}) {
1228
+ return (
1229
+ <React.Suspense fallback="Loading 2...">
1230
+ <ChildWithID parentID={parentID} />
1231
+ </React.Suspense>
1232
+ );
1233
+ });
1234
+
1235
+ function App() {
1236
+ const id = useOpaqueIdentifier();
1237
+ const [show, setShow] = useState(false);
1238
+ _setShow = setShow;
1239
+ return (
1240
+ <div>
1241
+ <Children />
1242
+ <ChildrenWithID parentID={id} />
1243
+ {show && (
1244
+ <span aria-labelledby={id} ref={setShowRef}>
1245
+ {'Child Three'}
1246
+ </span>
1247
+ )}
1248
+ </div>
1249
+ );
1250
+ }
1251
+
1252
+ const container = document.createElement('div');
1253
+ container.innerHTML = ReactDOMServer.renderToString(<App />);
1254
+ expect(Scheduler).toHaveYielded([
1255
+ 'Child One',
1256
+ 'Child Two',
1257
+ 'Child with ID',
1258
+ ]);
1259
+ expect(container.textContent).toEqual(
1260
+ 'Child OneChild TwoChild with ID',
1261
+ );
1262
+
1263
+ const serverId = container
1264
+ .getElementsByTagName('span')[2]
1265
+ .getAttribute('id');
1266
+ expect(serverId).not.toBeNull();
1267
+
1268
+ const childOneSpan = container.getElementsByTagName('span')[0];
1269
+
1270
+ const root = ReactDOM.createRoot(container, {hydrate: true});
1271
+ root.render(<App show={false} />);
1272
+ expect(Scheduler).toHaveYielded([]);
1273
+
1274
+ //Hydrate just child one before updating state
1275
+ expect(Scheduler).toFlushAndYieldThrough(['Child One']);
1276
+ expect(child1Ref.current).toBe(null);
1277
+ expect(Scheduler).toHaveYielded([]);
1278
+
1279
+ ReactTestUtils.act(() => {
1280
+ _setShow(true);
1281
+
1282
+ // State update should trigger the ID to update, which changes the props
1283
+ // of ChildWithID. This should cause ChildWithID to hydrate before Children
1284
+ expect(Scheduler).toFlushAndYieldThrough([
1285
+ 'Child with ID',
1286
+ 'Child with ID',
1287
+ 'Child with ID',
1288
+ 'Child One',
1289
+ 'Child Two',
1290
+ ]);
1291
+
1292
+ expect(child1Ref.current).toBe(null);
1293
+ expect(childWithIDRef.current).toEqual(
1294
+ container.getElementsByTagName('span')[2],
1295
+ );
1296
+
1297
+ expect(setShowRef.current).toEqual(
1298
+ container.getElementsByTagName('span')[3],
1299
+ );
1300
+
1301
+ expect(childWithIDRef.current.getAttribute('id')).toEqual(
1302
+ setShowRef.current.getAttribute('aria-labelledby'),
1303
+ );
1304
+ expect(childWithIDRef.current.getAttribute('id')).not.toEqual(
1305
+ serverId,
1306
+ );
1307
+ });
1308
+
1309
+ // Children hydrates after ChildWithID
1310
+ expect(child1Ref.current).toBe(childOneSpan);
1311
+
1312
+ Scheduler.unstable_flushAll();
1313
+
1314
+ expect(Scheduler).toHaveYielded([]);
1315
+ });
1316
+
1317
+ it('useOpaqueIdentifier: IDs match when part of the DOM tree is server rendered and part is client rendered', async () => {
1318
+ let suspend = true;
1319
+ let resolve;
1320
+ const promise = new Promise(
1321
+ resolvePromise => (resolve = resolvePromise),
1322
+ );
1323
+
1324
+ function Child({text}) {
1325
+ if (suspend) {
1326
+ throw promise;
1327
+ } else {
1328
+ return text;
1329
+ }
1330
+ }
1331
+
1332
+ function RenderedChild() {
1333
+ useEffect(() => {
1334
+ Scheduler.unstable_yieldValue('Child did commit');
1335
+ });
1336
+ return null;
1337
+ }
1338
+
1339
+ function App() {
1340
+ const id = useOpaqueIdentifier();
1341
+ useEffect(() => {
1342
+ Scheduler.unstable_yieldValue('Did commit');
1343
+ });
1344
+ return (
1345
+ <div>
1346
+ <div id={id}>Child One</div>
1347
+ <RenderedChild />
1348
+ <React.Suspense fallback={'Fallback'}>
1349
+ <div id={id}>
1350
+ <Child text="Child Two" />
1351
+ </div>
1352
+ </React.Suspense>
1353
+ </div>
1354
+ );
1355
+ }
1356
+
1357
+ const container = document.createElement('div');
1358
+ document.body.appendChild(container);
1359
+
1360
+ container.innerHTML = ReactDOMServer.renderToString(<App />);
1361
+
1362
+ suspend = true;
1363
+ const root = ReactDOM.createRoot(container, {hydrate: true});
1364
+ await ReactTestUtils.act(async () => {
1365
+ root.render(<App />);
1366
+ });
1367
+ jest.runAllTimers();
1368
+ expect(Scheduler).toHaveYielded(['Child did commit', 'Did commit']);
1369
+ expect(Scheduler).toFlushAndYield([]);
1370
+
1371
+ const serverId = container.children[0].children[0].getAttribute('id');
1372
+ expect(container.children[0].children.length).toEqual(1);
1373
+ expect(
1374
+ container.children[0].children[0].getAttribute('id'),
1375
+ ).not.toBeNull();
1376
+
1377
+ await ReactTestUtils.act(async () => {
1378
+ suspend = false;
1379
+ resolve();
1380
+ await promise;
1381
+ });
1382
+
1383
+ expect(Scheduler).toHaveYielded(['Child did commit', 'Did commit']);
1384
+ expect(Scheduler).toFlushAndYield([]);
1385
+ jest.runAllTimers();
1386
+
1387
+ expect(container.children[0].children.length).toEqual(2);
1388
+ expect(container.children[0].children[0].getAttribute('id')).toEqual(
1389
+ container.children[0].children[1].getAttribute('id'),
1390
+ );
1391
+ expect(
1392
+ container.children[0].children[0].getAttribute('id'),
1393
+ ).not.toEqual(serverId);
1394
+ expect(
1395
+ container.children[0].children[0].getAttribute('id'),
1396
+ ).not.toBeNull();
1397
+ });
1398
+
1399
+ it('useOpaqueIdentifier warn when there is a hydration error', async () => {
1400
+ function Child({appId}) {
1401
+ return <div aria-labelledby={appId} />;
1402
+ }
1403
+ function App() {
1404
+ const id = useOpaqueIdentifier();
1405
+ return <Child appId={id} />;
1406
+ }
1407
+
1408
+ const container = document.createElement('div');
1409
+ document.body.appendChild(container);
1410
+
1411
+ // This is the wrong HTML string
1412
+ container.innerHTML = '<span></span>';
1413
+ ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1414
+ expect(() =>
1415
+ expect(() => Scheduler.unstable_flushAll()).toThrow(),
1416
+ ).toErrorDev([
1417
+ 'Warning: Expected server HTML to contain a matching <div> in <div>.',
1418
+ ]);
1419
+ });
1420
+
1421
+ it('useOpaqueIdentifier: IDs match when part of the DOM tree is server rendered and part is client rendered', async () => {
1422
+ let suspend = true;
1423
+
1424
+ function Child({text}) {
1425
+ if (suspend) {
1426
+ throw new Promise(() => {});
1427
+ } else {
1428
+ return text;
1429
+ }
1430
+ }
1431
+
1432
+ function RenderedChild() {
1433
+ useEffect(() => {
1434
+ Scheduler.unstable_yieldValue('Child did commit');
1435
+ });
1436
+ return null;
1437
+ }
1438
+
1439
+ function App() {
1440
+ const id = useOpaqueIdentifier();
1441
+ useEffect(() => {
1442
+ Scheduler.unstable_yieldValue('Did commit');
1443
+ });
1444
+ return (
1445
+ <div>
1446
+ <div id={id}>Child One</div>
1447
+ <RenderedChild />
1448
+ <React.Suspense fallback={'Fallback'}>
1449
+ <div id={id}>
1450
+ <Child text="Child Two" />
1451
+ </div>
1452
+ </React.Suspense>
1453
+ </div>
1454
+ );
1455
+ }
1456
+
1457
+ const container = document.createElement('div');
1458
+ document.body.appendChild(container);
1459
+
1460
+ container.innerHTML = ReactDOMServer.renderToString(<App />);
1461
+
1462
+ suspend = false;
1463
+ const root = ReactDOM.createRoot(container, {hydrate: true});
1464
+ await ReactTestUtils.act(async () => {
1465
+ root.render(<App />);
1466
+ });
1467
+ jest.runAllTimers();
1468
+ expect(Scheduler).toHaveYielded([
1469
+ 'Child did commit',
1470
+ 'Did commit',
1471
+ 'Child did commit',
1472
+ 'Did commit',
1473
+ ]);
1474
+ expect(Scheduler).toFlushAndYield([]);
1475
+
1476
+ expect(container.children[0].children.length).toEqual(2);
1477
+ expect(container.children[0].children[0].getAttribute('id')).toEqual(
1478
+ container.children[0].children[1].getAttribute('id'),
1479
+ );
1480
+ expect(
1481
+ container.children[0].children[0].getAttribute('id'),
1482
+ ).not.toBeNull();
1483
+ });
1484
+
1485
+ it('useOpaqueIdentifier warn when there is a hydration error', async () => {
1486
+ function Child({appId}) {
1487
+ return <div aria-labelledby={appId} />;
1488
+ }
1489
+ function App() {
1490
+ const id = useOpaqueIdentifier();
1491
+ return <Child appId={id} />;
1492
+ }
1493
+
1494
+ const container = document.createElement('div');
1495
+ document.body.appendChild(container);
1496
+
1497
+ // This is the wrong HTML string
1498
+ container.innerHTML = '<span></span>';
1499
+ ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1500
+ expect(() =>
1501
+ expect(() => Scheduler.unstable_flushAll()).toThrow(),
1502
+ ).toErrorDev([
1503
+ 'Warning: Expected server HTML to contain a matching <div> in <div>.',
1504
+ ]);
1505
+ });
1506
+
1507
+ it('useOpaqueIdentifier throws when there is a hydration error and we are using ID as a string', async () => {
1508
+ function Child({appId}) {
1509
+ return <div aria-labelledby={appId + ''} />;
1510
+ }
1511
+ function App() {
1512
+ const id = useOpaqueIdentifier();
1513
+ return <Child appId={id} />;
1514
+ }
1515
+
1516
+ const container = document.createElement('div');
1517
+ document.body.appendChild(container);
1518
+
1519
+ // This is the wrong HTML string
1520
+ container.innerHTML = '<span></span>';
1521
+ ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1522
+ expect(() =>
1523
+ expect(() => Scheduler.unstable_flushAll()).toThrow(
1524
+ 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1525
+ 'Do not read the value directly.',
1526
+ ),
1527
+ ).toErrorDev(
1528
+ [
1529
+ 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
1530
+ 'Warning: Did not expect server HTML to contain a <span> in <div>.',
1531
+ ],
1532
+ {withoutStack: 1},
1533
+ );
1534
+ });
1535
+
1536
+ it('useOpaqueIdentifier throws when there is a hydration error and we are using ID as a string', async () => {
1537
+ function Child({appId}) {
1538
+ return <div aria-labelledby={appId + ''} />;
1539
+ }
1540
+ function App() {
1541
+ const id = useOpaqueIdentifier();
1542
+ return <Child appId={id} />;
1543
+ }
1544
+
1545
+ const container = document.createElement('div');
1546
+ document.body.appendChild(container);
1547
+
1548
+ // This is the wrong HTML string
1549
+ container.innerHTML = '<span></span>';
1550
+ ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1551
+ expect(() =>
1552
+ expect(() => Scheduler.unstable_flushAll()).toThrow(
1553
+ 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1554
+ 'Do not read the value directly.',
1555
+ ),
1556
+ ).toErrorDev(
1557
+ [
1558
+ 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
1559
+ 'Warning: Did not expect server HTML to contain a <span> in <div>.',
1560
+ ],
1561
+ {withoutStack: 1},
1562
+ );
1563
+ });
1564
+
1565
+ it('useOpaqueIdentifier throws if you try to use the result as a string in a child component', async () => {
1566
+ function Child({appId}) {
1567
+ return <div aria-labelledby={appId + ''} />;
1568
+ }
1569
+ function App() {
1570
+ const id = useOpaqueIdentifier();
1571
+ return <Child appId={id} />;
1572
+ }
1573
+
1574
+ const container = document.createElement('div');
1575
+ document.body.appendChild(container);
1576
+
1577
+ container.innerHTML = ReactDOMServer.renderToString(<App />);
1578
+ ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1579
+ expect(() =>
1580
+ expect(() => Scheduler.unstable_flushAll()).toThrow(
1581
+ 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1582
+ 'Do not read the value directly.',
1583
+ ),
1584
+ ).toErrorDev(
1585
+ [
1586
+ 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
1587
+ 'Warning: Did not expect server HTML to contain a <div> in <div>.',
1588
+ ],
1589
+ {withoutStack: 1},
1590
+ );
1591
+ });
1592
+
1593
+ it('useOpaqueIdentifier throws if you try to use the result as a string', async () => {
1594
+ function App() {
1595
+ const id = useOpaqueIdentifier();
1596
+ return <div aria-labelledby={id + ''} />;
1597
+ }
1598
+
1599
+ const container = document.createElement('div');
1600
+ document.body.appendChild(container);
1601
+
1602
+ container.innerHTML = ReactDOMServer.renderToString(<App />);
1603
+ ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1604
+ expect(() =>
1605
+ expect(() => Scheduler.unstable_flushAll()).toThrow(
1606
+ 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1607
+ 'Do not read the value directly.',
1608
+ ),
1609
+ ).toErrorDev(
1610
+ [
1611
+ 'Warning: The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.',
1612
+ 'Warning: Did not expect server HTML to contain a <div> in <div>.',
1613
+ ],
1614
+ {withoutStack: 1},
1615
+ );
1616
+ });
1617
+
1618
+ it('useOpaqueIdentifier throws if you try to use the result as a string in a child component wrapped in a Suspense', async () => {
1619
+ function Child({appId}) {
1620
+ return <div aria-labelledby={appId + ''} />;
1621
+ }
1622
+ function App() {
1623
+ const id = useOpaqueIdentifier();
1624
+ return (
1625
+ <React.Suspense fallback={null}>
1626
+ <Child appId={id} />
1627
+ </React.Suspense>
1628
+ );
1629
+ }
1630
+
1631
+ const container = document.createElement('div');
1632
+ document.body.appendChild(container);
1633
+
1634
+ container.innerHTML = ReactDOMServer.renderToString(<App />);
1635
+
1636
+ ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1637
+
1638
+ expect(() =>
1639
+ expect(() => Scheduler.unstable_flushAll()).toThrow(
1640
+ 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1641
+ 'Do not read the value directly.',
1642
+ ),
1643
+ ).toErrorDev([
1644
+ 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1645
+ 'Do not read the value directly.',
1646
+ ]);
1647
+ });
1648
+
1649
+ it('useOpaqueIdentifier throws if you try to add the result as a number in a child component wrapped in a Suspense', async () => {
1650
+ function Child({appId}) {
1651
+ return <div aria-labelledby={+appId} />;
1652
+ }
1653
+ function App() {
1654
+ const [show] = useState(false);
1655
+ const id = useOpaqueIdentifier();
1656
+ return (
1657
+ <React.Suspense fallback={null}>
1658
+ {show && <div id={id} />}
1659
+ <Child appId={id} />
1660
+ </React.Suspense>
1661
+ );
1662
+ }
1663
+
1664
+ const container = document.createElement('div');
1665
+ document.body.appendChild(container);
1666
+
1667
+ container.innerHTML = ReactDOMServer.renderToString(<App />);
1668
+
1669
+ ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1670
+
1671
+ expect(() =>
1672
+ expect(() => Scheduler.unstable_flushAll()).toThrow(
1673
+ 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1674
+ 'Do not read the value directly.',
1675
+ ),
1676
+ ).toErrorDev([
1677
+ 'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
1678
+ 'Do not read the value directly.',
1679
+ ]);
1680
+ });
1681
+
1682
+ it('useOpaqueIdentifier with two opaque identifiers on the same page', () => {
1683
+ let _setShow;
1684
+
1685
+ function App() {
1686
+ const id1 = useOpaqueIdentifier();
1687
+ const id2 = useOpaqueIdentifier();
1688
+ const [show, setShow] = useState(true);
1689
+ _setShow = setShow;
1690
+
1691
+ return (
1692
+ <div>
1693
+ <React.Suspense fallback={null}>
1694
+ {show ? (
1695
+ <span id={id1}>{'Child'}</span>
1696
+ ) : (
1697
+ <span id={id2}>{'Child'}</span>
1698
+ )}
1699
+ </React.Suspense>
1700
+ <span aria-labelledby={id1}>{'test'}</span>
1701
+ </div>
1702
+ );
1703
+ }
1704
+
1705
+ const container = document.createElement('div');
1706
+ document.body.appendChild(container);
1707
+
1708
+ container.innerHTML = ReactDOMServer.renderToString(<App />);
1709
+
1710
+ const serverID = container
1711
+ .getElementsByTagName('span')[0]
1712
+ .getAttribute('id');
1713
+ expect(serverID).not.toBeNull();
1714
+ expect(
1715
+ container
1716
+ .getElementsByTagName('span')[1]
1717
+ .getAttribute('aria-labelledby'),
1718
+ ).toEqual(serverID);
1719
+
1720
+ ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
1721
+ jest.runAllTimers();
1722
+ expect(Scheduler).toHaveYielded([]);
1723
+ expect(Scheduler).toFlushAndYield([]);
1724
+
1725
+ ReactTestUtils.act(() => {
1726
+ _setShow(false);
1727
+ });
1728
+
1729
+ expect(
1730
+ container
1731
+ .getElementsByTagName('span')[1]
1732
+ .getAttribute('aria-labelledby'),
1733
+ ).toEqual(serverID);
1734
+ expect(
1735
+ container.getElementsByTagName('span')[0].getAttribute('id'),
1736
+ ).not.toEqual(serverID);
1737
+ expect(
1738
+ container.getElementsByTagName('span')[0].getAttribute('id'),
1739
+ ).not.toBeNull();
1740
+ });
1741
+ });
1742
+ }
1743
});