Add unit tests for aborted life-cycles
This tests the life-cycles when work gets aborted.
Sebastian Markbage committed
Oct 20, 2016 at 18:09 UTC
37ca3874af3e94f6d108d31779adaee742cb4684
1 file changed
+314
-7
src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
+314
-7
@@ -839,8 +839,14 @@ describe('ReactIncremental', () => {
839
it('can call sCU while resuming a partly mounted component', () => {
840
var ops = [];
841
842
+ var instances = new Set();
843
+
844
class Bar extends React.Component {
845
state = { y: 'A' };
846
+ constructor() {
847
+ super();
848
+ instances.add(this);
849
+ }
850
shouldComponentUpdate(newProps, newState) {
851
return this.props.x !== newProps.x ||
852
this.state.y !== newState.y;
@@ -855,20 +861,29 @@ describe('ReactIncremental', () => {
861
ops.push('Foo');
862
return [
863
<Bar key="a" x="A" />,
858
- <Bar key="b" x="B" />,
864
+ <Bar key="b" x={props.step === 0 ? 'B' : 'B2'} />,
865
<Bar key="c" x="C" />,
866
+ <Bar key="d" x="D" />,
867
];
868
}
869
863
- ReactNoop.render(<Foo />);
864
- ReactNoop.flushDeferredPri(30);
865
- expect(ops).toEqual(['Foo', 'Bar:A', 'Bar:B']);
870
+ ReactNoop.render(<Foo step={0} />);
871
+ ReactNoop.flushDeferredPri(40);
872
+ expect(ops).toEqual(['Foo', 'Bar:A', 'Bar:B', 'Bar:C']);
873
+
874
+ expect(instances.size).toBe(3);
875
876
ops = [];
877
869
- ReactNoop.render(<Foo />);
870
- ReactNoop.flushDeferredPri(40);
871
- expect(ops).toEqual(['Foo', 'Bar:B', 'Bar:C']);
878
+ ReactNoop.render(<Foo step={1} />);
879
+ ReactNoop.flushDeferredPri(50);
880
+ // A completed and was reused. B completed but couldn't be reused because
881
+ // props differences. C didn't complete and therefore couldn't be reused.
882
+ // D never even started so it needed a new instance.
883
+ expect(ops).toEqual(['Foo', 'Bar:B2', 'Bar:C', 'Bar:D']);
884
+
885
+ // We expect each rerender to correspond to a new instance.
886
+ expect(instances.size).toBe(6);
887
});
888
889
it('gets new props when setting state on a partly updated component', () => {
@@ -927,4 +942,296 @@ describe('ReactIncremental', () => {
942
expect(ops).toEqual(['Bar:A-1', 'Baz', 'Baz']);
943
});
944
945
+ it('calls componentWillMount twice if the initial render is aborted', () => {
946
+ var ops = [];
947
+
948
+ class LifeCycle extends React.Component {
949
+ state = { x: this.props.x };
950
+ componentWillMount() {
951
+ ops.push('componentWillMount:' + this.state.x + '-' + this.props.x);
952
+ }
953
+ componentDidMount() {
954
+ ops.push('componentDidMount:' + this.state.x + '-' + this.props.x);
955
+ }
956
+ render() {
957
+ return <span />;
958
+ }
959
+ }
960
+
961
+ function Trail() {
962
+ ops.push('Trail');
963
+ }
964
+
965
+ function App(props) {
966
+ ops.push('App');
967
+ return (
968
+ <div>
969
+ <LifeCycle x={props.x} />
970
+ <Trail />
971
+ </div>
972
+ );
973
+ }
974
+
975
+ ReactNoop.render(<App x={0} />);
976
+ ReactNoop.flushDeferredPri(30);
977
+
978
+ expect(ops).toEqual([
979
+ 'App',
980
+ 'componentWillMount:0-0',
981
+ ]);
982
+
983
+ ops = [];
984
+
985
+ ReactNoop.render(<App x={1} />);
986
+ ReactNoop.flush();
987
+
988
+ expect(ops).toEqual([
989
+ 'App',
990
+ 'componentWillMount:1-1',
991
+ 'Trail',
992
+ 'componentDidMount:1-1',
993
+ ]);
994
+ });
995
+
996
+ it('calls componentWill* twice if an update render is aborted', () => {
997
+ var ops = [];
998
+
999
+ class LifeCycle extends React.Component {
1000
+ componentWillMount() {
1001
+ ops.push('componentWillMount:' + this.props.x);
1002
+ }
1003
+ componentDidMount() {
1004
+ ops.push('componentDidMount:' + this.props.x);
1005
+ }
1006
+ componentWillReceiveProps(nextProps) {
1007
+ ops.push('componentWillReceiveProps:' + this.props.x + '-' + nextProps.x);
1008
+ }
1009
+ shouldComponentUpdate(nextProps) {
1010
+ ops.push('shouldComponentUpdate:' + this.props.x + '-' + nextProps.x);
1011
+ return true;
1012
+ }
1013
+ componentWillUpdate(nextProps) {
1014
+ ops.push('componentWillUpdate:' + this.props.x + '-' + nextProps.x);
1015
+ }
1016
+ componentDidUpdate(prevProps) {
1017
+ ops.push('componentDidUpdate:' + this.props.x + '-' + prevProps.x);
1018
+ }
1019
+ render() {
1020
+ ops.push('render:' + this.props.x);
1021
+ return <span />;
1022
+ }
1023
+ }
1024
+
1025
+ function Sibling() {
1026
+ // The sibling is used to confirm that we've completed the first child,
1027
+ // but not yet flushed.
1028
+ ops.push('Sibling');
1029
+ return <span />;
1030
+ }
1031
+
1032
+ function App(props) {
1033
+ ops.push('App');
1034
+
1035
+ return [
1036
+ <LifeCycle key="a" x={props.x} />,
1037
+ <Sibling key="b" />,
1038
+ ];
1039
+ }
1040
+
1041
+ ReactNoop.render(<App x={0} />);
1042
+ ReactNoop.flush();
1043
+
1044
+ expect(ops).toEqual([
1045
+ 'App',
1046
+ 'componentWillMount:0',
1047
+ 'render:0',
1048
+ 'Sibling',
1049
+ 'componentDidMount:0',
1050
+ ]);
1051
+
1052
+ ops = [];
1053
+
1054
+ ReactNoop.render(<App x={1} />);
1055
+ ReactNoop.flushDeferredPri(30);
1056
+
1057
+ expect(ops).toEqual([
1058
+ 'App',
1059
+ 'componentWillReceiveProps:0-1',
1060
+ 'shouldComponentUpdate:0-1',
1061
+ 'componentWillUpdate:0-1',
1062
+ 'render:1',
1063
+ 'Sibling',
1064
+ // no componentDidUpdate
1065
+ ]);
1066
+
1067
+ ops = [];
1068
+
1069
+ ReactNoop.render(<App x={2} />);
1070
+ ReactNoop.flush();
1071
+
1072
+ expect(ops).toEqual([
1073
+ 'App',
1074
+ 'componentWillReceiveProps:1-2',
1075
+ 'shouldComponentUpdate:1-2',
1076
+ 'componentWillUpdate:1-2',
1077
+ 'render:2',
1078
+ 'Sibling',
1079
+ // When componentDidUpdate finally gets called, it covers both updates.
1080
+ 'componentDidUpdate:2-0',
1081
+ ]);
1082
+ });
1083
+
1084
+ it('does not call componentWillReceiveProps for state-only updates', () => {
1085
+ var ops = [];
1086
+
1087
+ var instances = [];
1088
+
1089
+ class LifeCycle extends React.Component {
1090
+ state = { x: 0 };
1091
+ tick() {
1092
+ this.setState({
1093
+ x: this.state.x + 1,
1094
+ });
1095
+ }
1096
+ componentWillMount() {
1097
+ instances.push(this);
1098
+ ops.push('componentWillMount:' + this.state.x);
1099
+ }
1100
+ componentDidMount() {
1101
+ ops.push('componentDidMount:' + this.state.x);
1102
+ }
1103
+ componentWillReceiveProps(nextProps) {
1104
+ ops.push('componentWillReceiveProps');
1105
+ }
1106
+ shouldComponentUpdate(nextProps, nextState) {
1107
+ ops.push('shouldComponentUpdate:' + this.state.x + '-' + nextState.x);
1108
+ return true;
1109
+ }
1110
+ componentWillUpdate(nextProps, nextState) {
1111
+ ops.push('componentWillUpdate:' + this.state.x + '-' + nextState.x);
1112
+ }
1113
+ componentDidUpdate(prevProps, prevState) {
1114
+ ops.push('componentDidUpdate:' + this.state.x + '-' + prevState.x);
1115
+ }
1116
+ render() {
1117
+ ops.push('render:' + this.state.x);
1118
+ return <span />;
1119
+ }
1120
+ }
1121
+
1122
+ // This wrap is a bit contrived because we can't pause a completed root and
1123
+ // there is currently an issue where a component can't reuse its render
1124
+ // output unless it fully completed.
1125
+ class Wrap extends React.Component {
1126
+ state = { y: 0 };
1127
+ componentWillMount() {
1128
+ instances.push(this);
1129
+ }
1130
+ tick() {
1131
+ this.setState({
1132
+ y: this.state.y + 1,
1133
+ });
1134
+ }
1135
+ render() {
1136
+ ops.push('Wrap');
1137
+ return <LifeCycle y={this.state.y} />;
1138
+ }
1139
+ }
1140
+
1141
+ function Sibling() {
1142
+ // The sibling is used to confirm that we've completed the first child,
1143
+ // but not yet flushed.
1144
+ ops.push('Sibling');
1145
+ return <span />;
1146
+ }
1147
+
1148
+ function App(props) {
1149
+ ops.push('App');
1150
+ return [
1151
+ <Wrap key="a" />,
1152
+ <Sibling key="b" />,
1153
+ ];
1154
+ }
1155
+
1156
+ ReactNoop.render(<App y={0} />);
1157
+ ReactNoop.flush();
1158
+
1159
+ expect(ops).toEqual([
1160
+ 'App',
1161
+ 'Wrap',
1162
+ 'componentWillMount:0',
1163
+ 'render:0',
1164
+ 'Sibling',
1165
+ 'componentDidMount:0',
1166
+ ]);
1167
+
1168
+ ops = [];
1169
+
1170
+ // LifeCycle
1171
+ instances[1].tick();
1172
+
1173
+ ReactNoop.flushDeferredPri(25);
1174
+
1175
+ expect(ops).toEqual([
1176
+ // no componentWillReceiveProps
1177
+ 'shouldComponentUpdate:0-1',
1178
+ 'componentWillUpdate:0-1',
1179
+ 'render:1',
1180
+ // no componentDidUpdate
1181
+ ]);
1182
+
1183
+ ops = [];
1184
+
1185
+ // LifeCycle
1186
+ instances[1].tick();
1187
+
1188
+ ReactNoop.flush();
1189
+
1190
+ expect(ops).toEqual([
1191
+ // no componentWillReceiveProps
1192
+ 'shouldComponentUpdate:1-2',
1193
+ 'componentWillUpdate:1-2',
1194
+ 'render:2',
1195
+ // When componentDidUpdate finally gets called, it covers both updates.
1196
+ 'componentDidUpdate:2-0',
1197
+ ]);
1198
+
1199
+ ops = [];
1200
+
1201
+ // Next we will update props of LifeCycle by updating its parent.
1202
+
1203
+ instances[0].tick();
1204
+
1205
+ ReactNoop.flushDeferredPri(30);
1206
+
1207
+ expect(ops).toEqual([
1208
+ 'Wrap',
1209
+ 'componentWillReceiveProps',
1210
+ 'shouldComponentUpdate:2-2',
1211
+ 'componentWillUpdate:2-2',
1212
+ 'render:2',
1213
+ // no componentDidUpdate
1214
+ ]);
1215
+
1216
+ ops = [];
1217
+
1218
+ // Next we will update LifeCycle directly but not with new props.
1219
+ instances[1].tick();
1220
+
1221
+ ReactNoop.flush();
1222
+
1223
+ expect(ops).toEqual([
1224
+ // This should not trigger another componentWillReceiveProps because
1225
+ // we never got new props.
1226
+ 'shouldComponentUpdate:2-3',
1227
+ 'componentWillUpdate:2-3',
1228
+ 'render:3',
1229
+ 'componentDidUpdate:3-2',
1230
+ ]);
1231
+
1232
+ // TODO: Test that we get the expected values for the same scenario with
1233
+ // incomplete parents.
1234
+
1235
+ });
1236
+
1237
});