@samitouri / QOS-React / commits / 31518135c2

Strengthen nested update counter test coverage (#15166)

* Isolate ReactUpdates-test cases This ensures their behavior is consistent when run in isolation, and that they actually test the cases they're describing. * Add coverage for cases where we reset nestedUpdateCounter These cases explicitly verify that we reset the counter in right places. * Add a mutually recursive test case * Add test coverage for useLayoutEffect loop

Dan Abramov committed Mar 21, 2019 at 14:52 UTC 31518135c25aaa1b5c2799d2a18b6b9e9178409c
1 file changed +123
packages/react-dom/src/__tests__/ReactUpdates-test.js
+123
@@ -15,6 +15,7 @@ let ReactTestUtils;
15
16 describe('ReactUpdates', () => {
17 beforeEach(() => {
18 + jest.resetModules();
19 React = require('react');
20 ReactDOM = require('react-dom');
21 ReactTestUtils = require('react-dom/test-utils');
@@ -1311,6 +1312,46 @@ describe('ReactUpdates', () => {
1312 ReactDOM.render(<Foo />, container);
1313 });
1314
1315 + it('resets the update counter for unrelated updates', () => {
1316 + const container = document.createElement('div');
1317 + const ref = React.createRef();
1318 +
1319 + class EventuallyTerminating extends React.Component {
1320 + state = {step: 0};
1321 + componentDidMount() {
1322 + this.setState({step: 1});
1323 + }
1324 + componentDidUpdate() {
1325 + if (this.state.step < limit) {
1326 + this.setState({step: this.state.step + 1});
1327 + }
1328 + }
1329 + render() {
1330 + return this.state.step;
1331 + }
1332 + }
1333 +
1334 + let limit = 55;
1335 + expect(() => {
1336 + ReactDOM.render(<EventuallyTerminating ref={ref} />, container);
1337 + }).toThrow('Maximum');
1338 +
1339 + // Verify that we don't go over the limit if these updates are unrelated.
1340 + limit -= 10;
1341 + ReactDOM.render(<EventuallyTerminating ref={ref} />, container);
1342 + expect(container.textContent).toBe(limit.toString());
1343 + ref.current.setState({step: 0});
1344 + expect(container.textContent).toBe(limit.toString());
1345 + ref.current.setState({step: 0});
1346 + expect(container.textContent).toBe(limit.toString());
1347 +
1348 + limit += 10;
1349 + expect(() => {
1350 + ref.current.setState({step: 0});
1351 + }).toThrow('Maximum');
1352 + expect(ref.current).toBe(null);
1353 + });
1354 +
1355 it('does not fall into an infinite update loop', () => {
1356 class NonTerminating extends React.Component {
1357 state = {step: 0};
@@ -1336,6 +1377,88 @@ describe('ReactUpdates', () => {
1377 }).toThrow('Maximum');
1378 });
1379
1380 + it('does not fall into an infinite update loop with useLayoutEffect', () => {
1381 + function NonTerminating() {
1382 + const [step, setStep] = React.useState(0);
1383 + React.useLayoutEffect(() => {
1384 + setStep(x => x + 1);
1385 + });
1386 + return step;
1387 + }
1388 +
1389 + const container = document.createElement('div');
1390 + expect(() => {
1391 + ReactDOM.render(<NonTerminating />, container);
1392 + }).toThrow('Maximum');
1393 + });
1394 +
1395 + it('can recover after falling into an infinite update loop', () => {
1396 + class NonTerminating extends React.Component {
1397 + state = {step: 0};
1398 + componentDidMount() {
1399 + this.setState({step: 1});
1400 + }
1401 + componentDidUpdate() {
1402 + this.setState({step: 2});
1403 + }
1404 + render() {
1405 + return this.state.step;
1406 + }
1407 + }
1408 +
1409 + class Terminating extends React.Component {
1410 + state = {step: 0};
1411 + componentDidMount() {
1412 + this.setState({step: 1});
1413 + }
1414 + render() {
1415 + return this.state.step;
1416 + }
1417 + }
1418 +
1419 + const container = document.createElement('div');
1420 + expect(() => {
1421 + ReactDOM.render(<NonTerminating />, container);
1422 + }).toThrow('Maximum');
1423 +
1424 + ReactDOM.render(<Terminating />, container);
1425 + expect(container.textContent).toBe('1');
1426 +
1427 + expect(() => {
1428 + ReactDOM.render(<NonTerminating />, container);
1429 + }).toThrow('Maximum');
1430 +
1431 + ReactDOM.render(<Terminating />, container);
1432 + expect(container.textContent).toBe('1');
1433 + });
1434 +
1435 + it('does not fall into mutually recursive infinite update loop with same container', () => {
1436 + // Note: this test would fail if there were two or more different roots.
1437 +
1438 + class A extends React.Component {
1439 + componentDidMount() {
1440 + ReactDOM.render(<B />, container);
1441 + }
1442 + render() {
1443 + return null;
1444 + }
1445 + }
1446 +
1447 + class B extends React.Component {
1448 + componentDidMount() {
1449 + ReactDOM.render(<A />, container);
1450 + }
1451 + render() {
1452 + return null;
1453 + }
1454 + }
1455 +
1456 + const container = document.createElement('div');
1457 + expect(() => {
1458 + ReactDOM.render(<A />, container);
1459 + }).toThrow('Maximum');
1460 + });
1461 +
1462 it('does not fall into an infinite error loop', () => {
1463 function BadRender() {
1464 throw new Error('error');