Skip to content

Commit 6d60db8

Browse files
committed
Regression test: Bad shouldYield causes act to hang
Based on a bug report from @bvaughn. `act` should not consult `shouldYield` when it's performing work, because in a unit testing environment, I/O (such as `setTimeout`) is likely mocked. So the result of `shouldYield` can't be trusted. In this regression test, I simulate the bug by mocking `shouldYield` to always return `true`. This causes an infinite loop in `act`, because it will keep trying to render and React will keep yielding. I will fix the bug in the next commit by ignoring `shouldYield` whenever we're inside an `act` scope.
1 parent 106ea1c commit 6d60db8

File tree

1 file changed

+67
-0
lines changed

1 file changed

+67
-0
lines changed

packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,70 @@ describe(
320320
});
321321
},
322322
);
323+
324+
describe('`act` bypasses Scheduler methods completely,', () => {
325+
let infiniteLoopGuard;
326+
327+
beforeEach(() => {
328+
jest.resetModules();
329+
330+
infiniteLoopGuard = 0;
331+
332+
jest.mock('scheduler', () => {
333+
const actual = jest.requireActual('scheduler/unstable_mock');
334+
return {
335+
...actual,
336+
unstable_shouldYield() {
337+
// This simulates a bug report where `shouldYield` returns true in a
338+
// unit testing environment. Because `act` will keep working until
339+
// there's no more work left, it would fall into an infinite loop.
340+
// The fix is that when performing work inside `act`, we should bypass
341+
// `shouldYield` completely, because we can't trust it to be correct.
342+
if (infiniteLoopGuard++ > 100) {
343+
throw new Error('Detected an infinte loop');
344+
}
345+
return true;
346+
},
347+
};
348+
});
349+
350+
React = require('react');
351+
ReactNoop = require('react-noop-renderer');
352+
startTransition = React.startTransition;
353+
});
354+
355+
afterEach(() => {
356+
jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock'));
357+
});
358+
359+
it('inside `act`, does not call `shouldYield`, even during a concurrent render', async () => {
360+
function App() {
361+
return (
362+
<>
363+
<div>A</div>
364+
<div>B</div>
365+
<div>C</div>
366+
</>
367+
);
368+
}
369+
370+
const root = ReactNoop.createRoot();
371+
const publicAct = React.unstable_act;
372+
const prevIsReactActEnvironment = global.IS_REACT_ACT_ENVIRONMENT;
373+
try {
374+
global.IS_REACT_ACT_ENVIRONMENT = true;
375+
await publicAct(async () => {
376+
startTransition(() => root.render(<App />));
377+
});
378+
} finally {
379+
global.IS_REACT_ACT_ENVIRONMENT = prevIsReactActEnvironment;
380+
}
381+
expect(root).toMatchRenderedOutput(
382+
<>
383+
<div>A</div>
384+
<div>B</div>
385+
<div>C</div>
386+
</>,
387+
);
388+
});
389+
});

0 commit comments

Comments
 (0)