+ )
+ }
+}
+
+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('')
+
+ usingJsdom(dom, () => {
+ ReactDOM.render(, 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().unmount()
+
+ td.verify(window.removeEventListener('click'), { ignoreExtraArgs: true })
+ td.verify(window.removeEventListener('focus'), { ignoreExtraArgs: true })
+
+ window.removeEventListener = oldRemoveEventListener
+ })
+})
diff --git a/test/test_helper.js b/test/test_helper.js
index 1c9ef44..885b0c3 100644
--- a/test/test_helper.js
+++ b/test/test_helper.js
@@ -2,16 +2,39 @@ import { jsdom } from 'jsdom'
import chai from 'chai'
import dirtyChai from 'dirty-chai'
-const doc = jsdom('')
-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('')
+ 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)
+}