react-event-listener
Version:
A React component that allow to bind events on the global scope
254 lines (207 loc) • 7.26 kB
JavaScript
/* eslint-env mocha */
import React from 'react';
import PropTypes from 'prop-types';
import { shallow } from 'enzyme';
import { assert } from 'chai';
import { spy } from 'sinon';
import { render, unmountComponentAtNode } from 'react-dom';
import { Simulate } from 'react-dom/test-utils';
import EventListener, { withOptions } from './index';
describe('EventListener', () => {
describe('prop: children', () => {
it('should work without', () => {
const wrapper = shallow(<EventListener target="window" />);
assert.strictEqual(wrapper.children().length, 0, 'Should work without children');
});
it('should render it', () => {
const wrapper = shallow(
<EventListener target="window">
<div>Foo</div>
</EventListener>,
);
assert.strictEqual(wrapper.children().length, 1, 'Should render his children.');
});
});
let node;
beforeEach(() => {
// Pattern from "react-router": http://git.io/vWpfs
node = document.createElement('div');
document.body.appendChild(node);
});
afterEach(() => {
unmountComponentAtNode(node);
node.parentNode.removeChild(node);
});
describe('prop: target', () => {
it('should work with a string', () => {
const handleClick = spy();
render(<EventListener target="document" onClick={handleClick} />, node);
document.body.click();
assert.strictEqual(handleClick.callCount, 1);
});
it('should work with a node', () => {
const handleClick = spy();
render(<EventListener target={document} onClick={handleClick} />, node);
document.body.click();
assert.strictEqual(handleClick.callCount, 1);
});
});
[
{
contextName: 'using Simulate.click(extraNode)',
name: 'should not invoke event listener to document',
invokeFn(extraNode) {
Simulate.click(extraNode);
},
expectFn(handle) {
assert.strictEqual(handle.callCount, 0);
},
},
{
contextName: 'using extraNode.click()',
name: 'should invoke event listener to document',
invokeFn(extraNode) {
extraNode.click();
},
expectFn(handle) {
assert.strictEqual(handle.callCount, 1);
},
},
].forEach(({ contextName, name, invokeFn, expectFn }) => {
describe(contextName, () => {
it(name, () => {
class TextComponent extends React.Component {
static propTypes = {
onClick: PropTypes.func,
};
handleClick = () => {
this.props.onClick();
};
render() {
return <EventListener target={document} onClick={this.handleClick} />;
}
}
const handleClick = spy();
render(<TextComponent onClick={handleClick} />, node);
assert.strictEqual(handleClick.callCount, 0);
const extraNode = document.createElement('button');
document.body.appendChild(extraNode);
invokeFn(extraNode);
expectFn(handleClick);
extraNode.parentNode.removeChild(extraNode);
});
});
});
describe('when props change', () => {
it('removes old listeners', () => {
const handleClick = spy();
render(<EventListener target={document} onClick={handleClick} />, node);
render(<EventListener target={document} />, node);
document.body.click();
assert.strictEqual(handleClick.callCount, 0);
});
it('adds new listeners', () => {
const handleClick = spy();
render(<EventListener target={document} />, node);
document.body.click();
assert.strictEqual(handleClick.callCount, 0);
render(<EventListener target={document} onClick={handleClick} />, node);
document.body.click();
assert.strictEqual(handleClick.callCount, 1);
});
describe('lifecycle', () => {
let extraNode;
beforeEach(() => {
extraNode = document.createElement('button');
document.body.appendChild(extraNode);
});
afterEach(() => {
extraNode.parentNode.removeChild(extraNode);
});
it('removes listeners from old node', () => {
const handleClick = spy();
render(<EventListener target={document} onClick={handleClick} />, node);
render(<EventListener target={extraNode} onClick={handleClick} />, node);
document.body.click();
assert.strictEqual(handleClick.callCount, 0);
});
it('adds listeners to new node', () => {
const handleClick = spy();
render(<EventListener target={extraNode} onClick={handleClick} />, node);
render(<EventListener target={document} onClick={handleClick} />, node);
document.body.click();
assert.strictEqual(handleClick.callCount, 1);
});
});
it("doesn't update if props are shallow equal", () => {
const handleClick = () => {};
const inst = render(<EventListener target={document} onClick={handleClick} />, node);
const componentWillUpdate = inst.componentWillUpdate;
let updated = false;
inst.componentWillUpdate = (...args) => {
updated = true;
componentWillUpdate.bind(inst)(...args);
};
render(<EventListener target={document} onClick={handleClick} />, node);
assert.strictEqual(updated, false);
});
});
describe('when using capture phase', () => {
it('attaches listeners with capture', () => {
let button;
const calls = [];
render(
<div>
<EventListener target={document} onClickCapture={() => calls.push('outer')} />
<button
type="submit"
ref={c => {
button = c;
}}
onClick={() => calls.push('inner')}
/>
</div>,
node,
);
assert.strictEqual(calls.length, 0);
button.click();
assert.deepEqual(calls, ['outer', 'inner'], 'Should be called in the right order.');
});
});
describe('when using withOptions helper', () => {
it('should return handler function & event options of merging default values', () => {
const obj = withOptions(() => 'test', {});
assert.strictEqual(obj.handler(), 'test');
assert.deepEqual(obj.options, { capture: false, passive: false });
});
it('should work with using withOptions helper', () => {
const handleClick = spy();
render(<EventListener target={document} onClick={withOptions(handleClick, {})} />, node);
document.body.click();
assert.strictEqual(handleClick.callCount, 1);
});
it('attaches listeners with capture (withOptions)', () => {
let button = null;
const calls = [];
render(
<div>
<EventListener
target={document}
onClick={withOptions(() => calls.push('outer'), { capture: true })}
/>
<button
type="submit"
ref={c => {
button = c;
}}
onClick={() => calls.push('inner')}
/>
</div>,
node,
);
assert.strictEqual(calls.length, 0);
button.click();
assert.deepEqual(calls, ['outer', 'inner'], 'Should be called in the right order.');
});
});
});