'use strict';
let React;
let ReactNoop;
let Scheduler;
let act;
let NormalPriority;
let IdlePriority;
let runWithPriority;
let startTransition;
let waitForAll;
let waitForPaint;
let assertLog;
let waitFor;
describe('ReactSchedulerIntegration', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
NormalPriority = Scheduler.unstable_NormalPriority;
IdlePriority = Scheduler.unstable_IdlePriority;
runWithPriority = Scheduler.unstable_runWithPriority;
startTransition = React.startTransition;
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitForPaint = InternalTestUtils.waitForPaint;
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;
});
function LegacyHiddenDiv({children, mode}) {
return (
<div hidden={mode === 'hidden'}>
<React.unstable_LegacyHidden
mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
{children}
</React.unstable_LegacyHidden>
</div>
);
}
it('passive effects are called before Normal-pri scheduled in layout effects', async () => {
const {useEffect, useLayoutEffect} = React;
function Effects({step}) {
useLayoutEffect(() => {
Scheduler.log('Layout Effect');
Scheduler.unstable_scheduleCallback(NormalPriority, () =>
Scheduler.log('Scheduled Normal Callback from Layout Effect'),
);
});
useEffect(() => {
Scheduler.log('Passive Effect');
});
return null;
}
function CleanupEffect() {
useLayoutEffect(() => () => {
Scheduler.log('Cleanup Layout Effect');
Scheduler.unstable_scheduleCallback(NormalPriority, () =>
Scheduler.log('Scheduled Normal Callback from Cleanup Layout Effect'),
);
});
return null;
}
await act(() => {
ReactNoop.render(<CleanupEffect />);
});
assertLog([]);
await act(() => {
ReactNoop.render(<Effects />);
});
assertLog([
'Cleanup Layout Effect',
'Layout Effect',
'Passive Effect',
'Scheduled Normal Callback from Cleanup Layout Effect',
'Scheduled Normal Callback from Layout Effect',
]);
});
it('requests a paint after committing', async () => {
const scheduleCallback = Scheduler.unstable_scheduleCallback;
const root = ReactNoop.createRoot();
root.render('Initial');
await waitForAll([]);
expect(root).toMatchRenderedOutput('Initial');
scheduleCallback(NormalPriority, () => Scheduler.log('A'));
scheduleCallback(NormalPriority, () => Scheduler.log('B'));
scheduleCallback(NormalPriority, () => Scheduler.log('C'));
React.startTransition(() => {
root.render('Update');
});
await waitFor(['A']);
scheduleCallback(NormalPriority, () => Scheduler.log('D'));
scheduleCallback(NormalPriority, () => Scheduler.log('E'));
await waitForPaint(['B', 'C']);
expect(root).toMatchRenderedOutput('Update');
await waitForAll(['D', 'E']);
});
it('idle updates are not blocked by offscreen work', async () => {
function Text({text}) {
Scheduler.log(text);
return text;
}
function App({label}) {
return (
<>
<Text text={`Visible: ` + label} />
<LegacyHiddenDiv mode="hidden">
<Text text={`Hidden: ` + label} />
</LegacyHiddenDiv>
</>
);
}
const root = ReactNoop.createRoot();
await act(async () => {
root.render(<App label="A" />);
await waitForPaint(['Visible: A']);
expect(root).toMatchRenderedOutput(
<>
Visible: A
<div hidden={true} />
</>,
);
runWithPriority(IdlePriority, () => {
root.render(<App label="B" />);
});
await waitForPaint(['Visible: B']);
expect(root).toMatchRenderedOutput(
<>
Visible: B
<div hidden={true} />
</>,
);
});
assertLog(['Hidden: B']);
expect(root).toMatchRenderedOutput(
<>
Visible: B<div hidden={true}>Hidden: B</div>
</>,
);
});
});
describe(
'regression test: does not infinite loop if `shouldYield` returns ' +
'true after a partial tree expires',
() => {
let logDuringShouldYield = false;
beforeEach(() => {
jest.resetModules();
jest.mock('scheduler', () => {
const actual = jest.requireActual('scheduler/unstable_mock');
return {
...actual,
unstable_shouldYield() {
if (logDuringShouldYield) {
actual.log('shouldYield');
}
return actual.unstable_shouldYield();
},
};
});
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
startTransition = React.startTransition;
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitForPaint = InternalTestUtils.waitForPaint;
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;
act = InternalTestUtils.act;
});
afterEach(() => {
jest.mock('scheduler', () =>
jest.requireActual('scheduler/unstable_mock'),
);
});
it('using public APIs to trigger real world scenario', async () => {
function Text({text}) {
return text;
}
function App({step}) {
return (
<>
<Text text="A" />
<TriggerErstwhileSchedulerBug />
<Text text="B" />
<TriggerErstwhileSchedulerBug />
<Text text="C" />
</>
);
}
function TriggerErstwhileSchedulerBug() {
Scheduler.unstable_advanceTime(10000);
Scheduler.unstable_requestPaint();
return null;
}
await act(async () => {
ReactNoop.render(<App />);
await waitForPaint([]);
await waitForPaint([]);
});
});
it('mock Scheduler module to check if `shouldYield` is called', async () => {
function Text({text}) {
Scheduler.log(text);
return text;
}
function App({step}) {
return (
<>
<Text text="A" />
<Text text="B" />
<Text text="C" />
</>
);
}
await act(async () => {
startTransition(() => {
ReactNoop.render(<App />);
});
await waitFor(['A']);
logDuringShouldYield = true;
await waitFor(['shouldYield']);
Scheduler.unstable_advanceTime(10000);
startTransition(() => {
ReactNoop.render(<App />);
});
await waitFor(['B', 'C']);
});
});
},
);
describe('`act` bypasses Scheduler methods completely,', () => {
let infiniteLoopGuard;
beforeEach(() => {
jest.resetModules();
infiniteLoopGuard = 0;
jest.mock('scheduler', () => {
const actual = jest.requireActual('scheduler/unstable_mock');
return {
...actual,
unstable_shouldYield() {
if (infiniteLoopGuard++ > 100) {
throw new Error('Detected an infinite loop');
}
return true;
},
};
});
React = require('react');
ReactNoop = require('react-noop-renderer');
startTransition = React.startTransition;
});
afterEach(() => {
jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock'));
});
it('inside `act`, does not call `shouldYield`, even during a concurrent render', async () => {
function App() {
return (
<>
<div>A</div>
<div>B</div>
<div>C</div>
</>
);
}
const root = ReactNoop.createRoot();
const publicAct = React.act;
const prevIsReactActEnvironment = global.IS_REACT_ACT_ENVIRONMENT;
try {
global.IS_REACT_ACT_ENVIRONMENT = true;
await publicAct(async () => {
startTransition(() => root.render(<App />));
});
} finally {
global.IS_REACT_ACT_ENVIRONMENT = prevIsReactActEnvironment;
}
expect(root).toMatchRenderedOutput(
<>
<div>A</div>
<div>B</div>
<div>C</div>
</>,
);
});
});