Skip to content

Commit 7136730

Browse files
committed
Add unit tests for aborted life-cycles
This tests the life-cycles when work gets aborted.
1 parent 59464a3 commit 7136730

File tree

1 file changed

+314
-7
lines changed

1 file changed

+314
-7
lines changed

src/renderers/shared/fiber/__tests__/ReactIncremental-test.js

Lines changed: 314 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -839,8 +839,14 @@ describe('ReactIncremental', () => {
839839
it('can call sCU while resuming a partly mounted component', () => {
840840
var ops = [];
841841

842+
var instances = new Set();
843+
842844
class Bar extends React.Component {
843845
state = { y: 'A' };
846+
constructor() {
847+
super();
848+
instances.add(this);
849+
}
844850
shouldComponentUpdate(newProps, newState) {
845851
return this.props.x !== newProps.x ||
846852
this.state.y !== newState.y;
@@ -855,20 +861,29 @@ describe('ReactIncremental', () => {
855861
ops.push('Foo');
856862
return [
857863
<Bar key="a" x="A" />,
858-
<Bar key="b" x="B" />,
864+
<Bar key="b" x={props.step === 0 ? 'B' : 'B2'} />,
859865
<Bar key="c" x="C" />,
866+
<Bar key="d" x="D" />,
860867
];
861868
}
862869

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);
866875

867876
ops = [];
868877

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);
872887
});
873888

874889
it('gets new props when setting state on a partly updated component', () => {
@@ -927,4 +942,296 @@ describe('ReactIncremental', () => {
927942
expect(ops).toEqual(['Bar:A-1', 'Baz', 'Baz']);
928943
});
929944

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+
9301237
});

0 commit comments

Comments
 (0)