Skip to content

Commit

Permalink
Add test for mouseover replaying
Browse files Browse the repository at this point in the history
We need to check if the "relatedTarget" is mounted due to how the old
event system dispatches from the "out" event.
  • Loading branch information
sebmarkbage committed Sep 17, 2019
1 parent 43fa780 commit c6d5872
Show file tree
Hide file tree
Showing 2 changed files with 282 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,58 @@ let ReactFeatureFlags;
let Suspense;
let SuspenseList;
let act;
let useHover;

function dispatchMouseEvent(to, from) {
if (!to) {
to = null;
}
if (!from) {
from = null;
}
if (from) {
const mouseOutEvent = document.createEvent('MouseEvents');
mouseOutEvent.initMouseEvent(
'mouseout',
true,
true,
window,
0,
50,
50,
50,
50,
false,
false,
false,
false,
0,
to,
);
from.dispatchEvent(mouseOutEvent);
}
if (to) {
const mouseOverEvent = document.createEvent('MouseEvents');
mouseOverEvent.initMouseEvent(
'mouseover',
true,
true,
window,
0,
50,
50,
50,
50,
false,
false,
false,
false,
0,
from,
);
to.dispatchEvent(mouseOverEvent);
}
}

describe('ReactDOMServerPartialHydration', () => {
beforeEach(() => {
Expand All @@ -34,6 +86,8 @@ describe('ReactDOMServerPartialHydration', () => {
Scheduler = require('scheduler');
Suspense = React.Suspense;
SuspenseList = React.unstable_SuspenseList;

useHover = require('react-events/hover').useHover;
});

it('hydrates a parent even if a child Suspense boundary is blocked', async () => {
Expand Down Expand Up @@ -2040,4 +2094,223 @@ describe('ReactDOMServerPartialHydration', () => {

document.body.removeChild(parentContainer);
});

it('blocks only on the last continuous event (legacy system)', async () => {
let suspend1 = false;
let resolve1;
let promise1 = new Promise(resolvePromise => (resolve1 = resolvePromise));
let suspend2 = false;
let resolve2;
let promise2 = new Promise(resolvePromise => (resolve2 = resolvePromise));

function First({text}) {
if (suspend1) {
throw promise1;
} else {
return 'Hello';
}
}

function Second({text}) {
if (suspend2) {
throw promise2;
} else {
return 'World';
}
}

let ops = [];

function App() {
return (
<div>
<Suspense fallback="Loading First...">
<span
onMouseEnter={() => ops.push('Mouse Enter First')}
onMouseLeave={() => ops.push('Mouse Leave First')}
/>
{/* We suspend after to test what happens when we eager
attach the listener. */}
<First />
</Suspense>
<Suspense fallback="Loading Second...">
<span
onMouseEnter={() => ops.push('Mouse Enter Second')}
onMouseLeave={() => ops.push('Mouse Leave Second')}>
<Second />
</span>
</Suspense>
</div>
);
}

let finalHTML = ReactDOMServer.renderToString(<App />);
let container = document.createElement('div');
container.innerHTML = finalHTML;

// We need this to be in the document since we'll dispatch events on it.
document.body.appendChild(container);

let appDiv = container.getElementsByTagName('div')[0];
let firstSpan = appDiv.getElementsByTagName('span')[0];
let secondSpan = appDiv.getElementsByTagName('span')[1];
expect(firstSpan.textContent).toBe('');
expect(secondSpan.textContent).toBe('World');

// On the client we don't have all data yet but we want to start
// hydrating anyway.
suspend1 = true;
suspend2 = true;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App />);

Scheduler.unstable_flushAll();
jest.runAllTimers();

dispatchMouseEvent(appDiv, null);
dispatchMouseEvent(firstSpan, appDiv);
dispatchMouseEvent(secondSpan, firstSpan);

// Neither target is yet hydrated.
expect(ops).toEqual([]);

// Resolving the second promise so that rendering can complete.
suspend2 = false;
resolve2();
await promise2;

Scheduler.unstable_flushAll();
jest.runAllTimers();

// We've unblocked the current hover target so we should be
// able to replay it now.
expect(ops).toEqual(['Mouse Enter Second']);

// Resolving the first promise has no effect now.
suspend1 = false;
resolve1();
await promise1;

Scheduler.unstable_flushAll();
jest.runAllTimers();

expect(ops).toEqual(['Mouse Enter Second']);

document.body.removeChild(container);
});

it('blocks only on the last continuous event (Responder system)', async () => {
let suspend1 = false;
let resolve1;
let promise1 = new Promise(resolvePromise => (resolve1 = resolvePromise));
let suspend2 = false;
let resolve2;
let promise2 = new Promise(resolvePromise => (resolve2 = resolvePromise));

function First({text}) {
if (suspend1) {
throw promise1;
} else {
return 'Hello';
}
}

function Second({text}) {
if (suspend2) {
throw promise2;
} else {
return 'World';
}
}

let ops = [];

function App() {
const listener1 = useHover({
onHoverStart() {
ops.push('Hover Start First');
},
onHoverEnd() {
ops.push('Hover End First');
},
});
const listener2 = useHover({
onHoverStart() {
ops.push('Hover Start Second');
},
onHoverEnd() {
ops.push('Hover End Second');
},
});
return (
<div>
<Suspense fallback="Loading First...">
<span listeners={listener1} />
{/* We suspend after to test what happens when we eager
attach the listener. */}
<First />
</Suspense>
<Suspense fallback="Loading Second...">
<span listeners={listener2}>
<Second />
</span>
</Suspense>
</div>
);
}

let finalHTML = ReactDOMServer.renderToString(<App />);
let container = document.createElement('div');
container.innerHTML = finalHTML;

// We need this to be in the document since we'll dispatch events on it.
document.body.appendChild(container);

let appDiv = container.getElementsByTagName('div')[0];
let firstSpan = appDiv.getElementsByTagName('span')[0];
let secondSpan = appDiv.getElementsByTagName('span')[1];
expect(firstSpan.textContent).toBe('');
expect(secondSpan.textContent).toBe('World');

// On the client we don't have all data yet but we want to start
// hydrating anyway.
suspend1 = true;
suspend2 = true;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App />);

Scheduler.unstable_flushAll();
jest.runAllTimers();

dispatchMouseEvent(appDiv, null);
dispatchMouseEvent(firstSpan, appDiv);
dispatchMouseEvent(secondSpan, firstSpan);

// Neither target is yet hydrated.
expect(ops).toEqual([]);

// Resolving the second promise so that rendering can complete.
suspend2 = false;
resolve2();
await promise2;

Scheduler.unstable_flushAll();
jest.runAllTimers();

// We've unblocked the current hover target so we should be
// able to replay it now.
expect(ops).toEqual(['Hover Start Second']);

// Resolving the first promise has no effect now.
suspend1 = false;
resolve1();
await promise1;

Scheduler.unstable_flushAll();
jest.runAllTimers();

expect(ops).toEqual(['Hover Start Second']);

document.body.removeChild(container);
});
});
11 changes: 9 additions & 2 deletions packages/react-dom/src/events/EnterLeaveEventPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
getNodeFromInstance,
} from '../client/ReactDOMComponentTree';
import {HostComponent, HostText} from 'shared/ReactWorkTags';
import {getNearestMountedFiber} from 'react-reconciler/reflection';

const eventTypes = {
mouseEnter: {
Expand Down Expand Up @@ -100,8 +101,14 @@ const EnterLeaveEventPlugin = {
from = targetInst;
const related = nativeEvent.relatedTarget || nativeEvent.toElement;
to = related ? getClosestInstanceFromNode(related) : null;
if (to !== null && to.tag !== HostComponent && to.tag !== HostText) {
to = null;
if (to !== null) {
const nearestMounted = getNearestMountedFiber(to);
if (
to !== nearestMounted ||
(to.tag !== HostComponent && to.tag !== HostText)
) {
to = null;
}
}
} else {
// Moving to a node from outside the window.
Expand Down

0 comments on commit c6d5872

Please sign in to comment.