@samitouri / QOS-React-2 / commits / 37ca3874af

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