diff --git a/mocha.opts b/mocha.opts new file mode 100644 index 0000000..6c66a0c --- /dev/null +++ b/mocha.opts @@ -0,0 +1,2 @@ +--recursive +--require test/setup.js diff --git a/package.json b/package.json index 1963005..37ec9cc 100644 --- a/package.json +++ b/package.json @@ -53,11 +53,12 @@ "babel-preset-react": "^6.11.1", "babel-preset-react-optimize": "^1.0.1", "babel-preset-stage-0": "^6.5.0", - "chai": "^3.5.0", - "chai-enzyme": "^0.5.0", - "cheerio": "^0.20.0", + "chai": "^4.1.2", + "chai-enzyme": "1.0.0-beta.0", + "cheerio": "^1.0.0-0", "css-loader": "^0.15.6", - "enzyme": "^2.4.1", + "enzyme": "^3.1.0", + "enzyme-adapter-react-15": "^1.0.1", "eslint": "^3.10.2", "eslint-config-airbnb": "^13.0.0", "eslint-import-resolver-webpack": "^0.7.0", @@ -71,9 +72,11 @@ "less-loader": "^2.2.0", "lodash": "^4.15.0", "mocha": "^2.2.5", - "react": "^0.14.8 || ^15", + "react": "^15.6.2", "react-addons-test-utils": "^0.14.8 || ^15", - "react-dom": "^0.14.8 || ^15", + "react-dom": "^15.5.0", + "react-dom-factories": "^1.0.2", + "react-test-renderer": "^15.5.0", "simulant": "^0.2.2", "sinon": "^1.17.5", "sinon-chai": "^2.8.0", diff --git a/test/keymap.js b/test/keymap.js index 3c1c285..36b1a8e 100644 --- a/test/keymap.js +++ b/test/keymap.js @@ -24,5 +24,9 @@ export default { 'OPEN': 'enter', 'CLOSE': 'esc', }, - 'NON-EXISTING': {}, -} + 'PARENT': { + 'OPEN': 'enter', + 'NEXT': 'tab', + }, + 'NO-SHORTCUTS': {}, +}; diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 0000000..a67022f --- /dev/null +++ b/test/setup.js @@ -0,0 +1,36 @@ +/** + * This file initialises the test environment with tools used for + * defining the test suite. + * + * It is run once before the test suite is executed and should only + * include setup code that is applicable to the entire suite. + * + * For configuration options of mocha itself, see the mocha.opts file. + */ + +// React testing framework for traversing React components' output +import Enzyme from 'enzyme'; +import Adaptor from 'enzyme-adapter-react-15'; + +// Assertion library for more expressive syntax +import chai from 'chai'; + +// chai plugin that allows React-specific assertions for enzyme +import chaiEnzyme from 'chai-enzyme'; + +// chai plugin that allows assertions on function calls +import sinonChai from 'sinon-chai'; + +// JS implementation of DOM and HTML spec +import jsdom from 'jsdom'; + +chai.use(chaiEnzyme()); +chai.use(sinonChai); + +Enzyme.configure({ adapter: new Adaptor() }); + +global.document = jsdom.jsdom(''); +global.window = document.defaultView; +global.Image = window.Image; +global.navigator = window.navigator; +global.CustomEvent = window.CustomEvent; diff --git a/test/shortcut-manager.spec.js b/test/shortcut-manager.spec.js index 67b4bb4..2a7cbc6 100644 --- a/test/shortcut-manager.spec.js +++ b/test/shortcut-manager.spec.js @@ -1,47 +1,42 @@ -import jsdom from 'jsdom' -import chai from 'chai' -import _ from 'lodash' -import sinonChai from 'sinon-chai' -import sinon from 'sinon' +import chai from 'chai'; +import _ from 'lodash'; +import sinon from 'sinon'; -import keymap from './keymap' +import simulant from 'simulant'; -chai.use(sinonChai) +import keymap from './keymap'; -const { expect } = chai +const { expect } = chai; + +import renderComponent from './support/renderComponent'; +import KeyCodes from './support/KeyCodes'; describe('Shortcut manager', () => { - let ShortcutManager = null + let ShortcutManager = null; before(() => { - global.document = jsdom.jsdom('') - global.window = document.defaultView - global.Image = window.Image - global.navigator = window.navigator - global.CustomEvent = window.CustomEvent - - ShortcutManager = require('../src').ShortcutManager - }) + ShortcutManager = require('../src').ShortcutManager; + }); it('should return empty object when calling empty constructor', () => { - const manager = new ShortcutManager() - expect(manager.getAllShortcuts()).to.be.empty - }) + const manager = new ShortcutManager(); + expect(manager.getAllShortcuts()).to.be.empty; + }); it('should return all shortcuts', () => { - const manager = new ShortcutManager(keymap) - expect(manager.getAllShortcuts()).to.not.be.empty - expect(manager.getAllShortcuts()).to.be.equal(keymap) + const manager = new ShortcutManager(keymap); + expect(manager.getAllShortcuts()).to.not.be.empty; + expect(manager.getAllShortcuts()).to.be.equal(keymap); - manager.setKeymap({}) - expect(manager.getAllShortcuts()).to.be.empty + manager.setKeymap({}); + expect(manager.getAllShortcuts()).to.be.empty; - manager.setKeymap(keymap) - expect(manager.getAllShortcuts()).to.be.equal(keymap) - }) + manager.setKeymap(keymap); + expect(manager.getAllShortcuts()).to.be.equal(keymap); + }); it('should return all shortcuts for the Windows platform', () => { - const manager = new ShortcutManager(keymap) + const manager = new ShortcutManager(keymap); const keyMapResult = { 'Test': { MOVE_LEFT: 'left', @@ -58,14 +53,18 @@ describe('Shortcut manager', () => { 'OPEN': 'enter', 'CLOSE': 'esc', }, - 'NON-EXISTING': {}, - } + 'PARENT': { + 'OPEN': 'enter', + 'NEXT': 'tab', + }, + 'NO-SHORTCUTS': {}, + }; - expect(manager.getAllShortcutsForPlatform('windows')).to.eql(keyMapResult) - }) + expect(manager.getAllShortcutsForPlatform('windows')).to.eql(keyMapResult); + }); it('should return all shortcuts for the macOs platform', () => { - const manager = new ShortcutManager(keymap) + const manager = new ShortcutManager(keymap); const keyMapResult = { 'Test': { MOVE_LEFT: 'left', @@ -82,89 +81,125 @@ describe('Shortcut manager', () => { 'OPEN': 'enter', 'CLOSE': 'esc', }, - 'NON-EXISTING': {}, - } + 'PARENT': { + 'OPEN': 'enter', + 'NEXT': 'tab', + }, + 'NO-SHORTCUTS': {}, + }; - expect(manager.getAllShortcutsForPlatform('osx')).to.eql(keyMapResult) - }) + expect(manager.getAllShortcutsForPlatform('osx')).to.eql(keyMapResult); + }); it('should expose the change event type as a static constant', () => expect(ShortcutManager.CHANGE_EVENT).to.exist - ) + ); it('should have static CHANGE_EVENT', () => expect(ShortcutManager.CHANGE_EVENT).to.be.equal('shortcuts:update') - ) + ); it('should call onUpdate', () => { - const manager = new ShortcutManager() - const spy = sinon.spy() - manager.addUpdateListener(spy) - manager.setKeymap({}) - expect(spy).to.have.beenCalled - }) + const manager = new ShortcutManager(); + const spy = sinon.spy(); + manager.addUpdateListener(spy); + manager.setKeymap({}); + expect(spy).to.have.been.called; + }); it('should throw an error when setKeymap is called without arg', () => { - const manager = new ShortcutManager(keymap) - const error = /setKeymap: keymap argument is not defined or falsy./ - expect(manager.setKeymap).to.throw(error) - }) + const manager = new ShortcutManager(keymap); + const error = /setKeymap: keymap argument is not defined or falsy./; + expect(manager.setKeymap).to.throw(error); + }); it('should extend the keymap', () => { - const manager = new ShortcutManager() - const newKeymap = { 'TESTING-NAMESPACE': {} } - const extendedKeymap = Object.assign({}, keymap, newKeymap) - manager.setKeymap(keymap) - manager.extendKeymap(newKeymap) + const manager = new ShortcutManager(); + const newKeymap = { 'TESTING-NAMESPACE': {} }; + const extendedKeymap = Object.assign({}, keymap, newKeymap); + manager.setKeymap(keymap); + manager.extendKeymap(newKeymap); - expect(manager.getAllShortcuts()).to.eql(extendedKeymap) - }) + expect(manager.getAllShortcuts()).to.eql(extendedKeymap); + }); it('should return array of shortcuts', () => { - const manager = new ShortcutManager(keymap) - let shortcuts = manager.getShortcuts('Test') - expect(shortcuts).to.be.an.array + const manager = new ShortcutManager(keymap); + let shortcuts = manager.getShortcuts('Test'); + expect(shortcuts).to.be.an('array'); - let shouldContainStrings = _.every(shortcuts, _.isString) - expect(shouldContainStrings).to.be.equal(true) - expect(shortcuts.length).to.be.equal(5) + let shouldContainStrings = _.every(shortcuts, _.isString); + expect(shouldContainStrings).to.be.equal(true); + expect(shortcuts.length).to.be.equal(5); - shortcuts = manager.getShortcuts('Next') - expect(shortcuts).to.be.an.array - shouldContainStrings = _.every(shortcuts, _.isString) - expect(shouldContainStrings).to.be.equal(true) - expect(shortcuts.length).to.be.equal(5) - }) + shortcuts = manager.getShortcuts('Next'); + expect(shortcuts).to.be.an('array'); + shouldContainStrings = _.every(shortcuts, _.isString); + expect(shouldContainStrings).to.be.equal(true); + expect(shortcuts.length).to.be.equal(5); + }); it('should not throw an error when getting not existing key from keymap', () => { - const manager = new ShortcutManager(keymap) - const notExist = () => manager.getShortcuts('NotExist') - expect(notExist).to.not.throw() - }) + const manager = new ShortcutManager(keymap); + const notExist = () => manager.getShortcuts('NotExist'); + expect(notExist).to.not.throw(); + }); it('should return correct key label', () => { - const manager = new ShortcutManager() - manager.setKeymap(keymap) + const manager = new ShortcutManager(); + manager.setKeymap(keymap); // Test - expect(manager.findShortcutName('alt+backspace', 'Test')).to.be.equal('DELETE') - expect(manager.findShortcutName('w', 'Test')).to.be.equal('MOVE_UP') - expect(manager.findShortcutName('up', 'Test')).to.be.equal('MOVE_UP') - expect(manager.findShortcutName('left', 'Test')).to.be.equal('MOVE_LEFT') - expect(manager.findShortcutName('right', 'Test')).to.be.equal('MOVE_RIGHT') + expect(manager.findShortcutName('alt+backspace', 'Test')).to.be.equal('DELETE'); + expect(manager.findShortcutName('w', 'Test')).to.be.equal('MOVE_UP'); + expect(manager.findShortcutName('up', 'Test')).to.be.equal('MOVE_UP'); + expect(manager.findShortcutName('left', 'Test')).to.be.equal('MOVE_LEFT'); + expect(manager.findShortcutName('right', 'Test')).to.be.equal('MOVE_RIGHT'); // Next - expect(manager.findShortcutName('alt+o', 'Next')).to.be.equal('OPEN') - expect(manager.findShortcutName('d', 'Next')).to.be.equal('ABORT') - expect(manager.findShortcutName('c', 'Next')).to.be.equal('ABORT') - expect(manager.findShortcutName('esc', 'Next')).to.be.equal('CLOSE') - expect(manager.findShortcutName('enter', 'Next')).to.be.equal('CLOSE') - }) + expect(manager.findShortcutName('alt+o', 'Next')).to.be.equal('OPEN'); + expect(manager.findShortcutName('d', 'Next')).to.be.equal('ABORT'); + expect(manager.findShortcutName('c', 'Next')).to.be.equal('ABORT'); + expect(manager.findShortcutName('esc', 'Next')).to.be.equal('CLOSE'); + expect(manager.findShortcutName('enter', 'Next')).to.be.equal('CLOSE'); + }); it('should throw an error', () => { - const manager = new ShortcutManager() - const fn = () => manager.findShortcutName('left') - expect(manager.findShortcutName).to.throw(/findShortcutName: keyName argument is not defined or falsy./) - expect(fn).to.throw(/findShortcutName: componentName argument is not defined or falsy./) - }) -}) + const manager = new ShortcutManager(); + const fn = () => manager.findShortcutName('left'); + expect(manager.findShortcutName).to.throw(/findShortcutName: keyName argument is not defined or falsy./); + expect(fn).to.throw(/findShortcutName: componentName argument is not defined or falsy./); + }); + + context('when the keymap is updated', () => { + beforeEach(function () { + const { wrapper, node, context } = renderComponent(); + node.focus(); + + this.node = node; + this.wrapper = wrapper; + this.shortcuts = context.shortcuts; + this.handler = wrapper.props().handler; + }); + + it('then uses the new keymap', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.SPACE }); + + expect(this.handler).to.not.have.been.called; + + this.shortcuts.setKeymap({ + ...keymap, + 'TESTING': { + 'SPACE': 'space', + }, + }); + + setTimeout(() => { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.SPACE }); + expect(this.handler).to.have.been.calledWith('SPACE'); + + this.shortcuts.setKeymap(keymap); + }, 100); + }); + }); +}); diff --git a/test/shortcuts.alwaysFireHandler.spec.js b/test/shortcuts.alwaysFireHandler.spec.js new file mode 100644 index 0000000..08da07e --- /dev/null +++ b/test/shortcuts.alwaysFireHandler.spec.js @@ -0,0 +1,134 @@ +import React from 'react'; + +import { expect } from 'chai'; +import simulant from 'simulant'; + +import keymap from './keymap'; +import KeyCodes from './support/KeyCodes'; +import ShortcutManager from '../src/shortcut-manager'; +import renderComponent from './support/renderComponent'; + +describe(' alwaysFireHandler prop:', () => { + beforeEach(function () { + const keymapWithPrintableChar = { + ...keymap, + 'TESTING': { + ...keymap.TESTING, + 'All': 'a', + }, + }; + + const shortcutsManager = new ShortcutManager(keymapWithPrintableChar); + this.context = { shortcuts: shortcutsManager }; + }); + + context('when no alwaysFireHandler value has been provided', () => { + beforeEach(function () { + const { wrapper, node } = renderComponent({ + mergeWithBaseProps: { + children: , + }, + context: this.context, + }); + + this.wrapper = wrapper; + this.node = node; + }); + + it('then has a default value of false', () => { + + }); + context('and an input element is focused', () => { + beforeEach(function () { + this.childNode = this.node.querySelector('.input'); + this.childNode.focus(); + }); + + context('and a non-printable key matching a shortcut is pressed', () => { + it('then calls the handler with the correct arguments', function () { + simulant.fire(this.childNode, 'keydown', { keyCode: KeyCodes.ENTER, key: 'ENTER' }); + + expect(this.wrapper.props().handler).to.have.been.calledWith('OPEN'); + }); + }); + + context('and a non-printable key NOT matching a shortcut is pressed', () => { + it('then does NOT call the handler', function () { + simulant.fire(this.childNode, 'keydown', { keyCode: KeyCodes.TAB, key: 'TAB' }); + + expect(this.wrapper.props().handler).to.not.have.been.called; + }); + }); + + context('and a printable key matching a shortcut is pressed', () => { + it('then does NOT calls the handler', function () { + simulant.fire(this.childNode, 'keydown', { keyCode: 65, key: 'a' }); + + expect(this.wrapper.props().handler).to.not.have.been.called; + }); + }); + + context('and a printable key NOT matching a shortcut is pressed', () => { + it('then does NOT calls the handler', function () { + simulant.fire(this.childNode, 'keydown', { keyCode: 66, key: 'b' }); + + expect(this.wrapper.props().handler).to.not.have.been.called; + }); + }); + }); + }); + + context('when a truthy alwaysFireHandler value has been provided', () => { + beforeEach(function () { + const { wrapper, node } = renderComponent({ + mergeWithBaseProps: { + children: , + alwaysFireHandler: true, + }, + context: this.context, + }); + + this.wrapper = wrapper; + this.node = node; + }); + + context('and an input element is focused', () => { + beforeEach(function () { + this.childNode = this.node.querySelector('.input'); + this.childNode.focus(); + }); + + context('and a non-printable key matching a shortcut is pressed', () => { + it('then calls the handler with the correct arguments', function () { + simulant.fire(this.childNode, 'keydown', { keyCode: KeyCodes.ENTER, key: 'ENTER' }); + + expect(this.wrapper.props().handler).to.have.been.calledWith('OPEN'); + }); + }); + + context('and a non-printable key NOT matching a shortcut is pressed', () => { + it('then does NOT call the handler', function () { + simulant.fire(this.childNode, 'keydown', { keyCode: KeyCodes.TAB, key: 'TAB' }); + + expect(this.wrapper.props().handler).to.not.have.been.called; + }); + }); + + context('and a printable key matching a shortcut is pressed', () => { + it('then does NOT calls the handler', function () { + simulant.fire(this.childNode, 'keydown', { keyCode: 65, key: 'a' }); + + expect(this.wrapper.props().handler).to.not.have.been.called; + }); + }); + + context('and a printable key NOT matching a shortcut is pressed', () => { + it('then does NOT calls the handler', function () { + simulant.fire(this.childNode, 'keydown', { keyCode: 66, key: 'b' }); + + expect(this.wrapper.props().handler).to.not.have.been.called; + }); + }); + }); + }); +}); diff --git a/test/shortcuts.global.spec.js b/test/shortcuts.global.spec.js new file mode 100644 index 0000000..4e8a2ab --- /dev/null +++ b/test/shortcuts.global.spec.js @@ -0,0 +1,177 @@ +import { expect } from 'chai'; +import simulant from 'simulant'; + +import KeyCodes from './support/KeyCodes'; +import renderComponent from './support/renderComponent'; + +describe(' global prop:', () => { + context('when no global value has been provided', () => { + beforeEach(function () { + const { wrapper, node } = renderComponent({ props: {} }); + node.focus(); + + this.node = node; + this.wrapper = wrapper; + this.handler = wrapper.props().handler; + }); + + it('then has a value of false', function () { + expect(this.wrapper.props().global).to.be.equal(false); + }); + }); + + context('when global is set to true', () => { + beforeEach(function () { + const { wrapper, node } = renderComponent({ mergeWithBaseProps: { global: true } }); + node.focus(); + + this.node = node; + this.wrapper = wrapper; + this.handler = wrapper.props().handler; + }); + + context('and component does NOT contain a as a child', () => { + context('and a matching key is pressed', () => { + it('then calls the handler once with the correct arguments', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ENTER }); + + expect(this.handler).to.have.been.calledOnce; + expect(this.handler).to.have.been.calledWith('OPEN'); + }); + }); + + context('and a key that does NOT match any shortcuts is pressed', () => { + it('then does NOT call the handler', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.TAB }); + + expect(this.handler).to.not.have.been.called; + }); + }); + + context('and the component has been unmounted', () => { + it('then does NOT call the handler when a matching key is pressed', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ENTER }); + expect(this.handler).to.have.been.calledOnce; + + this.wrapper.unmount(); + + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ENTER }); + expect(this.handler).to.have.been.calledOnce; + }); + }); + }); + + context('and the contains another (NON-global) as a child, with overlapping shortcuts', () => { + beforeEach(function () { + const { wrapper: childWrapper, component: childComponent } = renderComponent({ + mergeWithBaseProps: { + className: 'test', + }, + }); + + const { wrapper: parentWrapper, node: parentNode } = renderComponent({ mergeWithBaseProps: { + children: childComponent, + name: 'PARENT', + global: true, + } }); + + const node = parentNode.querySelector('.test'); + node.focus(); + + this.node = node; + this.childWrapper = childWrapper; + this.parentWrapper = parentWrapper; + }); + + context('and a key corresponding to a shortcut only in the child is pressed', () => { + it('then the parent Shortcuts\' handler is NOT called', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ESC }); + + expect(this.childWrapper.props().handler).to.have.been.calledWith('CLOSE'); + expect(this.parentWrapper.props().handler).to.not.have.been.called; + }); + }); + + context('and a key corresponding to a shortcut only in the parent is pressed', () => { + it('then the parent Shortcuts\' handler is called with the correct arguments', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.TAB }); + + expect(this.childWrapper.props().handler).to.not.have.been.called; + expect(this.parentWrapper.props().handler).to.have.been.calledWith('NEXT'); + }); + }); + + context('and a key corresponding to a shortcut both in the child and parent is pressed', () => { + beforeEach(function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ENTER }); + }); + + it('then the parent Shortcuts\' handler is called with the correct arguments', function () { + expect(this.childWrapper.props().handler).to.have.been.calledWith('OPEN'); + expect(this.parentWrapper.props().handler).to.have.been.calledWith('OPEN'); + }); + + it('then calls the parent handler before the child handler', function () { + expect(this.parentWrapper.props().handler).to.have.been.calledBefore(this.childWrapper.props().handler); + }); + }); + }); + + context('and the contains another global as a child, with overlapping shortcuts', () => { + beforeEach(function () { + const { wrapper: childWrapper, component: childComponent } = renderComponent({ + mergeWithBaseProps: { + className: 'test', + global: true, + }, + }); + + const { wrapper: parentWrapper, node: parentNode } = renderComponent({ mergeWithBaseProps: { + children: childComponent, + name: 'PARENT', + global: true, + } }); + + const node = parentNode.querySelector('.test'); + node.focus(); + + this.node = node; + this.childWrapper = childWrapper; + this.parentWrapper = parentWrapper; + }); + + context('and a key corresponding to a shortcut only in the child is pressed', () => { + it('then the parent Shortcuts\' handler is NOT called', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ESC }); + + expect(this.childWrapper.props().handler).to.have.been.calledWith('CLOSE'); + expect(this.parentWrapper.props().handler).to.not.have.been.called; + }); + }); + + context('and a key corresponding to a shortcut only in the parent is pressed', () => { + it('then the parent Shortcuts\' handler is called with the correct arguments', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.TAB }); + + expect(this.childWrapper.props().handler).to.not.have.been.called; + expect(this.parentWrapper.props().handler).to.have.been.calledWith('NEXT'); + }); + }); + + context('and a key corresponding to a shortcut both in the child and parent is pressed', () => { + beforeEach(function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ENTER }); + }); + + it('then the parent Shortcuts\' handler is called with the correct arguments', function () { + expect(this.childWrapper.props().handler).to.have.been.calledWith('OPEN'); + expect(this.parentWrapper.props().handler).to.have.been.calledWith('OPEN'); + }); + + it('then calls the parent handler before the child handler', function () { + expect(this.parentWrapper.props().handler).to.have.been.calledBefore(this.childWrapper.props().handler); + }); + }); + }); + }); +}); diff --git a/test/shortcuts.isolate.spec.js b/test/shortcuts.isolate.spec.js new file mode 100644 index 0000000..3e5b0a2 --- /dev/null +++ b/test/shortcuts.isolate.spec.js @@ -0,0 +1,102 @@ +import { expect } from 'chai'; +import simulant from 'simulant'; + +import KeyCodes from './support/KeyCodes'; +import renderComponent from './support/renderComponent'; + +describe(' isolate prop:', () => { + context('when no isolate value has been provided', () => { + beforeEach(function () { + const { wrapper, node } = renderComponent({ props: {} }); + node.focus(); + + this.node = node; + this.wrapper = wrapper; + this.handler = wrapper.props().handler; + }); + + it('then has a value of false', function () { + expect(this.wrapper.props().isolate).to.be.equal(false); + }); + }); + + context('when isolate is set to true', () => { + context('and the component is rendered under another as a child', () => { + beforeEach(function () { + const { wrapper: childWrapper, component: childComponent } = renderComponent({ + mergeWithBaseProps: { + className: 'test', + isolate: true, + }, + }); + + const { wrapper: parentWrapper, node: parentNode } = renderComponent({ mergeWithBaseProps: { + children: childComponent, + name: 'PARENT', + } }); + + const node = parentNode.querySelector('.test'); + node.focus(); + + this.node = node; + this.childWrapper = childWrapper; + this.parentWrapper = parentWrapper; + }); + + context('and a matching key is pressed', () => { + beforeEach(function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ENTER }); + }); + + it('then calls the child handler once with the correct arguments', function () { + expect(this.childWrapper.props().handler).to.have.been.calledOnce; + expect(this.childWrapper.props().handler).to.have.been.calledWith('OPEN'); + }); + + it('then DOES NOT call the parent handler', function () { + expect(this.parentWrapper.props().handler).to.not.have.been.called; + }); + }); + }); + + context('and the component is rendered under another (global) as a child', () => { + beforeEach(function () { + const { wrapper: childWrapper, component: childComponent } = renderComponent({ + mergeWithBaseProps: { + className: 'test', + isolate: true, + }, + }); + + const { wrapper: parentWrapper, node: parentNode } = renderComponent({ mergeWithBaseProps: { + children: childComponent, + name: 'PARENT', + global: true, + } }); + + const node = parentNode.querySelector('.test'); + node.focus(); + + this.node = node; + this.childWrapper = childWrapper; + this.parentWrapper = parentWrapper; + }); + + context('and a matching key is pressed', () => { + beforeEach(function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ENTER }); + }); + + it('then calls the child handler once with the correct arguments', function () { + expect(this.childWrapper.props().handler).to.have.been.calledOnce; + expect(this.childWrapper.props().handler).to.have.been.calledWith('OPEN'); + }); + + it('then calls the parent handler once with the correct arguments', function () { + expect(this.parentWrapper.props().handler).to.have.been.calledOnce; + expect(this.parentWrapper.props().handler).to.have.been.calledWith('OPEN'); + }); + }); + }); + }); +}); diff --git a/test/shortcuts.props.spec.js b/test/shortcuts.props.spec.js new file mode 100644 index 0000000..08b33fb --- /dev/null +++ b/test/shortcuts.props.spec.js @@ -0,0 +1,105 @@ +import React from 'react'; + +import { expect } from 'chai'; + +import renderComponent from './support/renderComponent'; + +describe(' props:', () => { + describe('when no overriding values are provided', () => { + beforeEach(function () { + const { wrapper } = renderComponent({ props: {} }); + this.defaultProps = wrapper.props(); + }); + + it('then tabIndex has a value of -1', function () { + expect(this.defaultProps.tabIndex).to.be.equal(-1); + }); + + it('then className has a value of null', function () { + expect(this.defaultProps.className).to.be.equal(null); + }); + + it('then isolate has a value of false', function () { + expect(this.defaultProps.isolate).to.be.equal(false); + }); + + it('then there are no children', function () { + expect(this.defaultProps.children).to.be.equal(undefined); + }); + + it('then eventType has a value of null', function () { + expect(this.defaultProps.eventType).to.be.equal(null); + }); + + it('then preventDefault has a value of false', function () { + expect(this.defaultProps.preventDefault).to.be.equal(false); + }); + + it('has no handler function', function () { + expect(this.defaultProps.handler).to.be.equal(undefined); + }); + }); + + context('when overriding the default prop values', () => { + it('then renders positive tabIndex values correctly', () => { + const { wrapper } = renderComponent({ props: { tabIndex: 42 } }); + + expect(wrapper).to.have.attr('tabindex').equal('42'); + }); + + it('then renders zero tabIndex values correctly', () => { + const { wrapper } = renderComponent({ props: { tabIndex: 0 } }); + + expect(wrapper).to.have.attr('tabindex').equal('0'); + }); + + it('then renders className values correctly', () => { + const mergeWithBaseProps = { className: 'testing' }; + const { wrapper } = renderComponent({ props: mergeWithBaseProps }); + + expect(wrapper).to.have.className(mergeWithBaseProps.className); + }); + + it('then renders children correctly', () => { + const mergeWithBaseProps = { children:
}; + const { wrapper } = renderComponent({ props: mergeWithBaseProps }); + + expect(wrapper).to.contain(mergeWithBaseProps.children); + }); + + it('then correctly sets the isolate value', () => { + const mergeWithBaseProps = { isolate: true }; + const { wrapper } = renderComponent({ props: mergeWithBaseProps }); + + expect(wrapper.props().isolate).to.be.equal(true); + }); + + it('then correctly sets the handler value', () => { + const mergeWithBaseProps = { handler: () => {} }; + const { wrapper } = renderComponent({ props: mergeWithBaseProps }); + + expect(wrapper.props().handler).to.be.equal(mergeWithBaseProps.handler); + }); + + it('then correctly sets the name value', () => { + const mergeWithBaseProps = { name: 'TESTING' }; + const { wrapper } = renderComponent({ props: mergeWithBaseProps }); + + expect(wrapper.props().name).to.be.equal(mergeWithBaseProps.name); + }); + + it('then correctly sets the eventType value', () => { + const mergeWithBaseProps = { eventType: 'keyUp' }; + const { wrapper } = renderComponent({ props: mergeWithBaseProps }); + + expect(wrapper.props().eventType).to.be.equal(mergeWithBaseProps.eventType); + }); + + it('then correctly sets preventDefault to true', () => { + const mergeWithBaseProps = { preventDefault: true }; + const { wrapper } = renderComponent({ props: mergeWithBaseProps }); + + expect(wrapper.props().preventDefault).to.be.equal(true); + }); + }); +}); diff --git a/test/shortcuts.spec.js b/test/shortcuts.spec.js index a4bcd49..30ae56c 100644 --- a/test/shortcuts.spec.js +++ b/test/shortcuts.spec.js @@ -1,510 +1,111 @@ -import jsdom from 'jsdom' -import chai from 'chai' -import sinonChai from 'sinon-chai' -import sinon from 'sinon' -import _ from 'lodash' +import React from 'react'; -import keymap from './keymap' +import { expect } from 'chai'; +import simulant from 'simulant'; -describe('Shortcuts component', () => { - let baseProps = null - let baseContext = null - - let simulant = null - let ShortcutManager = null - let Shortcuts = null - let ReactDOM = null - let React = null - let enzyme = null - - chai.use(sinonChai) - const { expect } = chai - - beforeEach(() => { - global.document = jsdom.jsdom('') - global.window = document.defaultView - global.Image = window.Image - global.navigator = window.navigator - global.CustomEvent = window.CustomEvent - simulant = require('simulant') - ReactDOM = require('react-dom') - React = require('react') - enzyme = require('enzyme') - const chaiEnzyme = require('chai-enzyme') - - chai.use(chaiEnzyme()) - - ShortcutManager = require('../src').ShortcutManager - const shortcutsManager = new ShortcutManager(keymap) - - Shortcuts = require('../src/').Shortcuts - - baseProps = { - handler: sinon.spy(), - name: 'TESTING', - className: null, - } - baseContext = { shortcuts: shortcutsManager } - }) - - it('should render component', () => { - const shortcutComponent = React.createElement(Shortcuts, baseProps) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.find('div')).to.have.length(1) - }) - - it('should have a tabIndex of -1 by default', () => { - let shortcutComponent = React.createElement(Shortcuts, baseProps) - let wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().tabIndex).to.be.equal(-1) - - let props = _.assign({}, baseProps, { tabIndex: 42 }) - shortcutComponent = React.createElement(Shortcuts, props) - wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().tabIndex).to.be.equal(props.tabIndex) - let realTabIndex = ReactDOM.findDOMNode(wrapper.instance()).getAttribute('tabindex') - expect(realTabIndex).to.have.equal(String(props.tabIndex)) - - props.tabIndex = 0 - shortcutComponent = React.createElement(Shortcuts, props) - wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().tabIndex).to.be.equal(props.tabIndex) - realTabIndex = ReactDOM.findDOMNode(wrapper.instance()).getAttribute('tabindex') - expect(realTabIndex).to.have.equal(String(props.tabIndex)) - }) - - it('should not have className by default', () => { - const shortcutComponent = React.createElement(Shortcuts, baseProps) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().className).to.be.equal(null) - }) - - it('should have className', () => { - const props = _.assign({}, baseProps, { className: 'testing' }) - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().className).to.be.equal('testing') - expect(wrapper).to.have.className('testing') - }) - - it('should have isolate prop set to false by default', () => { - const shortcutComponent = React.createElement(Shortcuts, baseProps) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().isolate).to.be.equal(false) - }) - - it('should have isolate prop', () => { - const props = _.assign({}, baseProps, { isolate: true }) - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().isolate).to.be.equal(true) - }) - - it('should not have children by default', () => { - const shortcutComponent = React.createElement(Shortcuts, baseProps) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().children).to.be.equal(undefined) - }) - - it('should have children', () => { - const props = _.assign({}, baseProps, { children: React.DOM.div() }) - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper).to.contain(React.DOM.div()) - }) - - it('should have handler prop', () => { - const shortcutComponent = React.createElement(Shortcuts, baseProps) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().handler).to.be.function - }) - - it('should have name prop', () => { - const props = _.assign({}, baseProps, - {name: 'TESTING'}) - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().name).to.be.equal('TESTING') - }) - - it('should not have eventType prop by default', () => { - const shortcutComponent = React.createElement(Shortcuts, baseProps) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().eventType).to.be.equal(null) - }) - - it('should have eventType prop', () => { - const props = _.assign({}, baseProps, { eventType: 'keyUp' }) - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().eventType).to.be.equal('keyUp') - }) - - it('should have stopPropagation prop by default', () => { - const shortcutComponent = React.createElement(Shortcuts, baseProps) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().stopPropagation).to.be.equal(true) - }) - - it('should have stopPropagation prop set to false', () => { - const props = _.assign({}, baseProps, { stopPropagation: false }) - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().stopPropagation).to.be.equal(false) - }) - - it('should have preventDefault prop set to false by default', () => { - const shortcutComponent = React.createElement(Shortcuts, baseProps) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().preventDefault).to.be.equal(false) - }) - - it('should have preventDefault prop set to true', () => { - const props = _.assign({}, baseProps, { preventDefault: true }) - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().preventDefault).to.be.equal(true) - }) - - it('should not have targetNodeSelector prop by default', () => { - const shortcutComponent = React.createElement(Shortcuts, baseProps) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().targetNodeSelector).to.be.equal(null) - }) - - it('should have targetNode prop', () => { - const props = _.assign({}, baseProps, { targetNodeSelector: 'body' }) - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().targetNodeSelector).to.be.equal('body') - }) - - it('should have global prop set to false by default', () => { - const shortcutComponent = React.createElement(Shortcuts, baseProps) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().global).to.be.equal(false) - }) - - it('should have global prop set to true', () => { - const props = _.assign({}, baseProps, { global: true }) - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - expect(wrapper.props().global).to.be.equal(true) - }) - - it('should fire the handler prop with the correct argument', () => { - const shortcutComponent = React.createElement(Shortcuts, baseProps) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - const node = ReactDOM.findDOMNode(wrapper.instance()) - node.focus() - - const enter = 13 - simulant.fire(node, 'keydown', { keyCode: enter }) - - expect(wrapper.props().handler).to.have.been.calledWith('OPEN') - - const esc = 27 - simulant.fire(node, 'keydown', { keyCode: esc }) - - expect(wrapper.props().handler).to.have.been.calledWith('CLOSE') - }) - - it('should not fire the handler', () => { - const props = _.assign({}, baseProps, { name: 'NON-EXISTING' }) - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - const node = ReactDOM.findDOMNode(wrapper.instance()) - node.focus() - - const enter = 13 - simulant.fire(node, 'keydown', { keyCode: enter }) - - expect(wrapper.props().handler).to.not.have.been.called - }) - - it('should not fire twice when global prop is truthy', () => { - const props = _.assign({}, baseProps, { global: true }) - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - const node = ReactDOM.findDOMNode(wrapper.instance()) - node.focus() - - const enter = 13 - simulant.fire(node, 'keydown', { keyCode: enter }) - - expect(wrapper.props().handler).to.have.been.calledOnce - }) - - it('should not fire when the component has been unmounted', () => { - const shortcutComponent = React.createElement(Shortcuts, baseProps) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) +import KeyCodes from './support/KeyCodes'; +import renderComponent from './support/renderComponent'; - const node = ReactDOM.findDOMNode(wrapper.instance()) - node.focus() - - wrapper.unmount() - - const enter = 13 - simulant.fire(node, 'keydown', { keyCode: enter }) - - expect(wrapper.props().handler).to.not.have.been.called - }) - - it('should update the shortcuts and fire the handler', () => { - const shortcutComponent = React.createElement(Shortcuts, baseProps) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - const node = ReactDOM.findDOMNode(wrapper.instance()) - node.focus() - - const space = 32 - simulant.fire(node, 'keydown', { keyCode: space }) - - expect(wrapper.props().handler).to.not.have.been.called - - const editedKeymap = _.assign({}, keymap, { - 'TESTING': { - 'SPACE': 'space', - }, - } - ) - baseContext.shortcuts.setKeymap(editedKeymap) - - simulant.fire(node, 'keydown', { keyCode: space }) - - expect(baseProps.handler).to.have.been.called - - // NOTE: rollback the previous keymap - baseContext.shortcuts.setKeymap(keymap) - }) - - it('should fire the handler from a child input', () => { - const props = _.assign({}, baseProps, { - children: React.DOM.input({ type: 'text', className: 'input' }), - }) - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - const parentNode = ReactDOM.findDOMNode(wrapper.instance()) - const node = parentNode.querySelector('.input') - node.focus() - - const enter = 13 - simulant.fire(node, 'keydown', { keyCode: enter, key: 'Enter' }) - - expect(wrapper.props().handler).to.have.been.called - }) - - it('should fire the handler when using targetNodeSelector', () => { - const props = _.assign({}, baseProps, { targetNodeSelector: 'body' }) - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - const enter = 13 - simulant.fire(document.body, 'keydown', { keyCode: enter, key: 'Enter' }) - - expect(wrapper.props().handler).to.have.been.called - }) - - it('should throw and error if targetNodeSelector is not found', () => { - const props = _.assign({}, baseProps, { targetNodeSelector: 'non-existing' }) - const shortcutComponent = React.createElement(Shortcuts, props) - - try { - enzyme.mount(shortcutComponent, { context: baseContext }) - } catch (err) { - expect(err).to.match(/Node selector 'non-existing' was not found/) - } - }) - - it('should fire the handler from focused input', () => { - const props = _.assign({}, baseProps, { - alwaysFireHandler: true, - children: React.DOM.input({type: 'text', className: 'input'}) - }) - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - const parentNode = ReactDOM.findDOMNode(wrapper.instance()) - const node = parentNode.querySelector('.input') - node.focus() - - const enter = 13 - simulant.fire(node, 'keydown', { keyCode: enter }) - - expect(wrapper.props().handler).to.have.been.called - }) - - - describe('Shortcuts component inside Shortcuts component:', () => { - - it('should not fire parent handler when child handler is fired', () => { - const props = _.assign({}, baseProps, { - children: React.createElement(Shortcuts, _.assign({}, baseProps, { className: 'test' })), - }) - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - const parentNode = ReactDOM.findDOMNode(wrapper.instance()) - const node = parentNode.querySelector('.test') - - node.focus() - - const enter = 13 - simulant.fire(node, 'keydown', { keyCode: enter }) - - expect(baseProps.handler).to.have.been.calledOnce - }) - - it('should fire parent handler when child handler is fired', () => { - const props = _.assign({}, baseProps, { - children: React.createElement(Shortcuts, _.assign({}, baseProps, { className: 'test', stopPropagation: false })), - }) - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - const parentNode = ReactDOM.findDOMNode(wrapper.instance()) - const node = parentNode.querySelector('.test') - - node.focus() - - const enter = 13 - simulant.fire(node, 'keydown', { keyCode: enter }) - - expect(baseProps.handler).to.have.been.calledTwice - }) - - it('should fire parent handler when parent handler has global prop', () => { - const props = _.assign({}, baseProps, { - children: React.createElement(Shortcuts, _.assign({}, baseProps, { className: 'test' })), - global: true, - }) - - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - const parentNode = ReactDOM.findDOMNode(wrapper.instance()) - const node = parentNode.querySelector('.test') - - node.focus() - - const enter = 13 - simulant.fire(node, 'keydown', { keyCode: enter }) - - expect(baseProps.handler).to.have.been.calledTwice - }) - - it('should fire parent handler but not the child handler', () => { - const props = _.assign({}, baseProps, { - children: React.createElement(Shortcuts, _.assign({}, baseProps, { name: 'NON-EXISTING', className: 'test' })), - global: true, - }) - - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - const parentNode = ReactDOM.findDOMNode(wrapper.instance()) - const node = parentNode.querySelector('.test') - - node.focus() - - const enter = 13 - simulant.fire(node, 'keydown', { keyCode: enter }) - - expect(baseProps.handler).to.have.been.calledOnce - }) - - it('should fire for all global components', () => { - const props = _.assign({}, baseProps, { - children: React.createElement(Shortcuts, _.assign({}, baseProps, { - global: true, - children: React.createElement(Shortcuts, _.assign({}, baseProps, { name: 'NON-EXISTING', className: 'test' })), - })), - global: true, - }) - - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - const parentNode = ReactDOM.findDOMNode(wrapper.instance()) - const node = parentNode.querySelector('.test') - - node.focus() - - const enter = 13 - simulant.fire(node, 'keydown', { keyCode: enter }) - - expect(baseProps.handler).to.have.been.calledTwice - }) - - it('should not fire parent handler when a child has isolate prop set to true', () => { - const childHandlerSpy = sinon.spy() - const props = _.assign({}, baseProps, { - children: React.createElement(Shortcuts, _.assign({}, baseProps, { - className: 'test', - isolate: true, - handler: childHandlerSpy, - })), - }) - - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - const parentNode = ReactDOM.findDOMNode(wrapper.instance()) - const node = parentNode.querySelector('.test') - - node.focus() - - const enter = 13 - simulant.fire(node, 'keydown', { keyCode: enter }) - - expect(childHandlerSpy).to.have.been.called - expect(baseProps.handler).to.not.have.been.called - }) - - it('should fire parent handler when is global and a child has isolate prop set to true', () => { - const props = _.assign({}, baseProps, { - global: true, - children: React.createElement(Shortcuts, _.assign({}, baseProps, { className: 'test', isolate: true })), - }) - - const shortcutComponent = React.createElement(Shortcuts, props) - const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) - - const parentNode = ReactDOM.findDOMNode(wrapper.instance()) - const node = parentNode.querySelector('.test') - - node.focus() - - const enter = 13 - simulant.fire(node, 'keydown', { keyCode: enter }) - - expect(baseProps.handler).to.have.been.called - }) - }) -}) +describe('Shortcuts component', () => { + describe('Calling the handler function:', () => { + context('when an element in a namespace with defined shortcuts is focused', () => { + beforeEach(function () { + const { wrapper, node } = renderComponent(); + node.focus(); + + this.node = node; + this.wrapper = wrapper; + this.handler = wrapper.props().handler; + }); + + context('and a matching key is pressed', () => { + it('then calls the handler with the correct action', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ENTER }); + expect(this.handler).to.have.been.calledWith('OPEN'); + + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ESC }); + expect(this.handler).to.have.been.calledWith('CLOSE'); + }); + }); + + context('and a key that doesn\'t match any shortcuts is pressed', () => { + it('then does NOT call the handler', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.TAB }); + + expect(this.handler).to.not.have.been.called; + }); + }); + + context('and the component has been unmounted', () => { + it('then does not call the handler when a matching key is pressed', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ENTER }); + + expect(this.handler).to.have.been.calledOnce; + + this.wrapper.unmount(); + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ENTER }); + + expect(this.handler).to.have.been.calledOnce; + }); + }); + }); + + context('when an element in a namespace WITHOUT defined shortcuts is focused and a key is pressed', () => { + beforeEach(function () { + const { wrapper, node } = renderComponent({ mergeWithBaseProps: { name: 'NO-SHORTCUTS' } }); + node.focus(); + + this.node = node; + this.wrapper = wrapper; + this.handler = wrapper.props().handler; + }); + + it('then does NOT call the handler', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ENTER }); + + expect(this.handler).to.not.have.been.called; + }); + }); + + context('when an element in a namespace NOT defined in the keymap is focused', () => { + beforeEach(function () { + const { wrapper, node } = renderComponent({ mergeWithBaseProps: { name: 'NO-SHORTCUTS' } }); + node.focus(); + + this.node = node; + this.wrapper = wrapper; + this.handler = wrapper.props().handler; + }); + + it('then does NOT call the handler', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ENTER }); + + expect(this.handler).to.not.have.been.called; + }); + }); + }); + + context('when a child element is focused and a key matching a shortcut is pressed', () => { + beforeEach(function () { + const { wrapper, node, context } = renderComponent({ + mergeWithBaseProps: { children: }, + }); + + node.focus(); + + const childNode = node.querySelector('.input'); + childNode.focus(); + + this.childNode = childNode; + this.wrapper = wrapper; + this.shortcuts = context.shortcuts; + this.handler = wrapper.props().handler; + }); + + it('then calls the handler', function () { + simulant.fire(this.childNode, 'keydown', { keyCode: KeyCodes.ENTER, key: 'Enter' }); + + expect(this.handler).to.have.been.calledWith('OPEN'); + }); + }); +}); diff --git a/test/shortcuts.stopPropagation.spec.js b/test/shortcuts.stopPropagation.spec.js new file mode 100644 index 0000000..0fa2c8a --- /dev/null +++ b/test/shortcuts.stopPropagation.spec.js @@ -0,0 +1,122 @@ +import React from 'react'; + +import { expect } from 'chai'; +import simulant from 'simulant'; + +import KeyCodes from './support/KeyCodes'; +import renderComponent from './support/renderComponent'; + +describe(' stopPropagation prop:', () => { + context('when no stopPropagation value has been provided', () => { + before(function () { + const { wrapper } = renderComponent({ props: {} }); + + this.wrapper = wrapper; + }); + + it('then has a value of null', function () { + expect(this.wrapper.props().stopPropagation).to.be.equal(true); + }); + + context('and the is rendered inside another with overlapping shortcuts', () => { + beforeEach(function () { + const { wrapper: childWrapper, component: childComponent } = renderComponent({ + mergeWithBaseProps: { + className: 'test', + }, + }); + + const { wrapper: parentWrapper, node: parentNode } = renderComponent({ mergeWithBaseProps: { + children: childComponent, + name: 'PARENT', + } }); + + const node = parentNode.querySelector('.test'); + node.focus(); + + this.node = node; + this.childWrapper = childWrapper; + this.parentWrapper = parentWrapper; + }); + + context('and a key corresponding to a shortcut only in the child is pressed', () => { + it('then the parent Shortcuts\' handler is NOT called', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ESC }); + + expect(this.childWrapper.props().handler).to.have.been.calledWith('CLOSE'); + expect(this.parentWrapper.props().handler).to.not.have.been.called; + }); + }); + + context('and a key corresponding to a shortcut only in the parent is pressed', () => { + it('then the parent Shortcuts\' handler is NOT called', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.TAB }); + + expect(this.childWrapper.props().handler).to.not.have.been.called; + expect(this.parentWrapper.props().handler).to.not.have.been.called; + }); + }); + + context('and a key corresponding to a shortcut both in the child and parent is pressed', () => { + it('then the parent Shortcuts\' handler is NOT called', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ENTER }); + + expect(this.childWrapper.props().handler).to.have.been.calledWith('OPEN'); + expect(this.parentWrapper.props().handler).to.not.have.been.called; + }); + }); + }); + }); + + context('when stopPropagation is set to false', () => { + context('and the is rendered inside another with overlapping shortcuts', () => { + beforeEach(function () { + const { wrapper: childWrapper, component: childComponent } = renderComponent({ + mergeWithBaseProps: { + stopPropagation: false, + className: 'test', + }, + }); + + const { wrapper: parentWrapper, node: parentNode } = renderComponent({ mergeWithBaseProps: { + children: childComponent, + name: 'PARENT', + } }); + + const node = parentNode.querySelector('.test'); + node.focus(); + + this.node = node; + this.childWrapper = childWrapper; + this.parentWrapper = parentWrapper; + }); + + context('and a key corresponding to a shortcut only in the child is pressed', () => { + it('then the parent Shortcuts\' handler is NOT called', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ESC }); + + expect(this.childWrapper.props().handler).to.have.been.calledWith('CLOSE'); + expect(this.parentWrapper.props().handler).to.not.have.been.called; + }); + }); + + context('and a key corresponding to a shortcut only in the parent is pressed', () => { + it('then the parent Shortcuts\' handler is called with the correct arguments', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.TAB }); + + expect(this.childWrapper.props().handler).to.not.have.been.called; + expect(this.parentWrapper.props().handler).to.have.been.calledWith('NEXT'); + }); + }); + + context('and a key corresponding to a shortcut both in the child and parent is pressed', () => { + it('then the parent Shortcuts\' handler is called with the correct arguments', function () { + simulant.fire(this.node, 'keydown', { keyCode: KeyCodes.ENTER }); + + expect(this.childWrapper.props().handler).to.have.been.calledOnce; + expect(this.parentWrapper.props().handler).to.have.been.calledOnce; + }); + }); + }); + }); +}); diff --git a/test/shortcuts.targetNodeSelector.spec.js b/test/shortcuts.targetNodeSelector.spec.js new file mode 100644 index 0000000..0b1e33d --- /dev/null +++ b/test/shortcuts.targetNodeSelector.spec.js @@ -0,0 +1,52 @@ +import React from 'react'; + +import enzyme from 'enzyme'; +import { expect } from 'chai'; +import simulant from 'simulant'; + +import KeyCodes from './support/KeyCodes'; +import renderComponent from './support/renderComponent'; + +import Shortcuts from '../src/component/shortcuts'; +import ShortcutManager from '../src/shortcut-manager'; + +describe(' targetNodeSelector prop:', () => { + context('when no alwaysFireHandler value has been provided', () => { + before(function () { + const { wrapper } = renderComponent({ props: {} }); + + this.wrapper = wrapper; + }); + + it('then has a value of null', function () { + expect(this.wrapper.props().targetNodeSelector).to.be.equal(null); + }); + }); + + context('when provided a targetNodeSelector', () => { + context('that matches an element in the DOM', () => { + it('then calls the handler when a matching key is pressed', () => { + const { wrapper } = renderComponent({ mergeWithBaseProps: { targetNodeSelector: 'body' } }); + + simulant.fire(document.body, 'keydown', { keyCode: KeyCodes.ENTER, key: 'Enter' }); + + expect(wrapper.props().handler).to.have.been.calledWith('OPEN'); + }); + }); + + context('that does NOT match an element in the DOM', () => { + it('then calls the handler when a matching key is pressed', () => { + const component = ( + + ); + + const shortcutsManager = new ShortcutManager({}); + const context = { shortcuts: shortcutsManager }; + + expect(() => enzyme.mount(component, { context })).to.throw( + 'Node selector \'non-existent\' was not found.' + ); + }); + }); + }); +}); diff --git a/test/support/KeyCodes.js b/test/support/KeyCodes.js new file mode 100644 index 0000000..02026a1 --- /dev/null +++ b/test/support/KeyCodes.js @@ -0,0 +1,6 @@ +export default { + ENTER: 13, + ESC: 27, + TAB: 9, + SPACE: 32, +}; diff --git a/test/support/renderComponent.js b/test/support/renderComponent.js new file mode 100644 index 0000000..e1fa9b6 --- /dev/null +++ b/test/support/renderComponent.js @@ -0,0 +1,51 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import enzyme from 'enzyme'; + +import sinon from 'sinon'; + +import keymap from '../keymap'; +import Shortcuts from '../../src/component/shortcuts'; +import ShortcutManager from '../../src/shortcut-manager'; + +function renderComponent({ props, mergeWithBaseProps, context } = {}) { + const effectiveProps = (() => { + if (props) { + return props; + } + + const baseProps = { + handler: sinon.spy(), + name: 'TESTING', + className: null, + }; + + if (mergeWithBaseProps) { + return { ...baseProps, ...mergeWithBaseProps }; + } + + return baseProps; + })(); + + const component = ( + + ); + + const effectiveContext = (() => { + if (context) { + return context; + } + + const shortcutsManager = new ShortcutManager(keymap); + return { shortcuts: shortcutsManager }; + })(); + + const wrapper = enzyme.mount(component, { context: effectiveContext }); + const node = ReactDOM.findDOMNode(wrapper.instance()); + + return { + wrapper, component, node, context: effectiveContext, + }; +} + +export default renderComponent; diff --git a/test/utils.js b/test/utils.js index 160873e..9400c5d 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,13 +1,13 @@ -import chai from 'chai' -import _ from 'lodash' -import { isArray, isPlainObject, findKey, compact, flatten, map } from '../src/utils' +import chai from 'chai'; +import _ from 'lodash'; +import { isArray, isPlainObject, findKey, compact, flatten, map } from '../src/utils'; describe('utils', () => { - const { expect } = chai - let primitives + const { expect } = chai; + let primitives; beforeEach(() => { - function fn() { this.a = 1 } + function fn() { this.a = 1; } primitives = [ ['array'], @@ -17,40 +17,40 @@ describe('utils', () => { null, undefined, NaN, - new Map([[ 1, 'one' ], [ 2, 'two' ]]), + new Map([[1, 'one'], [2, 'two']]), new fn(), true, 42, - ] - }) + ]; + }); describe('isArray', () => { it('should be true for arrays', () => { primitives.forEach((val, idx) => { if (idx === 0) { - expect(isArray(val)).to.be.true - expect(_.isArray(val)).to.be.true + expect(isArray(val)).to.be.true; + expect(_.isArray(val)).to.be.true; } else { - expect(isArray(val)).to.be.false - expect(_.isArray(val)).to.be.false + expect(isArray(val)).to.be.false; + expect(_.isArray(val)).to.be.false; } - }) - }) - }) + }); + }); + }); describe('isPlainObject', () => { it('should be true for plain objects', () => { primitives.forEach((val, idx) => { if (idx === 1 || idx === 2) { - expect(isPlainObject(val)).to.be.true - expect(_.isPlainObject(val)).to.be.true + expect(isPlainObject(val)).to.be.true; + expect(_.isPlainObject(val)).to.be.true; } else { - expect(isPlainObject(val)).to.be.false - expect(_.isPlainObject(val)).to.be.false + expect(isPlainObject(val)).to.be.false; + expect(_.isPlainObject(val)).to.be.false; } - }) - }) - }) + }); + }); + }); describe('findKey', () => { it('should return the matching key', () => { @@ -59,15 +59,15 @@ describe('utils', () => { obj: { val: 4, }, - } + }; - const checkOne = val => val === 1 - const checkTwo = val => typeof val === 'object' + const checkOne = val => val === 1; + const checkTwo = val => typeof val === 'object'; - expect(findKey(obj, checkOne)).to.deep.equal(_.findKey(obj, checkOne)) - expect(findKey(obj, checkTwo)).to.deep.equal(_.findKey(obj, checkTwo)) - }) - }) + expect(findKey(obj, checkOne)).to.deep.equal(_.findKey(obj, checkOne)); + expect(findKey(obj, checkTwo)).to.deep.equal(_.findKey(obj, checkTwo)); + }); + }); describe('compact', () => { it('removes falsy values', () => { @@ -81,52 +81,52 @@ describe('utils', () => { NaN, '', 'false, null, 0, "", undefined, and NaN are falsy', - ] + ]; - expect(compact(values)).to.deep.equal(_.compact(values)) - }) - }) + expect(compact(values)).to.deep.equal(_.compact(values)); + }); + }); describe('flatten', () => { it('flattens an array 1 level', () => { - const value = [1, [2, [3, [4]], 5, [[[6], 7], 8], 9]] - expect(flatten(value)).to.deep.equal(_.flatten(value)) - }) - }) + const value = [1, [2, [3, [4]], 5, [[[6], 7], 8], 9]]; + expect(flatten(value)).to.deep.equal(_.flatten(value)); + }); + }); describe('map', () => { it('should map an array', () => { - const values = [1, 2, 3, 4] - const mapFn = val => val * 10 + const values = [1, 2, 3, 4]; + const mapFn = val => val * 10; - expect(map(values, mapFn)).to.deep.equal(_.map(values, mapFn)) - expect(map(values, mapFn)).to.deep.equal([10, 20, 30, 40]) + expect(map(values, mapFn)).to.deep.equal(_.map(values, mapFn)); + expect(map(values, mapFn)).to.deep.equal([10, 20, 30, 40]); // ensure that values array is not mutated - expect(values).to.deep.equal([1, 2, 3, 4]) - }) + expect(values).to.deep.equal([1, 2, 3, 4]); + }); it('should map an object', () => { const obj = { one: 1, two: 2, three: 3, - } - const mapFn = (val, key) => `${key} - ${val * 10}` + }; + const mapFn = (val, key) => `${key} - ${val * 10}`; - expect(map(obj, mapFn)).to.deep.equal(_.map(obj, mapFn)) + expect(map(obj, mapFn)).to.deep.equal(_.map(obj, mapFn)); expect(map(obj, mapFn)).to.deep.equal([ 'one - 10', 'two - 20', 'three - 30', - ]) + ]); // ensure the object was not mutated expect(obj).to.deep.equal({ one: 1, two: 2, three: 3, - }) - }) - }) -}) + }); + }); + }); +});