Skip to content

Commit 3151813

Browse files
authored
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
1 parent 66f280c commit 3151813

File tree

1 file changed

+123
-0
lines changed

1 file changed

+123
-0
lines changed

packages/react-dom/src/__tests__/ReactUpdates-test.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ let ReactTestUtils;
1515

1616
describe('ReactUpdates', () => {
1717
beforeEach(() => {
18+
jest.resetModules();
1819
React = require('react');
1920
ReactDOM = require('react-dom');
2021
ReactTestUtils = require('react-dom/test-utils');
@@ -1311,6 +1312,46 @@ describe('ReactUpdates', () => {
13111312
ReactDOM.render(<Foo />, container);
13121313
});
13131314

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+
13141355
it('does not fall into an infinite update loop', () => {
13151356
class NonTerminating extends React.Component {
13161357
state = {step: 0};
@@ -1336,6 +1377,88 @@ describe('ReactUpdates', () => {
13361377
}).toThrow('Maximum');
13371378
});
13381379

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+
13391462
it('does not fall into an infinite error loop', () => {
13401463
function BadRender() {
13411464
throw new Error('error');

0 commit comments

Comments
 (0)