Skip to content

Commit

Permalink
Add Test For Higher Order Components Focusable (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbinto authored and SpiritBreaker226 committed Apr 28, 2016
1 parent f53814f commit 5ff17e8
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 10 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"test": "npm run test:cover",
"test:watch": "npm run test:nocover -- --watch --watch-extensions js",
"test:nocover": "mocha --compilers js:babel-core/register --require ./test/test_helper.js './test/**/*.js'",
"test:debug": "mocha --compilers js:babel-core/register --debug-brk --require ./test/test_helper.js './test/**/*.js'",
"test:cover": "istanbul cover --root src/ --include-all-sources --report lcov --report json --report text --report html _mocha -- -r babel-register -r test/test_helper.js 'test/**/*.test.js'",
"lint": "eslint src/ test/ || true",
"lint:failfast": "eslint src/ test/",
Expand Down
4 changes: 4 additions & 0 deletions src/higher_order_components/focusable.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@ module.exports = function Focusable(ComponentClass) {
window.removeEventListener('focus', this._onFocus, true)
}

// Handles most cases of the user clicking in another field, or anywhere
// outside the focusable element.
_onDocumentClick(e) {
this.setState({ focused: this._containsDOMElement(e.target) })
}

// Also cover the case where the user tabs out of a focusable element with
// keyboard (since this wouldn't create a click event).
_onFocus() {
this.setState({ focused: this._containsDOMElement(document.activeElement) })
}
Expand Down
103 changes: 103 additions & 0 deletions test/higher_order_components/focusable.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/* global describe, it, beforeEach */

import { jsdom } from 'jsdom'
import { usingJsdom } from '../test_helper'

import React from 'react'
import ReactDOM from 'react-dom'
import { expect } from 'chai'
// import Form from '../../src/components/form'
import { mount } from 'enzyme'
import td from 'testdouble'
import focusable from '../../src/higher_order_components/focusable.js'

class Layout extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
return (<div>
<span className="before" tabIndex="1" />
<ExampleFocusable className="focusable" />
<span className="after" />
</div>)
}
}

@focusable
class ExampleFocusable extends React.Component { // eslint-disable-line react/no-multi-comp
static propTypes = {
focused: React.PropTypes.boolean,
}
render() {
return (
<div className="theDiv">
<input ref="input" className="theInput" />
{this.props.focused && <p>Focused!</p>}
</div>
)
}
}

const sendFocusEvent = (el) => {
// .focus() only sets document.activeElement.
// We also need to dispatch the event.
el.focus()
el.dispatchEvent(new Event('focus'))
}

describe('@focusable decorator', () => {
// Note: Because this test uses `usingJsdom`, it executes as a single
// test with multiple assertions. `usingJsdom` cannot be used in a
// describe block, since it mutates globals, and tests execute at a
// different time.

it('toggles props.focused when child component is clicked/focused/blurred', () => {
const dom = jsdom('<html><div id="app"></div></html>')

usingJsdom(dom, () => {
ReactDOM.render(<Layout />, dom.getElementById('app'))

const app = dom.getElementById('app')
const input = dom.querySelectorAll('.theInput')[0]
const span = dom.querySelectorAll('.before')[0]

expect(app.textContent).to.equal('',
'initially renders child component with props.focused=false')

// click inside the Focusable
input.click()
expect(app.textContent).to.equal('Focused!',
'sets props.focused=true on child component when it is clicked')

// click outside the Focusable
span.click()
expect(app.textContent).to.equal('',
'sets props.focused=false on child component when another element is clicked')

// focus inside the Focusable
sendFocusEvent(input)
expect(app.textContent).to.equal('Focused!',
'sets props.focused=true on child component when it is focused')


// focus outside the Focusable
sendFocusEvent(span)
expect(app.textContent).to.equal('',
'sets props.focused=false on child component when another element is focused')
})
})


it('removes event listeners when component unmounts', () => {
// There is no programatic way to get a list of event listeners.
// Only way to test this is to monkey-patch window.removeEventListener.
const oldRemoveEventListener = window.removeEventListener
window.removeEventListener = td.function()

// Mount and immediately unmount => calls componentWillUnmount
mount(<Layout />).unmount()

td.verify(window.removeEventListener('click'), { ignoreExtraArgs: true })
td.verify(window.removeEventListener('focus'), { ignoreExtraArgs: true })

window.removeEventListener = oldRemoveEventListener
})
})
43 changes: 33 additions & 10 deletions test/test_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,39 @@ import { jsdom } from 'jsdom'
import chai from 'chai'
import dirtyChai from 'dirty-chai'

const doc = jsdom('<!doctype html><html><body></body></html>')
const win = doc.defaultView
chai.use(dirtyChai)

global.document = doc
global.window = win
const globalify = (doc, win) => {
global.document = doc
global.window = win

Object.keys(window).forEach((key) => {
if (!(key in global)) {
global[key] = window[key]
}
})
Object.keys(window).forEach((key) => {
if (!(key in global)) {
global[key] = window[key]
}
})
}

chai.use(dirtyChai)
{
const doc = jsdom('<!doctype html><html><body></body></html>')
const win = doc.defaultView
globalify(doc, win)
}

// Use `usingJsdom` to wrap any tests that need to use their own jsdom.
// It will swap in the supplied jsdom, execute the callback, and then
// restore the test_helper jsdom defined above.

// Note: Only use this in a single test (`it` block)! Putting it in
// a `describe` block will cause unwanted global pollution.
export const usingJsdom = (doc, callback) => {
const oldWin = global.window
const oldDoc = global.document

const win = doc.defaultView
globalify(doc, win)

callback()

globalify(oldDoc, oldWin)
}

0 comments on commit 5ff17e8

Please sign in to comment.