'use strict';
let React;
let ReactDOM;
let ReactDOMClient;
let Scheduler;
let act;
let assertLog;
let waitFor;
describe('ReactDOMNativeEventHeuristic-test', () => {
let container;
beforeEach(() => {
jest.resetModules();
container = document.createElement('div');
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitFor = InternalTestUtils.waitFor;
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
function dispatchAndSetCurrentEvent(el, event) {
try {
window.event = event;
el.dispatchEvent(event);
} finally {
window.event = undefined;
}
}
it('ignores discrete events on a pending removed element', async () => {
const disableButtonRef = React.createRef();
const submitButtonRef = React.createRef();
function Form() {
const [active, setActive] = React.useState(true);
React.useLayoutEffect(() => {
disableButtonRef.current.onclick = disableForm;
});
function disableForm() {
setActive(false);
}
return (
<div>
<button ref={disableButtonRef}>Disable</button>
{active ? <button ref={submitButtonRef}>Submit</button> : null}
</div>
);
}
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Form />);
});
const disableButton = disableButtonRef.current;
expect(disableButton.tagName).toBe('BUTTON');
await act(async () => {
const firstEvent = document.createEvent('Event');
firstEvent.initEvent('click', true, true);
dispatchAndSetCurrentEvent(disableButton, firstEvent);
});
expect(submitButtonRef.current).toBe(null);
});
it('ignores discrete events on a pending removed event listener', async () => {
const disableButtonRef = React.createRef();
const submitButtonRef = React.createRef();
let formSubmitted = false;
function Form() {
const [active, setActive] = React.useState(true);
React.useLayoutEffect(() => {
disableButtonRef.current.onclick = disableForm;
submitButtonRef.current.onclick = active
? submitForm
: disabledSubmitForm;
});
function disableForm() {
setActive(false);
}
function submitForm() {
formSubmitted = true;
}
function disabledSubmitForm() {
}
return (
<div>
<button ref={disableButtonRef}>Disable</button>
<button ref={submitButtonRef}>Submit</button>
</div>
);
}
const root = ReactDOMClient.createRoot(container);
await act(() => root.render(<Form />));
const disableButton = disableButtonRef.current;
expect(disableButton.tagName).toBe('BUTTON');
const firstEvent = document.createEvent('Event');
firstEvent.initEvent('click', true, true);
await act(() => {
dispatchAndSetCurrentEvent(disableButton, firstEvent);
const submitButton = submitButtonRef.current;
expect(submitButton.tagName).toBe('BUTTON');
ReactDOM.flushSync();
const secondEvent = document.createEvent('Event');
secondEvent.initEvent('click', true, true);
dispatchAndSetCurrentEvent(submitButton, secondEvent);
});
expect(formSubmitted).toBe(false);
});
it('uses the newest discrete events on a pending changed event listener', async () => {
const enableButtonRef = React.createRef();
const submitButtonRef = React.createRef();
let formSubmitted = false;
function Form() {
const [active, setActive] = React.useState(false);
React.useLayoutEffect(() => {
enableButtonRef.current.onclick = enableForm;
submitButtonRef.current.onclick = active ? submitForm : null;
});
function enableForm() {
setActive(true);
}
function submitForm() {
formSubmitted = true;
}
return (
<div>
<button ref={enableButtonRef}>Enable</button>
<button ref={submitButtonRef}>Submit</button>
</div>
);
}
const root = ReactDOMClient.createRoot(container);
await act(() => root.render(<Form />));
const enableButton = enableButtonRef.current;
expect(enableButton.tagName).toBe('BUTTON');
await act(() => {
const firstEvent = document.createEvent('Event');
firstEvent.initEvent('click', true, true);
dispatchAndSetCurrentEvent(enableButton, firstEvent);
const submitButton = submitButtonRef.current;
expect(submitButton.tagName).toBe('BUTTON');
ReactDOM.flushSync();
const secondEvent = document.createEvent('Event');
secondEvent.initEvent('click', true, true);
dispatchAndSetCurrentEvent(submitButton, secondEvent);
});
expect(formSubmitted).toBe(true);
});
it('mouse over should be user-blocking but not discrete', async () => {
const root = ReactDOMClient.createRoot(container);
const target = React.createRef(null);
function Foo() {
const [isHover, setHover] = React.useState(false);
React.useLayoutEffect(() => {
target.current.onmouseover = () => setHover(true);
});
return <div ref={target}>{isHover ? 'hovered' : 'not hovered'}</div>;
}
await act(() => {
root.render(<Foo />);
});
expect(container.textContent).toEqual('not hovered');
await act(() => {
const mouseOverEvent = document.createEvent('MouseEvents');
mouseOverEvent.initEvent('mouseover', true, true);
dispatchAndSetCurrentEvent(target.current, mouseOverEvent);
ReactDOM.flushSync();
expect(container.textContent).toEqual('not hovered');
});
expect(container.textContent).toEqual('hovered');
});
it('mouse enter should be user-blocking but not discrete', async () => {
const root = ReactDOMClient.createRoot(container);
const target = React.createRef(null);
function Foo() {
const [isHover, setHover] = React.useState(false);
React.useLayoutEffect(() => {
target.current.onmouseenter = () => setHover(true);
});
return <div ref={target}>{isHover ? 'hovered' : 'not hovered'}</div>;
}
await act(() => {
root.render(<Foo />);
});
expect(container.textContent).toEqual('not hovered');
await act(() => {
const mouseEnterEvent = document.createEvent('MouseEvents');
mouseEnterEvent.initEvent('mouseenter', true, true);
dispatchAndSetCurrentEvent(target.current, mouseEnterEvent);
ReactDOM.flushSync();
expect(container.textContent).toEqual('not hovered');
});
expect(container.textContent).toEqual('hovered');
});
it('continuous native events flush as expected', async () => {
const root = ReactDOMClient.createRoot(container);
const target = React.createRef(null);
function Foo({hovered}) {
const hoverString = hovered ? 'hovered' : 'not hovered';
Scheduler.log(hoverString);
return <div ref={target}>{hoverString}</div>;
}
await act(() => {
root.render(<Foo hovered={false} />);
});
expect(container.textContent).toEqual('not hovered');
assertLog(['not hovered']);
await act(async () => {
const mouseEnterEvent = document.createEvent('MouseEvents');
mouseEnterEvent.initEvent('mouseover', true, true);
target.current.addEventListener('mouseover', () => {
root.render(<Foo hovered={true} />);
});
dispatchAndSetCurrentEvent(target.current, mouseEnterEvent);
assertLog([]);
expect(container.textContent).toEqual('not hovered');
await waitFor(['hovered']);
expect(container.textContent).toEqual('hovered');
});
expect(container.textContent).toEqual('hovered');
});
it('should batch inside native events', async () => {
const root = ReactDOMClient.createRoot(container);
const target = React.createRef(null);
function Foo() {
const [count, setCount] = React.useState(0);
const countRef = React.useRef(-1);
React.useLayoutEffect(() => {
countRef.current = count;
target.current.onclick = () => {
setCount(countRef.current + 1);
setCount(countRef.current + 1);
};
});
return <div ref={target}>Count: {count}</div>;
}
await act(() => {
root.render(<Foo />);
});
expect(container.textContent).toEqual('Count: 0');
await act(async () => {
const pressEvent = document.createEvent('Event');
pressEvent.initEvent('click', true, true);
dispatchAndSetCurrentEvent(target.current, pressEvent);
});
expect(container.textContent).toEqual('Count: 1');
});
it('should not flush discrete events at the end of outermost batchedUpdates', async () => {
const root = ReactDOMClient.createRoot(container);
let target;
function Foo() {
const [count, setCount] = React.useState(0);
return (
<div
ref={el => {
target = el;
if (target !== null) {
el.onclick = () => {
ReactDOM.unstable_batchedUpdates(() => {
setCount(count + 1);
});
Scheduler.log(
container.textContent + ' [after batchedUpdates]',
);
};
}
}}>
Count: {count}
</div>
);
}
await act(() => {
root.render(<Foo />);
});
expect(container.textContent).toEqual('Count: 0');
await act(async () => {
const pressEvent = document.createEvent('Event');
pressEvent.initEvent('click', true, true);
dispatchAndSetCurrentEvent(target, pressEvent);
assertLog(['Count: 0 [after batchedUpdates]']);
expect(container.textContent).toEqual('Count: 0');
});
expect(container.textContent).toEqual('Count: 1');
});
});