arkit
Version:
Visualises JavaScript, TypeScript and Flow codebases as meaningful and committable architecture diagrams
369 lines (321 loc) • 11.7 kB
JavaScript
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
;
let EventPluginHub;
let EventPluginRegistry;
let React;
let ReactDOM;
let ReactDOMComponentTree;
let ReactBrowserEventEmitter;
let ReactTestUtils;
let idCallOrder;
const recordID = function(id) {
idCallOrder.push(id);
};
const recordIDAndStopPropagation = function(id, event) {
recordID(id);
event.stopPropagation();
};
const recordIDAndReturnFalse = function(id, event) {
recordID(id);
return false;
};
const LISTENER = jest.fn();
const ON_CLICK_KEY = 'onClick';
const ON_CHANGE_KEY = 'onChange';
const ON_MOUSE_ENTER_KEY = 'onMouseEnter';
let GRANDPARENT;
let PARENT;
let CHILD;
let getListener;
let putListener;
let deleteAllListeners;
let container;
function registerSimpleTestHandler() {
putListener(CHILD, ON_CLICK_KEY, LISTENER);
const listener = getListener(CHILD, ON_CLICK_KEY);
expect(listener).toEqual(LISTENER);
return getListener(CHILD, ON_CLICK_KEY);
}
describe('ReactBrowserEventEmitter', () => {
beforeEach(() => {
jest.resetModules();
LISTENER.mockClear();
// TODO: can we express this test with only public API?
EventPluginHub = require('events/EventPluginHub');
EventPluginRegistry = require('events/EventPluginRegistry');
React = require('react');
ReactDOM = require('react-dom');
ReactDOMComponentTree = require('../client/ReactDOMComponentTree');
ReactBrowserEventEmitter = require('../events/ReactBrowserEventEmitter');
ReactTestUtils = require('react-dom/test-utils');
container = document.createElement('div');
document.body.appendChild(container);
let GRANDPARENT_PROPS = {};
let PARENT_PROPS = {};
let CHILD_PROPS = {};
function Child(props) {
return <div ref={c => (CHILD = c)} {...props} />;
}
class ChildWrapper extends React.PureComponent {
render() {
return <Child {...this.props} />;
}
}
function renderTree() {
ReactDOM.render(
<div ref={c => (GRANDPARENT = c)} {...GRANDPARENT_PROPS}>
<div ref={c => (PARENT = c)} {...PARENT_PROPS}>
<ChildWrapper {...CHILD_PROPS} />
</div>
</div>,
container,
);
}
renderTree();
getListener = function(node, eventName) {
const inst = ReactDOMComponentTree.getInstanceFromNode(node);
return EventPluginHub.getListener(inst, eventName);
};
putListener = function(node, eventName, listener) {
switch (node) {
case CHILD:
CHILD_PROPS[eventName] = listener;
break;
case PARENT:
PARENT_PROPS[eventName] = listener;
break;
case GRANDPARENT:
GRANDPARENT_PROPS[eventName] = listener;
break;
}
// Rerender with new event listeners
renderTree();
};
deleteAllListeners = function(node) {
switch (node) {
case CHILD:
CHILD_PROPS = {};
break;
case PARENT:
PARENT_PROPS = {};
break;
case GRANDPARENT:
GRANDPARENT_PROPS = {};
break;
}
renderTree();
};
idCallOrder = [];
});
afterEach(() => {
document.body.removeChild(container);
container = null;
});
it('should store a listener correctly', () => {
registerSimpleTestHandler();
const listener = getListener(CHILD, ON_CLICK_KEY);
expect(listener).toBe(LISTENER);
});
it('should retrieve a listener correctly', () => {
registerSimpleTestHandler();
const listener = getListener(CHILD, ON_CLICK_KEY);
expect(listener).toEqual(LISTENER);
});
it('should clear all handlers when asked to', () => {
registerSimpleTestHandler();
deleteAllListeners(CHILD);
const listener = getListener(CHILD, ON_CLICK_KEY);
expect(listener).toBe(undefined);
});
it('should invoke a simple handler registered on a node', () => {
registerSimpleTestHandler();
CHILD.click();
expect(LISTENER).toHaveBeenCalledTimes(1);
});
it('should not invoke handlers if ReactBrowserEventEmitter is disabled', () => {
registerSimpleTestHandler();
ReactBrowserEventEmitter.setEnabled(false);
CHILD.click();
expect(LISTENER).toHaveBeenCalledTimes(0);
ReactBrowserEventEmitter.setEnabled(true);
CHILD.click();
expect(LISTENER).toHaveBeenCalledTimes(1);
});
it('should bubble simply', () => {
putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD));
putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, PARENT));
putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT));
CHILD.click();
expect(idCallOrder.length).toBe(3);
expect(idCallOrder[0]).toBe(CHILD);
expect(idCallOrder[1]).toBe(PARENT);
expect(idCallOrder[2]).toBe(GRANDPARENT);
});
it('should bubble to the right handler after an update', () => {
putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, 'GRANDPARENT'));
putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, 'PARENT'));
putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, 'CHILD'));
CHILD.click();
expect(idCallOrder).toEqual(['CHILD', 'PARENT', 'GRANDPARENT']);
idCallOrder = [];
// Update just the grand parent without updating the child.
putListener(
GRANDPARENT,
ON_CLICK_KEY,
recordID.bind(null, 'UPDATED_GRANDPARENT'),
);
CHILD.click();
expect(idCallOrder).toEqual(['CHILD', 'PARENT', 'UPDATED_GRANDPARENT']);
});
it('should continue bubbling if an error is thrown', () => {
putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD));
putListener(PARENT, ON_CLICK_KEY, function() {
recordID(PARENT);
throw new Error('Handler interrupted');
});
putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT));
expect(function() {
ReactTestUtils.Simulate.click(CHILD);
}).toThrow();
expect(idCallOrder.length).toBe(3);
expect(idCallOrder[0]).toBe(CHILD);
expect(idCallOrder[1]).toBe(PARENT);
expect(idCallOrder[2]).toBe(GRANDPARENT);
});
it('should set currentTarget', () => {
putListener(CHILD, ON_CLICK_KEY, function(event) {
recordID(CHILD);
expect(event.currentTarget).toBe(CHILD);
});
putListener(PARENT, ON_CLICK_KEY, function(event) {
recordID(PARENT);
expect(event.currentTarget).toBe(PARENT);
});
putListener(GRANDPARENT, ON_CLICK_KEY, function(event) {
recordID(GRANDPARENT);
expect(event.currentTarget).toBe(GRANDPARENT);
});
CHILD.click();
expect(idCallOrder.length).toBe(3);
expect(idCallOrder[0]).toBe(CHILD);
expect(idCallOrder[1]).toBe(PARENT);
expect(idCallOrder[2]).toBe(GRANDPARENT);
});
it('should support stopPropagation()', () => {
putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD));
putListener(
PARENT,
ON_CLICK_KEY,
recordIDAndStopPropagation.bind(null, PARENT),
);
putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT));
CHILD.click();
expect(idCallOrder.length).toBe(2);
expect(idCallOrder[0]).toBe(CHILD);
expect(idCallOrder[1]).toBe(PARENT);
});
it('should support overriding .isPropagationStopped()', () => {
// Ew. See D4504876.
putListener(CHILD, ON_CLICK_KEY, recordID.bind(null, CHILD));
putListener(PARENT, ON_CLICK_KEY, function(e) {
recordID(PARENT, e);
// This stops React bubbling but avoids touching the native event
e.isPropagationStopped = () => true;
});
putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT));
CHILD.click();
expect(idCallOrder.length).toBe(2);
expect(idCallOrder[0]).toBe(CHILD);
expect(idCallOrder[1]).toBe(PARENT);
});
it('should stop after first dispatch if stopPropagation', () => {
putListener(
CHILD,
ON_CLICK_KEY,
recordIDAndStopPropagation.bind(null, CHILD),
);
putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, PARENT));
putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT));
CHILD.click();
expect(idCallOrder.length).toBe(1);
expect(idCallOrder[0]).toBe(CHILD);
});
it('should not stopPropagation if false is returned', () => {
putListener(CHILD, ON_CLICK_KEY, recordIDAndReturnFalse.bind(null, CHILD));
putListener(PARENT, ON_CLICK_KEY, recordID.bind(null, PARENT));
putListener(GRANDPARENT, ON_CLICK_KEY, recordID.bind(null, GRANDPARENT));
CHILD.click();
expect(idCallOrder.length).toBe(3);
expect(idCallOrder[0]).toBe(CHILD);
expect(idCallOrder[1]).toBe(PARENT);
expect(idCallOrder[2]).toBe(GRANDPARENT);
});
/**
* The entire event registration state of the world should be "locked-in" at
* the time the event occurs. This is to resolve many edge cases that come
* about from a listener on a lower-in-DOM node causing structural changes at
* places higher in the DOM. If this lower-in-DOM node causes new content to
* be rendered at a place higher-in-DOM, we need to be careful not to invoke
* these new listeners.
*/
it('should invoke handlers that were removed while bubbling', () => {
const handleParentClick = jest.fn();
const handleChildClick = function(event) {
deleteAllListeners(PARENT);
};
putListener(CHILD, ON_CLICK_KEY, handleChildClick);
putListener(PARENT, ON_CLICK_KEY, handleParentClick);
CHILD.click();
expect(handleParentClick).toHaveBeenCalledTimes(1);
});
it('should not invoke newly inserted handlers while bubbling', () => {
const handleParentClick = jest.fn();
const handleChildClick = function(event) {
putListener(PARENT, ON_CLICK_KEY, handleParentClick);
};
putListener(CHILD, ON_CLICK_KEY, handleChildClick);
CHILD.click();
expect(handleParentClick).toHaveBeenCalledTimes(0);
});
it('should have mouse enter simulated by test utils', () => {
putListener(CHILD, ON_MOUSE_ENTER_KEY, recordID.bind(null, CHILD));
ReactTestUtils.Simulate.mouseEnter(CHILD);
expect(idCallOrder.length).toBe(1);
expect(idCallOrder[0]).toBe(CHILD);
});
it('should listen to events only once', () => {
spyOnDevAndProd(EventTarget.prototype, 'addEventListener');
ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document);
ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document);
expect(EventTarget.prototype.addEventListener).toHaveBeenCalledTimes(1);
});
it('should work with event plugins without dependencies', () => {
spyOnDevAndProd(EventTarget.prototype, 'addEventListener');
ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document);
expect(EventTarget.prototype.addEventListener.calls.argsFor(0)[0]).toBe(
'click',
);
});
it('should work with event plugins with dependencies', () => {
spyOnDevAndProd(EventTarget.prototype, 'addEventListener');
ReactBrowserEventEmitter.listenTo(ON_CHANGE_KEY, document);
const setEventListeners = [];
const listenCalls = EventTarget.prototype.addEventListener.calls.allArgs();
for (let i = 0; i < listenCalls.length; i++) {
setEventListeners.push(listenCalls[i][1]);
}
const module = EventPluginRegistry.registrationNameModules[ON_CHANGE_KEY];
const dependencies = module.eventTypes.change.dependencies;
expect(setEventListeners.length).toEqual(dependencies.length);
for (let i = 0; i < setEventListeners.length; i++) {
expect(dependencies.indexOf(setEventListeners[i])).toBeTruthy();
}
});
});