arkit
Version:
Visualises JavaScript, TypeScript and Flow codebases as meaningful and committable architecture diagrams
496 lines (425 loc) • 14.4 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
*/
;
describe('SimpleEventPlugin', function() {
let React;
let ReactDOM;
let ReactFeatureFlags;
let onClick;
let container;
function expectClickThru(element) {
element.click();
expect(onClick).toHaveBeenCalledTimes(1);
}
function expectNoClickThru(element) {
element.click();
expect(onClick).toHaveBeenCalledTimes(0);
}
function mounted(element) {
container = document.createElement('div');
document.body.appendChild(container);
element = ReactDOM.render(element, container);
return element;
}
beforeEach(function() {
// TODO pull this into helper method, reduce repetition.
// mock the browser APIs which are used in schedule:
// - requestAnimationFrame should pass the DOMHighResTimeStamp argument
// - calling 'window.postMessage' should actually fire postmessage handlers
global.requestAnimationFrame = function(cb) {
return setTimeout(() => {
cb(Date.now());
});
};
const originalAddEventListener = global.addEventListener;
let postMessageCallback;
global.addEventListener = function(eventName, callback, useCapture) {
if (eventName === 'message') {
postMessageCallback = callback;
} else {
originalAddEventListener(eventName, callback, useCapture);
}
};
global.postMessage = function(messageKey, targetOrigin) {
const postMessageEvent = {source: window, data: messageKey};
if (postMessageCallback) {
postMessageCallback(postMessageEvent);
}
};
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
onClick = jest.fn();
});
afterEach(() => {
if (container && document.body.contains(container)) {
document.body.removeChild(container);
container = null;
}
});
it('A non-interactive tags click when disabled', function() {
const element = <div onClick={onClick} />;
expectClickThru(mounted(element));
});
it('A non-interactive tags clicks bubble when disabled', function() {
const element = mounted(
<div onClick={onClick}>
<div />
</div>,
);
const child = element.firstChild;
child.click();
expect(onClick).toHaveBeenCalledTimes(1);
});
it('does not register a click when clicking a child of a disabled element', function() {
const element = mounted(
<button onClick={onClick} disabled={true}>
<span />
</button>,
);
const child = element.querySelector('span');
child.click();
expect(onClick).toHaveBeenCalledTimes(0);
});
it('triggers click events for children of disabled elements', function() {
const element = mounted(
<button disabled={true}>
<span onClick={onClick} />
</button>,
);
const child = element.querySelector('span');
child.click();
expect(onClick).toHaveBeenCalledTimes(1);
});
it('triggers parent captured click events when target is a child of a disabled elements', function() {
const element = mounted(
<div onClickCapture={onClick}>
<button disabled={true}>
<span />
</button>
</div>,
);
const child = element.querySelector('span');
child.click();
expect(onClick).toHaveBeenCalledTimes(1);
});
it('triggers captured click events for children of disabled elements', function() {
const element = mounted(
<button disabled={true}>
<span onClickCapture={onClick} />
</button>,
);
const child = element.querySelector('span');
child.click();
expect(onClick).toHaveBeenCalledTimes(1);
});
['button', 'input', 'select', 'textarea'].forEach(function(tagName) {
describe(tagName, function() {
it('should forward clicks when it starts out not disabled', () => {
const element = React.createElement(tagName, {
onClick: onClick,
});
expectClickThru(mounted(element));
});
it('should not forward clicks when it starts out disabled', () => {
const element = React.createElement(tagName, {
onClick: onClick,
disabled: true,
});
expectNoClickThru(mounted(element));
});
it('should forward clicks when it becomes not disabled', () => {
container = document.createElement('div');
document.body.appendChild(container);
let element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick, disabled: true}),
container,
);
element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick}),
container,
);
expectClickThru(element);
});
it('should not forward clicks when it becomes disabled', () => {
container = document.createElement('div');
document.body.appendChild(container);
let element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick}),
container,
);
element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick, disabled: true}),
container,
);
expectNoClickThru(element);
});
it('should work correctly if the listener is changed', () => {
container = document.createElement('div');
document.body.appendChild(container);
let element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick, disabled: true}),
container,
);
element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick, disabled: false}),
container,
);
expectClickThru(element);
});
});
});
it('batches updates that occur as a result of a nested event dispatch', () => {
container = document.createElement('div');
document.body.appendChild(container);
let ops = [];
let button;
class Button extends React.Component {
state = {count: 0};
increment = () =>
this.setState(state => ({
count: state.count + 1,
}));
componentDidUpdate() {
ops.push(`didUpdate - Count: ${this.state.count}`);
}
render() {
return (
<button
ref={el => (button = el)}
onFocus={this.increment}
onClick={() => {
// The focus call synchronously dispatches a nested event. All of
// the updates in this handler should be batched together.
this.increment();
button.focus();
this.increment();
}}>
Count: {this.state.count}
</button>
);
}
}
function click() {
button.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
}
ReactDOM.render(<Button />, container);
expect(button.textContent).toEqual('Count: 0');
expect(ops).toEqual([]);
click();
// There should be exactly one update.
expect(ops).toEqual(['didUpdate - Count: 3']);
expect(button.textContent).toEqual('Count: 3');
});
describe('interactive events, in async mode', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactDOM = require('react-dom');
});
it('flushes pending interactive work before extracting event handler', () => {
container = document.createElement('div');
const root = ReactDOM.unstable_createRoot(container);
document.body.appendChild(container);
let ops = [];
let button;
class Button extends React.Component {
state = {disabled: false};
onClick = () => {
// Perform some side-effect
ops.push('Side-effect');
// Disable the button
this.setState({disabled: true});
};
render() {
ops.push(
`render button: ${this.state.disabled ? 'disabled' : 'enabled'}`,
);
return (
<button
ref={el => (button = el)}
// Handler is removed after the first click
onClick={this.state.disabled ? null : this.onClick}
/>
);
}
}
// Initial mount
root.render(<Button />);
// Should not have flushed yet because it's async
expect(ops).toEqual([]);
expect(button).toBe(undefined);
// Flush async work
jest.runAllTimers();
expect(ops).toEqual(['render button: enabled']);
ops = [];
function click() {
button.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
}
// Click the button to trigger the side-effect
click();
expect(ops).toEqual([
// The handler fired
'Side-effect',
// but the component did not re-render yet, because it's async
]);
ops = [];
// Click the button again
click();
expect(ops).toEqual([
// Before handling this second click event, the previous interactive
// update is flushed
'render button: disabled',
// The event handler was removed from the button, so there's no second
// side-effect
]);
ops = [];
// The handler should not fire again no matter how many times we
// click the handler.
click();
click();
click();
click();
click();
jest.runAllTimers();
expect(ops).toEqual([]);
});
it('end result of many interactive updates is deterministic', () => {
container = document.createElement('div');
const root = ReactDOM.unstable_createRoot(container);
document.body.appendChild(container);
let button;
class Button extends React.Component {
state = {count: 0};
render() {
return (
<button
ref={el => (button = el)}
onClick={() =>
// Intentionally not using the updater form here
this.setState({count: this.state.count + 1})
}>
Count: {this.state.count}
</button>
);
}
}
// Initial mount
root.render(<Button />);
// Should not have flushed yet because it's async
expect(button).toBe(undefined);
// Flush async work
jest.runAllTimers();
expect(button.textContent).toEqual('Count: 0');
function click() {
button.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
}
// Click the button a single time
click();
// The counter should not have updated yet because it's async
expect(button.textContent).toEqual('Count: 0');
// Click the button many more times
click();
click();
click();
click();
click();
click();
// Flush the remaining work
jest.runAllTimers();
// The counter should equal the total number of clicks
expect(button.textContent).toEqual('Count: 7');
});
it('flushes lowest pending interactive priority', () => {
container = document.createElement('div');
document.body.appendChild(container);
let button;
class Button extends React.Component {
state = {lowPriCount: 0};
render() {
return (
<button
ref={el => (button = el)}
onClick={
// Intentionally not using the updater form here
() => this.setState({lowPriCount: this.state.lowPriCount + 1})
}>
High-pri count: {this.props.highPriCount}, Low-pri count:{' '}
{this.state.lowPriCount}
</button>
);
}
}
class Wrapper extends React.Component {
state = {highPriCount: 0};
render() {
return (
<div
onClick={
// Intentionally not using the updater form here
() => this.setState({highPriCount: this.state.highPriCount + 1})
}>
<React.unstable_ConcurrentMode>
<Button highPriCount={this.state.highPriCount} />
</React.unstable_ConcurrentMode>
</div>
);
}
}
// Initial mount
ReactDOM.render(<Wrapper />, container);
expect(button.textContent).toEqual('High-pri count: 0, Low-pri count: 0');
function click() {
button.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
}
// Click the button a single time
click();
// The high-pri counter should flush synchronously, but not the
// low-pri counter
expect(button.textContent).toEqual('High-pri count: 1, Low-pri count: 0');
// Click the button many more times
click();
click();
click();
click();
click();
click();
// Flush the remaining work
jest.runAllTimers();
// Both counters should equal the total number of clicks
expect(button.textContent).toEqual('High-pri count: 7, Low-pri count: 7');
});
});
describe('iOS bubbling click fix', function() {
// See http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
it('does not add a local click to interactive elements', function() {
container = document.createElement('div');
ReactDOM.render(<button onClick={onClick} />, container);
const node = container.firstChild;
node.dispatchEvent(new MouseEvent('click'));
expect(onClick).toHaveBeenCalledTimes(0);
});
it('adds a local click listener to non-interactive elements', function() {
container = document.createElement('div');
ReactDOM.render(<div onClick={onClick} />, container);
const node = container.firstChild;
node.dispatchEvent(new MouseEvent('click'));
expect(onClick).toHaveBeenCalledTimes(0);
});
});
});