4
let ReactNoop;
5
let SimpleCacheProvider;
6
let Timeout;
7
+let StrictMode;
8
+let AsyncMode;
9
10
let cache;
11
let TextResource;
23
ReactNoop = require('react-noop-renderer');
24
SimpleCacheProvider = require('simple-cache-provider');
25
Timeout = React.Timeout;
26
+ StrictMode = React.StrictMode;
27
+ AsyncMode = React.unstable_AsyncMode;
28
29
function invalidateCache() {
30
cache = SimpleCacheProvider.createCache(invalidateCache);
833
expect(ReactNoop.getChildren()).toEqual([span('A')]);
834
});
835
});
836
+
837
+ describe('sync mode', () => {
838
+ it('times out immediately', async () => {
839
+ function App() {
840
+ return (
841
+ <Fallback timeout={1000} placeholder={<Text text="Loading..." />}>
842
+ <AsyncText ms={100} text="Result" />
843
+ </Fallback>
844
+ );
845
+ }
846
+
847
+ // Times out immediately, ignoring the specified threshold.
848
+ ReactNoop.renderLegacySyncRoot(<App />);
849
+ expect(ReactNoop.clearYields()).toEqual([
850
+ 'Suspend! [Result]',
851
+ 'Loading...',
852
+ ]);
853
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
854
+
855
+ await advanceTimers(100);
856
+ expect(ReactNoop.expire(100)).toEqual([
857
+ 'Promise resolved [Result]',
858
+ 'Result',
859
+ ]);
860
+
861
+ expect(ReactNoop.getChildren()).toEqual([span('Result')]);
862
+ });
863
+
864
+ it('times out immediately when Timeout is in loose mode, even if the suspender is async', async () => {
865
+ class UpdatingText extends React.Component {
866
+ state = {step: 1};
867
+ render() {
868
+ return <AsyncText ms={100} text={`Step: ${this.state.step}`} />;
869
+ }
870
+ }
871
+
872
+ function Placeholder() {
873
+ return (
874
+ <Fragment>
875
+ <Text text="Loading (1)" />
876
+ <Text text="Loading (2)" />
877
+ <Text text="Loading (3)" />
878
+ </Fragment>
879
+ );
880
+ }
881
+
882
+ const text = React.createRef(null);
883
+ function App() {
884
+ return (
885
+ <Fallback timeout={1000} placeholder={<Placeholder />}>
886
+ <AsyncMode>
887
+ <UpdatingText ref={text} />
888
+ <Text text="Sibling" />
889
+ </AsyncMode>
890
+ </Fallback>
891
+ );
892
+ }
893
+
894
+ // Initial mount. This is synchronous, because the root is sync.
895
+ ReactNoop.renderLegacySyncRoot(<App />);
896
+ await advanceTimers(100);
897
+ expect(ReactNoop.clearYields()).toEqual([
898
+ 'Suspend! [Step: 1]',
899
+ 'Sibling',
900
+ 'Loading (1)',
901
+ 'Loading (2)',
902
+ 'Loading (3)',
903
+ 'Promise resolved [Step: 1]',
904
+ 'Step: 1',
905
+ 'Sibling',
906
+ ]);
907
+ expect(ReactNoop.getChildren()).toEqual([
908
+ span('Step: 1'),
909
+ span('Sibling'),
910
+ ]);
911
+
912
+ // Update. This starts out asynchronously.
913
+ text.current.setState({step: 2}, () =>
914
+ ReactNoop.yield('Update did commit'),
915
+ );
916
+
917
+ // Suspend during an async render.
918
+ expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Step: 2]']);
919
+ expect(ReactNoop.flush()).toEqual([
920
+ 'Update did commit',
921
+ // Switch to the placeholder in a subsequent commit
922
+ 'Loading (1)',
923
+ 'Loading (2)',
924
+ 'Loading (3)',
925
+ ]);
926
+ expect(ReactNoop.getChildren()).toEqual([
927
+ span('Loading (1)'),
928
+ span('Loading (2)'),
929
+ span('Loading (3)'),
930
+ ]);
931
+
932
+ await advanceTimers(100);
933
+ expect(ReactNoop.flush()).toEqual([
934
+ 'Promise resolved [Step: 2]',
935
+ // TODO: The state of the children is lost when switching back. Revisit
936
+ // this in the follow up PR.
937
+ 'Step: 1',
938
+ 'Sibling',
939
+ ]);
940
+ expect(ReactNoop.getChildren()).toEqual([
941
+ span('Step: 1'),
942
+ span('Sibling'),
943
+ ]);
944
+ });
945
+
946
+ it(
947
+ 'continues rendering asynchronously even if a promise is captured by ' +
948
+ 'a sync boundary (strict)',
949
+ async () => {
950
+ class UpdatingText extends React.Component {
951
+ state = {text: this.props.initialText};
952
+ render() {
953
+ return this.props.children(this.state.text);
954
+ }
955
+ }
956
+
957
+ const text1 = React.createRef(null);
958
+ const text2 = React.createRef(null);
959
+ function App() {
960
+ return (
961
+ <StrictMode>
962
+ <Fallback timeout={1000} placeholder={<Text text="Loading..." />}>
963
+ <AsyncMode>
964
+ <UpdatingText ref={text1} initialText="Async: 1">
965
+ {text => (
966
+ <Fragment>
967
+ <Text text="Before" />
968
+ <AsyncText text={text} />
969
+ <Text text="After" />
970
+ </Fragment>
971
+ )}
972
+ </UpdatingText>
973
+ </AsyncMode>
974
+ </Fallback>
975
+ <AsyncMode>
976
+ <UpdatingText ref={text2} initialText="Sync: 1">
977
+ {text => (
978
+ <Fragment>
979
+ <Text text="Before" />
980
+ <Text text={text} />
981
+ <Text text="After" />
982
+ </Fragment>
983
+ )}
984
+ </UpdatingText>
985
+ </AsyncMode>
986
+ </StrictMode>
987
+ );
988
+ }
989
+
990
+ // Initial mount
991
+ ReactNoop.renderLegacySyncRoot(<App />, () =>
992
+ ReactNoop.yield('Did mount'),
993
+ );
994
+ await advanceTimers(100);
995
+ expect(ReactNoop.clearYields()).toEqual([
996
+ 'Before',
997
+ 'Suspend! [Async: 1]',
998
+ 'After',
999
+ 'Loading...',
1000
+ 'Before',
1001
+ 'Sync: 1',
1002
+ 'After',
1003
+ 'Did mount',
1004
+ 'Promise resolved [Async: 1]',
1005
+ 'Before',
1006
+ 'Async: 1',
1007
+ 'After',
1008
+ ]);
1009
+ expect(ReactNoop.getChildren()).toEqual([
1010
+ span('Before'),
1011
+ span('Async: 1'),
1012
+ span('After'),
1013
+
1014
+ span('Before'),
1015
+ span('Sync: 1'),
1016
+ span('After'),
1017
+ ]);
1018
+
1019
+ // Update. This starts out asynchronously.
1020
+ text1.current.setState({text: 'Async: 2'}, () =>
1021
+ ReactNoop.yield('Update 1 did commit'),
1022
+ );
1023
+ text2.current.setState({text: 'Sync: 2'}, () =>
1024
+ ReactNoop.yield('Update 2 did commit'),
1025
+ );
1026
+
1027
+ // Start rendering asynchronously
1028
+ ReactNoop.flushThrough([
1029
+ 'Before',
1030
+ // This child suspends
1031
+ 'Suspend! [Async: 2]',
1032
+ // But we can still render the rest of the async tree asynchronously
1033
+ 'After',
1034
+ ]);
1035
+
1036
+ // Suspend during an async render.
1037
+ expect(ReactNoop.flushNextYield()).toEqual(['Loading...']);
1038
+ expect(ReactNoop.flush()).toEqual(['Before', 'Sync: 2', 'After']);
1039
+ // Commit was suspended.
1040
+ expect(ReactNoop.getChildren()).toEqual([
1041
+ span('Before'),
1042
+ span('Async: 1'),
1043
+ span('After'),
1044
+
1045
+ span('Before'),
1046
+ span('Sync: 1'),
1047
+ span('After'),
1048
+ ]);
1049
+
1050
+ // When the placeholder is pinged, the boundary re-
1051
+ // renders asynchronously.
1052
+ ReactNoop.expire(100);
1053
+ await advanceTimers(100);
1054
+ expect(ReactNoop.flush()).toEqual([
1055
+ 'Promise resolved [Async: 2]',
1056
+ 'Before',
1057
+ 'Async: 2',
1058
+ 'After',
1059
+ 'Before',
1060
+ 'Sync: 2',
1061
+ 'After',
1062
+ 'Update 1 did commit',
1063
+ 'Update 2 did commit',
1064
+ ]);
1065
+
1066
+ expect(ReactNoop.getChildren()).toEqual([
1067
+ span('Before'),
1068
+ span('Async: 2'),
1069
+ span('After'),
1070
+
1071
+ span('Before'),
1072
+ span('Sync: 2'),
1073
+ span('After'),
1074
+ ]);
1075
+ },
1076
+ );
1077
+
1078
+ it(
1079
+ 'continues rendering asynchronously even if a promise is captured by ' +
1080
+ 'a sync boundary (loose)',
1081
+ async () => {
1082
+ class UpdatingText extends React.Component {
1083
+ state = {text: this.props.initialText};
1084
+ render() {
1085
+ return this.props.children(this.state.text);
1086
+ }
1087
+ }
1088
+
1089
+ const text1 = React.createRef(null);
1090
+ const text2 = React.createRef(null);
1091
+ function App() {
1092
+ return (
1093
+ <Fragment>
1094
+ <Fallback timeout={1000} placeholder={<Text text="Loading..." />}>
1095
+ <AsyncMode>
1096
+ <UpdatingText ref={text1} initialText="Async: 1">
1097
+ {text => (
1098
+ <Fragment>
1099
+ <Text text="Before" />
1100
+ <AsyncText text={text} />
1101
+ <Text text="After" />
1102
+ </Fragment>
1103
+ )}
1104
+ </UpdatingText>
1105
+ </AsyncMode>
1106
+ </Fallback>
1107
+ <AsyncMode>
1108
+ <UpdatingText ref={text2} initialText="Sync: 1">
1109
+ {text => (
1110
+ <Fragment>
1111
+ <Text text="Before" />
1112
+ <Text text={text} />
1113
+ <Text text="After" />
1114
+ </Fragment>
1115
+ )}
1116
+ </UpdatingText>
1117
+ </AsyncMode>
1118
+ </Fragment>
1119
+ );
1120
+ }
1121
+
1122
+ // Initial mount
1123
+ ReactNoop.renderLegacySyncRoot(<App />, () =>
1124
+ ReactNoop.yield('Did mount'),
1125
+ );
1126
+ await advanceTimers(100);
1127
+ expect(ReactNoop.clearYields()).toEqual([
1128
+ 'Before',
1129
+ 'Suspend! [Async: 1]',
1130
+ 'After',
1131
+ 'Before',
1132
+ 'Sync: 1',
1133
+ 'After',
1134
+ 'Did mount',
1135
+ // The placeholder is rendered in a subsequent commit
1136
+ 'Loading...',
1137
+ 'Promise resolved [Async: 1]',
1138
+ 'Before',
1139
+ 'Async: 1',
1140
+ 'After',
1141
+ ]);
1142
+ expect(ReactNoop.getChildren()).toEqual([
1143
+ span('Before'),
1144
+ span('Async: 1'),
1145
+ span('After'),
1146
+
1147
+ span('Before'),
1148
+ span('Sync: 1'),
1149
+ span('After'),
1150
+ ]);
1151
+
1152
+ // Update. This starts out asynchronously.
1153
+ text1.current.setState({text: 'Async: 2'}, () =>
1154
+ ReactNoop.yield('Update 1 did commit'),
1155
+ );
1156
+ text2.current.setState({text: 'Sync: 2'}, () =>
1157
+ ReactNoop.yield('Update 2 did commit'),
1158
+ );
1159
+
1160
+ // Start rendering asynchronously
1161
+ ReactNoop.flushThrough(['Before']);
1162
+
1163
+ // Now render the next child, which suspends
1164
+ expect(ReactNoop.flushNextYield()).toEqual([
1165
+ // This child suspends
1166
+ 'Suspend! [Async: 2]',
1167
+ ]);
1168
+ expect(ReactNoop.flush()).toEqual([
1169
+ 'After',
1170
+ 'Before',
1171
+ 'Sync: 2',
1172
+ 'After',
1173
+ 'Update 1 did commit',
1174
+ 'Update 2 did commit',
1175
+
1176
+ // Switch to the placeholder in a subsequent commit
1177
+ 'Loading...',
1178
+ ]);
1179
+ expect(ReactNoop.getChildren()).toEqual([
1180
+ span('Loading...'),
1181
+
1182
+ span('Before'),
1183
+ span('Sync: 2'),
1184
+ span('After'),
1185
+ ]);
1186
+
1187
+ // When the placeholder is pinged, the boundary must be re-rendered
1188
+ // synchronously.
1189
+ await advanceTimers(100);
1190
+ expect(ReactNoop.clearYields()).toEqual([
1191
+ 'Promise resolved [Async: 2]',
1192
+ 'Before',
1193
+ 'Async: 1',
1194
+ 'After',
1195
+ ]);
1196
+
1197
+ expect(ReactNoop.getChildren()).toEqual([
1198
+ span('Before'),
1199
+ // TODO: The state of the children is lost when switching back. Revisit
1200
+ // this in the follow up PR.
1201
+ span('Async: 1'),
1202
+ span('After'),
1203
+
1204
+ span('Before'),
1205
+ span('Sync: 2'),
1206
+ span('After'),
1207
+ ]);
1208
+ },
1209
+ );
1210
+
1211
+ it('does not re-render siblings in loose mode', async () => {
1212
+ class TextWithLifecycle extends React.Component {
1213
+ componentDidMount() {
1214
+ ReactNoop.yield(`Mount [${this.props.text}]`);
1215
+ }
1216
+ componentDidUpdate() {
1217
+ ReactNoop.yield(`Update [${this.props.text}]`);
1218
+ }
1219
+ render() {
1220
+ return <Text {...this.props} />;
1221
+ }
1222
+ }
1223
+
1224
+ class AsyncTextWithLifecycle extends React.Component {
1225
+ componentDidMount() {
1226
+ ReactNoop.yield(`Mount [${this.props.text}]`);
1227
+ }
1228
+ componentDidUpdate() {
1229
+ ReactNoop.yield(`Update [${this.props.text}]`);
1230
+ }
1231
+ render() {
1232
+ return <AsyncText {...this.props} />;
1233
+ }
1234
+ }
1235
+
1236
+ function App() {
1237
+ return (
1238
+ <Fallback
1239
+ timeout={1000}
1240
+ placeholder={<TextWithLifecycle text="Loading..." />}>
1241
+ <TextWithLifecycle text="A" />
1242
+ <AsyncTextWithLifecycle ms={100} text="B" />
1243
+ <TextWithLifecycle text="C" />
1244
+ </Fallback>
1245
+ );
1246
+ }
1247
+
1248
+ ReactNoop.renderLegacySyncRoot(<App />, () =>
1249
+ ReactNoop.yield('Commit root'),
1250
+ );
1251
+ expect(ReactNoop.clearYields()).toEqual([
1252
+ 'A',
1253
+ 'Suspend! [B]',
1254
+ 'C',
1255
+
1256
+ 'Mount [A]',
1257
+ 'Mount [B]',
1258
+ 'Mount [C]',
1259
+ 'Commit root',
1260
+
1261
+ // In a subsequent commit, render a placeholder
1262
+ 'Loading...',
1263
+ // Force delete all the existing children when switching to the
1264
+ // placeholder. This should be a mount, not an update.
1265
+ 'Mount [Loading...]',
1266
+ ]);
1267
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1268
+
1269
+ await advanceTimers(1000);
1270
+ expect(ReactNoop.expire(1000)).toEqual([
1271
+ 'Promise resolved [B]',
1272
+ 'A',
1273
+ 'B',
1274
+ 'C',
1275
+ // 'A' matched with the placeholder. It's ok to reuse children when
1276
+ // switching back. Though in a real app you probably don't want to.
1277
+ // TODO: This is wrong. The timed out children and the placeholder
1278
+ // should be siblings in async mode. Revisit in follow-up PR.
1279
+ 'Update [A]',
1280
+ 'Mount [B]',
1281
+ 'Mount [C]',
1282
+ ]);
1283
+
1284
+ expect(ReactNoop.getChildren()).toEqual([
1285
+ span('A'),
1286
+ span('B'),
1287
+ span('C'),
1288
+ ]);
1289
+ });
1290
+ });
1291
});
1292
1293
// TODO: