]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
rewrite modal unit tests
authorJohann-S <johann.servoire@gmail.com>
Wed, 1 May 2019 13:43:40 +0000 (15:43 +0200)
committerJohann-S <johann.servoire@gmail.com>
Tue, 23 Jul 2019 12:23:50 +0000 (14:23 +0200)
build/build-plugins.js
js/index.esm.js
js/index.umd.js
js/src/modal/modal.js [moved from js/src/modal.js with 95% similarity]
js/src/modal/modal.spec.js [new file with mode: 0644]
js/tests/unit/modal.js [deleted file]

index 6989e4f4db7e919975ef543032f2519829e4c142..d0d473f6665306cbe5b630265d76b8df32e8b9ae 100644 (file)
@@ -37,7 +37,7 @@ const bsPlugins = {
   Carousel: path.resolve(__dirname, '../js/src/carousel/carousel.js'),
   Collapse: path.resolve(__dirname, '../js/src/collapse/collapse.js'),
   Dropdown: path.resolve(__dirname, '../js/src/dropdown/dropdown.js'),
-  Modal: path.resolve(__dirname, '../js/src/modal.js'),
+  Modal: path.resolve(__dirname, '../js/src/modal/modal.js'),
   Popover: path.resolve(__dirname, '../js/src/popover.js'),
   ScrollSpy: path.resolve(__dirname, '../js/src/scrollspy.js'),
   Tab: path.resolve(__dirname, '../js/src/tab.js'),
index 3c3def944f84b8dfbd14abf448684436e9ff0b27..c2912d1910c0d05226275b72dd2e2d485a6d80cd 100644 (file)
@@ -10,7 +10,7 @@ import Button from './src/button/button'
 import Carousel from './src/carousel/carousel'
 import Collapse from './src/collapse/collapse'
 import Dropdown from './src/dropdown/dropdown'
-import Modal from './src/modal'
+import Modal from './src/modal/modal'
 import Popover from './src/popover'
 import ScrollSpy from './src/scrollspy'
 import Tab from './src/tab'
index ed6ba1e1d3e6d4388177853792ddc82d2e29b725..eafc90141dae736867678791eaab9ff704b42ffe 100644 (file)
@@ -10,7 +10,7 @@ import Button from './src/button/button'
 import Carousel from './src/carousel/carousel'
 import Collapse from './src/collapse/collapse'
 import Dropdown from './src/dropdown/dropdown'
-import Modal from './src/modal'
+import Modal from './src/modal/modal'
 import Popover from './src/popover'
 import ScrollSpy from './src/scrollspy'
 import Tab from './src/tab'
similarity index 95%
rename from js/src/modal.js
rename to js/src/modal/modal.js
index 9943f93e09dd760677c4b8794044c9b05c53b80f..4c430a01f5acbb38987f1763a51a64b0d6d667ed 100644 (file)
@@ -15,11 +15,11 @@ import {
   makeArray,
   reflow,
   typeCheckConfig
-} from './util/index'
-import Data from './dom/data'
-import EventHandler from './dom/event-handler'
-import Manipulator from './dom/manipulator'
-import SelectorEngine from './dom/selector-engine'
+} from '../util/index'
+import Data from '../dom/data'
+import EventHandler from '../dom/event-handler'
+import Manipulator from '../dom/manipulator'
+import SelectorEngine from '../dom/selector-engine'
 
 /**
  * ------------------------------------------------------------------------
@@ -171,7 +171,7 @@ class Modal {
 
     const hideEvent = EventHandler.trigger(this._element, Event.HIDE)
 
-    if (!this._isShown || hideEvent.defaultPrevented) {
+    if (hideEvent.defaultPrevented) {
       return
     }
 
@@ -310,14 +310,14 @@ class Modal {
           this.hide()
         }
       })
-    } else if (!this._isShown) {
+    } else {
       EventHandler.off(this._element, Event.KEYDOWN_DISMISS)
     }
   }
 
   _setResizeEvent() {
     if (this._isShown) {
-      EventHandler.on(window, Event.RESIZE, event => this.handleUpdate(event))
+      EventHandler.on(window, Event.RESIZE, () => this._adjustDialog())
     } else {
       EventHandler.off(window, Event.RESIZE)
     }
@@ -337,10 +337,8 @@ class Modal {
   }
 
   _removeBackdrop() {
-    if (this._backdrop) {
-      this._backdrop.parentNode.removeChild(this._backdrop)
-      this._backdrop = null
-    }
+    this._backdrop.parentNode.removeChild(this._backdrop)
+    this._backdrop = null
   }
 
   _showBackdrop(callback) {
@@ -381,10 +379,6 @@ class Modal {
 
       this._backdrop.classList.add(ClassName.SHOW)
 
-      if (!callback) {
-        return
-      }
-
       if (!animate) {
         callback()
         return
@@ -399,9 +393,7 @@ class Modal {
 
       const callbackRemove = () => {
         this._removeBackdrop()
-        if (callback) {
-          callback()
-        }
+        callback()
       }
 
       if (this._element.classList.contains(ClassName.FADE)) {
@@ -411,7 +403,7 @@ class Modal {
       } else {
         callbackRemove()
       }
-    } else if (callback) {
+    } else {
       callback()
     }
   }
@@ -557,19 +549,8 @@ class Modal {
  */
 
 EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
-  let target
   const selector = getSelectorFromElement(this)
-
-  if (selector) {
-    target = SelectorEngine.findOne(selector)
-  }
-
-  const config = Data.getData(target, DATA_KEY) ?
-    'toggle' :
-    {
-      ...Manipulator.getDataAttributes(target),
-      ...Manipulator.getDataAttributes(this)
-    }
+  const target = SelectorEngine.findOne(selector)
 
   if (this.tagName === 'A' || this.tagName === 'AREA') {
     event.preventDefault()
@@ -590,6 +571,11 @@ EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (
 
   let data = Data.getData(target, DATA_KEY)
   if (!data) {
+    const config = {
+      ...Manipulator.getDataAttributes(target),
+      ...Manipulator.getDataAttributes(this)
+    }
+
     data = new Modal(target, config)
   }
 
@@ -600,8 +586,9 @@ EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (
  * ------------------------------------------------------------------------
  * jQuery
  * ------------------------------------------------------------------------
+ * add .modal to jQuery only if jQuery is present
  */
-
+/* istanbul ignore if */
 if (typeof $ !== 'undefined') {
   const JQUERY_NO_CONFLICT = $.fn[NAME]
   $.fn[NAME] = Modal._jQueryInterface
diff --git a/js/src/modal/modal.spec.js b/js/src/modal/modal.spec.js
new file mode 100644 (file)
index 0000000..f1311c5
--- /dev/null
@@ -0,0 +1,974 @@
+import Modal from './modal'
+import EventHandler from '../dom/event-handler'
+import { makeArray } from '../util/index'
+
+/** Test helpers */
+import { getFixture, clearFixture, createEvent, jQueryMock } from '../../tests/helpers/fixture'
+
+describe('Modal', () => {
+  let fixtureEl
+  let style
+
+  beforeAll(() => {
+    fixtureEl = getFixture()
+
+    // Enable the scrollbar measurer
+    const css = '.modal-scrollbar-measure { position: absolute; top: -9999px; width: 50px; height: 50px; overflow: scroll; }'
+
+    style = document.createElement('style')
+    style.type = 'text/css'
+    style.appendChild(document.createTextNode(css))
+
+    document.head.appendChild(style)
+
+    // Simulate scrollbars
+    document.documentElement.style.paddingRight = '16px'
+  })
+
+  afterEach(() => {
+    clearFixture()
+
+    document.body.classList.remove('modal-open')
+    document.body.removeAttribute('style')
+    document.body.removeAttribute('data-padding-right')
+    const backdropList = makeArray(document.querySelectorAll('.modal-backdrop'))
+
+    backdropList.forEach(backdrop => {
+      document.body.removeChild(backdrop)
+    })
+
+    document.body.style.paddingRight = '0px'
+  })
+
+  afterAll(() => {
+    document.head.removeChild(style)
+    document.documentElement.style.paddingRight = '0px'
+  })
+
+  describe('VERSION', () => {
+    it('should return plugin version', () => {
+      expect(Modal.VERSION).toEqual(jasmine.any(String))
+    })
+  })
+
+  describe('Default', () => {
+    it('should return plugin default config', () => {
+      expect(Modal.Default).toEqual(jasmine.any(Object))
+    })
+  })
+
+  describe('toggle', () => {
+    it('should toggle a modal', done => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+      const originalPadding = '0px'
+
+      document.body.style.paddingRight = originalPadding
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        expect(document.body.getAttribute('data-padding-right')).toEqual(originalPadding, 'original body padding should be stored in data-padding-right')
+        modal.toggle()
+      })
+
+      modalEl.addEventListener('hidden.bs.modal', () => {
+        expect(document.body.getAttribute('data-padding-right')).toBeNull()
+        expect().nothing()
+        done()
+      })
+
+      modal.toggle()
+    })
+
+    it('should adjust the inline padding of fixed elements when opening and restore when closing', done => {
+      fixtureEl.innerHTML = [
+        '<div class="fixed-top" style="padding-right: 0px"></div>',
+        '<div class="modal"><div class="modal-dialog" /></div>'
+      ].join('')
+
+      const fixedEl = fixtureEl.querySelector('.fixed-top')
+      const originalPadding = parseInt(window.getComputedStyle(fixedEl).paddingRight, 10)
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        const expectedPadding = originalPadding + modal._getScrollbarWidth()
+        const currentPadding = parseInt(window.getComputedStyle(modalEl).paddingRight, 10)
+
+        expect(fixedEl.getAttribute('data-padding-right')).toEqual('0px', 'original fixed element padding should be stored in data-padding-right')
+        expect(currentPadding).toEqual(expectedPadding, 'fixed element padding should be adjusted while opening')
+        modal.toggle()
+      })
+
+      modalEl.addEventListener('hidden.bs.modal', () => {
+        const currentPadding = parseInt(window.getComputedStyle(modalEl).paddingRight, 10)
+
+        expect(fixedEl.getAttribute('data-padding-right')).toEqual(null, 'data-padding-right should be cleared after closing')
+        expect(currentPadding).toEqual(originalPadding, 'fixed element padding should be reset after closing')
+        done()
+      })
+
+      modal.toggle()
+    })
+
+    it('should adjust the inline margin of sticky elements when opening and restore when closing', done => {
+      fixtureEl.innerHTML = [
+        '<div class="sticky-top" style="margin-right: 0px;"></div>',
+        '<div class="modal"><div class="modal-dialog" /></div>'
+      ].join('')
+
+      const stickyTopEl = fixtureEl.querySelector('.sticky-top')
+      const originalMargin = parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        const expectedMargin = originalMargin - modal._getScrollbarWidth()
+        const currentMargin = parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
+
+        expect(stickyTopEl.getAttribute('data-margin-right')).toEqual('0px', 'original sticky element margin should be stored in data-margin-right')
+        expect(currentMargin).toEqual(expectedMargin, 'sticky element margin should be adjusted while opening')
+        modal.toggle()
+      })
+
+      modalEl.addEventListener('hidden.bs.modal', () => {
+        const currentMargin = parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
+
+        expect(stickyTopEl.getAttribute('data-margin-right')).toEqual(null, 'data-margin-right should be cleared after closing')
+        expect(currentMargin).toEqual(originalMargin, 'sticky element margin should be reset after closing')
+        done()
+      })
+
+      modal.toggle()
+    })
+
+    it('should ignore values set via CSS when trying to restore body padding after closing', done => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+      const styleTest = document.createElement('style')
+
+      styleTest.type = 'text/css'
+      styleTest.appendChild(document.createTextNode('body { padding-right: 7px; }'))
+      document.head.appendChild(styleTest)
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        modal.toggle()
+      })
+
+      modalEl.addEventListener('hidden.bs.modal', () => {
+        expect(window.getComputedStyle(document.body).paddingLeft).toEqual('0px', 'body does not have inline padding set')
+        document.head.removeChild(styleTest)
+        done()
+      })
+
+      modal.toggle()
+    })
+
+    it('should ignore other inline styles when trying to restore body padding after closing', done => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+      const styleTest = document.createElement('style')
+
+      styleTest.type = 'text/css'
+      styleTest.appendChild(document.createTextNode('body { padding-right: 7px; }'))
+
+      document.head.appendChild(styleTest)
+      document.body.style.color = 'red'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        modal.toggle()
+      })
+
+      modalEl.addEventListener('hidden.bs.modal', () => {
+        const bodyPaddingRight = document.body.style.paddingRight
+
+        expect(bodyPaddingRight === '0px' || bodyPaddingRight === '').toEqual(true, 'body does not have inline padding set')
+        expect(document.body.style.color).toEqual('red', 'body still has other inline styles set')
+        document.head.removeChild(styleTest)
+        document.body.removeAttribute('style')
+        done()
+      })
+
+      modal.toggle()
+    })
+
+    it('should properly restore non-pixel inline body padding after closing', done => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      document.body.style.paddingRight = '5%'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        modal.toggle()
+      })
+
+      modalEl.addEventListener('hidden.bs.modal', () => {
+        expect(document.body.style.paddingRight).toEqual('5%')
+        document.body.removeAttribute('style')
+        done()
+      })
+
+      modal.toggle()
+    })
+  })
+
+  describe('show', () => {
+    it('should show a modal', done => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      modalEl.addEventListener('show.bs.modal', e => {
+        expect(e).toBeDefined()
+      })
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        expect(modalEl.getAttribute('aria-modal')).toEqual('true')
+        expect(modalEl.getAttribute('aria-hidden')).toEqual(null)
+        expect(modalEl.style.display).toEqual('block')
+        expect(document.querySelector('.modal-backdrop')).toBeDefined()
+        done()
+      })
+
+      modal.show()
+    })
+
+    it('should show a modal without backdrop', done => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl, {
+        backdrop: false
+      })
+
+      modalEl.addEventListener('show.bs.modal', e => {
+        expect(e).toBeDefined()
+      })
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        expect(modalEl.getAttribute('aria-modal')).toEqual('true')
+        expect(modalEl.getAttribute('aria-hidden')).toEqual(null)
+        expect(modalEl.style.display).toEqual('block')
+        expect(document.querySelector('.modal-backdrop')).toBeNull()
+        done()
+      })
+
+      modal.show()
+    })
+
+    it('should show a modal and append the element', done => {
+      const modalEl = document.createElement('div')
+      const id = 'dynamicModal'
+
+      modalEl.setAttribute('id', id)
+      modalEl.classList.add('modal')
+      modalEl.innerHTML = '<div class="modal-dialog"></div>'
+
+      const modal = new Modal(modalEl)
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        const dynamicModal = document.getElementById(id)
+        expect(dynamicModal).toBeDefined()
+        dynamicModal.parentNode.removeChild(dynamicModal)
+        done()
+      })
+
+      modal.show()
+    })
+
+    it('should do nothing if a modal is shown', () => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      spyOn(EventHandler, 'trigger')
+      modal._isShown = true
+
+      modal.show()
+
+      expect(EventHandler.trigger).not.toHaveBeenCalled()
+    })
+
+    it('should do nothing if a modal is transitioning', () => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      spyOn(EventHandler, 'trigger')
+      modal._isTransitioning = true
+
+      modal.show()
+
+      expect(EventHandler.trigger).not.toHaveBeenCalled()
+    })
+
+    it('should not fire shown event when show is prevented', done => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      modalEl.addEventListener('show.bs.modal', e => {
+        e.preventDefault()
+
+        const expectedDone = () => {
+          expect().nothing()
+          done()
+        }
+
+        setTimeout(expectedDone, 10)
+      })
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        throw new Error('shown event triggered')
+      })
+
+      modal.show()
+    })
+
+    it('should set is transitioning if fade class is present', done => {
+      fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      modalEl.addEventListener('show.bs.modal', () => {
+        expect(modal._isTransitioning).toEqual(true)
+      })
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        expect(modal._isTransitioning).toEqual(false)
+        done()
+      })
+
+      modal.show()
+    })
+
+    it('should close modal when a click occured on data-dismiss="modal"', done => {
+      fixtureEl.innerHTML = [
+        '<div class="modal fade">',
+        '  <div class="modal-dialog">',
+        '    <div class="modal-header">',
+        '      <button type="button" data-dismiss="modal"></button>',
+        '    </div>',
+        '  </div>',
+        '</div>'
+      ].join('')
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const btnClose = fixtureEl.querySelector('[data-dismiss="modal"]')
+      const modal = new Modal(modalEl)
+
+      spyOn(modal, 'hide').and.callThrough()
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        btnClose.click()
+      })
+
+      modalEl.addEventListener('hidden.bs.modal', () => {
+        expect(modal.hide).toHaveBeenCalled()
+        done()
+      })
+
+      modal.show()
+    })
+
+    it('should set modal body scroll top to 0 if .modal-dialog-scrollable', done => {
+      fixtureEl.innerHTML = [
+        '<div class="modal fade">',
+        '  <div class="modal-dialog modal-dialog-scrollable">',
+        '    <div class="modal-body"></div>',
+        '  </div>',
+        '</div>'
+      ].join('')
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modalBody = modalEl.querySelector('.modal-body')
+      const modal = new Modal(modalEl)
+
+      spyOn(modal, 'hide').and.callThrough()
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        expect(modalBody.scrollTop).toEqual(0)
+        done()
+      })
+
+      modal.show()
+    })
+
+    it('should not enforce focus if focus equal to false', done => {
+      fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl, {
+        focus: false
+      })
+
+      spyOn(modal, '_enforceFocus')
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        expect(modal._enforceFocus).not.toHaveBeenCalled()
+        done()
+      })
+
+      modal.show()
+    })
+
+    it('should add listener when escape touch is pressed', done => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      spyOn(modal, 'hide').and.callThrough()
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        const keydownEscape = createEvent('keydown')
+        keydownEscape.which = 27
+
+        modalEl.dispatchEvent(keydownEscape)
+      })
+
+      modalEl.addEventListener('hidden.bs.modal', () => {
+        expect(modal.hide).toHaveBeenCalled()
+        done()
+      })
+
+      modal.show()
+    })
+
+    it('should do nothing when the pressed key is not escape', done => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      spyOn(modal, 'hide')
+
+      const expectDone = () => {
+        expect(modal.hide).not.toHaveBeenCalled()
+
+        done()
+      }
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        const keydownTab = createEvent('keydown')
+        keydownTab.which = 9
+
+        modalEl.dispatchEvent(keydownTab)
+        setTimeout(expectDone, 30)
+      })
+
+      modal.show()
+    })
+
+    it('should adjust dialog on resize', done => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      spyOn(modal, '_adjustDialog').and.callThrough()
+
+      const expectDone = () => {
+        expect(modal._adjustDialog).toHaveBeenCalled()
+
+        done()
+      }
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        const resizeEvent = createEvent('resize')
+
+        window.dispatchEvent(resizeEvent)
+        setTimeout(expectDone, 10)
+      })
+
+      modal.show()
+    })
+
+    it('should not close modal when clicking outside of modal-content if backdrop = false', done => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl, {
+        backdrop: false
+      })
+
+      const shownCallback = () => {
+        setTimeout(() => {
+          expect(modal._isShown).toEqual(true)
+          done()
+        }, 10)
+      }
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        modalEl.click()
+        shownCallback()
+      })
+
+      modalEl.addEventListener('hidden.bs.modal', () => {
+        throw new Error('Should not hide a modal')
+      })
+
+      modal.show()
+    })
+
+    it('should not adjust the inline body padding when it does not overflow', done => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+      const originalPadding = window.getComputedStyle(document.body).paddingRight
+
+      // Hide scrollbars to prevent the body overflowing
+      document.body.style.overflow = 'hidden'
+      document.documentElement.style.paddingRight = '0px'
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        const currentPadding = window.getComputedStyle(document.body).paddingRight
+
+        expect(currentPadding).toEqual(originalPadding, 'body padding should not be adjusted')
+
+        // Restore scrollbars
+        document.body.style.overflow = 'auto'
+        document.documentElement.style.paddingRight = '16px'
+        done()
+      })
+
+      modal.show()
+    })
+
+    it('should enforce focus', done => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const isIE11 = Boolean(window.MSInputMethodContext) && Boolean(document.documentMode)
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      spyOn(modal, '_enforceFocus').and.callThrough()
+
+      const focusInListener = () => {
+        expect(modal._element.focus).toHaveBeenCalled()
+        document.removeEventListener('focusin', focusInListener)
+        done()
+      }
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        expect(modal._enforceFocus).toHaveBeenCalled()
+
+        if (isIE11) {
+          done()
+          return
+        }
+
+        spyOn(modal._element, 'focus')
+
+        document.addEventListener('focusin', focusInListener)
+
+        const focusInEvent = createEvent('focusin', { bubbles: true })
+        Object.defineProperty(focusInEvent, 'target', {
+          value: fixtureEl
+        })
+
+        document.dispatchEvent(focusInEvent)
+      })
+
+      modal.show()
+    })
+  })
+
+  describe('hide', () => {
+    it('should hide a modal', done => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        modal.hide()
+      })
+
+      modalEl.addEventListener('hide.bs.modal', e => {
+        expect(e).toBeDefined()
+      })
+
+      modalEl.addEventListener('hidden.bs.modal', () => {
+        expect(modalEl.getAttribute('aria-modal')).toEqual(null)
+        expect(modalEl.getAttribute('aria-hidden')).toEqual('true')
+        expect(modalEl.style.display).toEqual('none')
+        expect(document.querySelector('.modal-backdrop')).toBeNull()
+        done()
+      })
+
+      modal.show()
+    })
+
+    it('should close modal when clicking outside of modal-content', done => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        modalEl.click()
+      })
+
+      modalEl.addEventListener('hidden.bs.modal', () => {
+        expect(modalEl.getAttribute('aria-modal')).toEqual(null)
+        expect(modalEl.getAttribute('aria-hidden')).toEqual('true')
+        expect(modalEl.style.display).toEqual('none')
+        expect(document.querySelector('.modal-backdrop')).toBeNull()
+        done()
+      })
+
+      modal.show()
+    })
+
+    it('should do nothing is the modal is not shown', () => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      modal.hide()
+
+      expect().nothing()
+    })
+
+    it('should do nothing is the modal is transitioning', () => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      modal._isTransitioning = true
+      modal.hide()
+
+      expect().nothing()
+    })
+
+    it('should not hide a modal if hide is prevented', done => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        modal.hide()
+      })
+
+      const hideCallback = () => {
+        setTimeout(() => {
+          expect(modal._isShown).toEqual(true)
+          done()
+        }, 10)
+      }
+
+      modalEl.addEventListener('hide.bs.modal', e => {
+        e.preventDefault()
+        hideCallback()
+      })
+
+      modalEl.addEventListener('hidden.bs.modal', () => {
+        throw new Error('should not trigger hidden')
+      })
+
+      modal.show()
+    })
+  })
+
+  describe('dispose', () => {
+    it('should dispose a modal', () => {
+      fixtureEl.innerHTML = '<div id="exampleModal" class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      expect(Modal._getInstance(modalEl)).toEqual(modal)
+
+      spyOn(EventHandler, 'off')
+
+      modal.dispose()
+
+      expect(Modal._getInstance(modalEl)).toEqual(null)
+      expect(EventHandler.off).toHaveBeenCalledTimes(4)
+    })
+  })
+
+  describe('handleUpdate', () => {
+    it('should call adjust dialog', () => {
+      fixtureEl.innerHTML = '<div id="exampleModal" class="modal"><div class="modal-dialog" /></div>'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+
+      spyOn(modal, '_adjustDialog')
+
+      modal.handleUpdate()
+
+      expect(modal._adjustDialog).toHaveBeenCalled()
+    })
+  })
+
+  describe('data-api', () => {
+    it('should open modal', done => {
+      fixtureEl.innerHTML = [
+        '<button type="button" data-toggle="modal" data-target="#exampleModal"></button>',
+        '<div id="exampleModal" class="modal"><div class="modal-dialog" /></div>'
+      ].join('')
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const trigger = fixtureEl.querySelector('[data-toggle="modal"]')
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        expect(modalEl.getAttribute('aria-modal')).toEqual('true')
+        expect(modalEl.getAttribute('aria-hidden')).toEqual(null)
+        expect(modalEl.style.display).toEqual('block')
+        expect(document.querySelector('.modal-backdrop')).toBeDefined()
+        done()
+      })
+
+      trigger.click()
+    })
+
+    it('should not recreate a new modal', done => {
+      fixtureEl.innerHTML = [
+        '<button type="button" data-toggle="modal" data-target="#exampleModal"></button>',
+        '<div id="exampleModal" class="modal"><div class="modal-dialog" /></div>'
+      ].join('')
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const modal = new Modal(modalEl)
+      const trigger = fixtureEl.querySelector('[data-toggle="modal"]')
+
+      spyOn(modal, 'show').and.callThrough()
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        expect(modal.show).toHaveBeenCalled()
+        done()
+      })
+
+      trigger.click()
+    })
+
+    it('should prevent default when the trigger is <a> or <area>', done => {
+      fixtureEl.innerHTML = [
+        '<a data-toggle="modal" href="#" data-target="#exampleModal"></a>',
+        '<div id="exampleModal" class="modal"><div class="modal-dialog" /></div>'
+      ].join('')
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const trigger = fixtureEl.querySelector('[data-toggle="modal"]')
+
+      spyOn(Event.prototype, 'preventDefault').and.callThrough()
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        expect(modalEl.getAttribute('aria-modal')).toEqual('true')
+        expect(modalEl.getAttribute('aria-hidden')).toEqual(null)
+        expect(modalEl.style.display).toEqual('block')
+        expect(document.querySelector('.modal-backdrop')).toBeDefined()
+        expect(Event.prototype.preventDefault).toHaveBeenCalled()
+        done()
+      })
+
+      trigger.click()
+    })
+
+    it('should focus the trigger on hide', done => {
+      fixtureEl.innerHTML = [
+        '<a data-toggle="modal" href="#" data-target="#exampleModal"></a>',
+        '<div id="exampleModal" class="modal"><div class="modal-dialog" /></div>'
+      ].join('')
+
+      // the element must be displayed, without that activeElement won't change
+      fixtureEl.style.display = 'block'
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const trigger = fixtureEl.querySelector('[data-toggle="modal"]')
+
+      spyOn(trigger, 'focus')
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        const modal = Modal._getInstance(modalEl)
+
+        modal.hide()
+      })
+
+      const hideListener = () => {
+        setTimeout(() => {
+          expect(trigger.focus).toHaveBeenCalled()
+          fixtureEl.style.display = 'none'
+          done()
+        }, 20)
+      }
+
+      modalEl.addEventListener('hidden.bs.modal', () => {
+        hideListener()
+      })
+
+      trigger.click()
+    })
+
+    it('should not focus the trigger if the modal is not visible', done => {
+      fixtureEl.innerHTML = [
+        '<a data-toggle="modal" href="#" data-target="#exampleModal"></a>',
+        '<div id="exampleModal" class="modal"><div class="modal-dialog" /></div>'
+      ].join('')
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const trigger = fixtureEl.querySelector('[data-toggle="modal"]')
+
+      spyOn(trigger, 'focus')
+
+      modalEl.addEventListener('shown.bs.modal', () => {
+        const modal = Modal._getInstance(modalEl)
+
+        modal.hide()
+      })
+
+      const hideListener = () => {
+        setTimeout(() => {
+          expect(trigger.focus).not.toHaveBeenCalled()
+          done()
+        }, 20)
+      }
+
+      modalEl.addEventListener('hidden.bs.modal', () => {
+        hideListener()
+      })
+
+      trigger.click()
+    })
+
+    it('should not focus the trigger if the modal is not shown', done => {
+      fixtureEl.innerHTML = [
+        '<a data-toggle="modal" href="#" data-target="#exampleModal"></a>',
+        '<div id="exampleModal" class="modal"><div class="modal-dialog" /></div>'
+      ].join('')
+
+      const modalEl = fixtureEl.querySelector('.modal')
+      const trigger = fixtureEl.querySelector('[data-toggle="modal"]')
+
+      spyOn(trigger, 'focus')
+
+      const showListener = () => {
+        setTimeout(() => {
+          expect(trigger.focus).not.toHaveBeenCalled()
+          done()
+        }, 10)
+      }
+
+      modalEl.addEventListener('show.bs.modal', e => {
+        e.preventDefault()
+        showListener()
+      })
+
+      trigger.click()
+    })
+  })
+
+  describe('_jQueryInterface', () => {
+    it('should create a modal', () => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const div = fixtureEl.querySelector('div')
+
+      jQueryMock.fn.modal = Modal._jQueryInterface
+      jQueryMock.elements = [div]
+
+      jQueryMock.fn.modal.call(jQueryMock)
+
+      expect(Modal._getInstance(div)).toBeDefined()
+    })
+
+    it('should not re create a modal', () => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const div = fixtureEl.querySelector('div')
+      const modal = new Modal(div)
+
+      jQueryMock.fn.modal = Modal._jQueryInterface
+      jQueryMock.elements = [div]
+
+      jQueryMock.fn.modal.call(jQueryMock)
+
+      expect(Modal._getInstance(div)).toEqual(modal)
+    })
+
+    it('should throw error on undefined method', () => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const div = fixtureEl.querySelector('div')
+      const action = 'undefinedMethod'
+
+      jQueryMock.fn.modal = Modal._jQueryInterface
+      jQueryMock.elements = [div]
+
+      try {
+        jQueryMock.fn.modal.call(jQueryMock, action)
+      } catch (error) {
+        expect(error.message).toEqual(`No method named "${action}"`)
+      }
+    })
+
+    it('should should call show method', () => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const div = fixtureEl.querySelector('div')
+      const modal = new Modal(div)
+
+      jQueryMock.fn.modal = Modal._jQueryInterface
+      jQueryMock.elements = [div]
+
+      spyOn(modal, 'show')
+
+      jQueryMock.fn.modal.call(jQueryMock, 'show')
+
+      expect(modal.show).toHaveBeenCalled()
+    })
+
+    it('should should not call show method', () => {
+      fixtureEl.innerHTML = '<div class="modal" data-show="false"><div class="modal-dialog" /></div>'
+
+      const div = fixtureEl.querySelector('div')
+
+      jQueryMock.fn.modal = Modal._jQueryInterface
+      jQueryMock.elements = [div]
+
+      spyOn(Modal.prototype, 'show')
+
+      jQueryMock.fn.modal.call(jQueryMock)
+
+      expect(Modal.prototype.show).not.toHaveBeenCalled()
+    })
+  })
+
+  describe('_getInstance', () => {
+    it('should return modal instance', () => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const div = fixtureEl.querySelector('div')
+      const modal = new Modal(div)
+
+      expect(Modal._getInstance(div)).toEqual(modal)
+    })
+
+    it('should return null when there is no modal instance', () => {
+      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" /></div>'
+
+      const div = fixtureEl.querySelector('div')
+
+      expect(Modal._getInstance(div)).toEqual(null)
+    })
+  })
+})
diff --git a/js/tests/unit/modal.js b/js/tests/unit/modal.js
deleted file mode 100644 (file)
index 0137802..0000000
+++ /dev/null
@@ -1,816 +0,0 @@
-$(function () {
-  'use strict'
-
-  window.Util = typeof bootstrap === 'undefined' ? Util : bootstrap.Util
-  var Modal = typeof window.bootstrap === 'undefined' ? window.Modal : window.bootstrap.Modal
-
-  QUnit.module('modal plugin')
-
-  QUnit.test('should be defined on jquery object', function (assert) {
-    assert.expect(1)
-    assert.ok($(document.body).modal, 'modal method is defined')
-  })
-
-  QUnit.module('modal', {
-    before: function () {
-      // Enable the scrollbar measurer
-      $('<style type="text/css"> .modal-scrollbar-measure { position: absolute; top: -9999px; width: 50px; height: 50px; overflow: scroll; } </style>').appendTo('head')
-      // Function to calculate the scrollbar width which is then compared to the padding or margin changes
-      $.fn.getScrollbarWidth = $.fn.modal.Constructor.prototype._getScrollbarWidth
-
-      // Simulate scrollbars
-      $('html').css('padding-right', '16px')
-    },
-    beforeEach: function () {
-      // Run all tests in noConflict mode -- it's the only way to ensure that the plugin works in noConflict mode
-      $.fn.bootstrapModal = $.fn.modal.noConflict()
-    },
-    afterEach: function () {
-      $('.modal-backdrop, #modal-test').remove()
-      $(document.body).removeClass('modal-open')
-      $.fn.modal = $.fn.bootstrapModal
-      delete $.fn.bootstrapModal
-      $('#qunit-fixture').html('')
-    }
-  })
-
-  QUnit.test('should provide no conflict', function (assert) {
-    assert.expect(1)
-    assert.strictEqual(typeof $.fn.modal, 'undefined', 'modal was set back to undefined (orig value)')
-  })
-
-  QUnit.test('should throw explicit error on undefined method', function (assert) {
-    assert.expect(1)
-    var $el = $('<div id="modal-test"><div class="modal-dialog" /></div>')
-    $el.bootstrapModal()
-    try {
-      $el.bootstrapModal('noMethod')
-    } catch (error) {
-      assert.strictEqual(error.message, 'No method named "noMethod"')
-    }
-  })
-
-  QUnit.test('should return jquery collection containing the element', function (assert) {
-    assert.expect(2)
-    var $el = $('<div id="modal-test"><div class="modal-dialog" /></div>')
-    var $modal = $el.bootstrapModal()
-    assert.ok($modal instanceof $, 'returns jquery collection')
-    assert.strictEqual($modal[0], $el[0], 'collection contains element')
-  })
-
-  QUnit.test('should expose defaults var for settings', function (assert) {
-    assert.expect(1)
-    assert.ok($.fn.bootstrapModal.Constructor.Default, 'default object exposed')
-  })
-
-  QUnit.test('should insert into dom when show method is called', function (assert) {
-    assert.expect(1)
-    var done = assert.async()
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('shown.bs.modal', function () {
-        assert.notEqual($('#modal-test').length, 0, 'modal inserted into dom')
-        done()
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should fire show event', function (assert) {
-    assert.expect(1)
-    var done = assert.async()
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('show.bs.modal', function () {
-        assert.ok(true, 'show event fired')
-        done()
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should not fire shown when show was prevented', function (assert) {
-    assert.expect(1)
-    var done = assert.async()
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('show.bs.modal', function (e) {
-        e.preventDefault()
-        assert.ok(true, 'show event fired')
-        done()
-      })
-      .on('shown.bs.modal', function () {
-        assert.ok(false, 'shown event fired')
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should hide modal when hide is called', function (assert) {
-    assert.expect(3)
-    var done = assert.async()
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('shown.bs.modal', function () {
-        assert.ok($('#modal-test').is(':visible'), 'modal visible')
-        assert.notEqual($('#modal-test').length, 0, 'modal inserted into dom')
-        $(this).bootstrapModal('hide')
-      })
-      .on('hidden.bs.modal', function () {
-        assert.ok(!$('#modal-test').is(':visible'), 'modal hidden')
-        done()
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should toggle when toggle is called', function (assert) {
-    assert.expect(3)
-    var done = assert.async()
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('shown.bs.modal', function () {
-        assert.ok($('#modal-test').is(':visible'), 'modal visible')
-        assert.notEqual($('#modal-test').length, 0, 'modal inserted into dom')
-        $(this).bootstrapModal('toggle')
-      })
-      .on('hidden.bs.modal', function () {
-        assert.ok(!$('#modal-test').is(':visible'), 'modal hidden')
-        done()
-      })
-      .bootstrapModal('toggle')
-  })
-
-  QUnit.test('should remove from dom when click [data-dismiss="modal"]', function (assert) {
-    assert.expect(3)
-    var done = assert.async()
-    $('<div id="modal-test"><div class="modal-dialog" /><span class="close" data-dismiss="modal"/></div>')
-      .on('shown.bs.modal', function () {
-        assert.ok($('#modal-test').is(':visible'), 'modal visible')
-        assert.notEqual($('#modal-test').length, 0, 'modal inserted into dom')
-        $(this).find('.close').trigger('click')
-      })
-      .on('hidden.bs.modal', function () {
-        assert.ok(!$('#modal-test').is(':visible'), 'modal hidden')
-        done()
-      })
-      .bootstrapModal('toggle')
-  })
-
-  QUnit.test('should allow modal close with "backdrop:false"', function (assert) {
-    assert.expect(2)
-    var done = assert.async()
-
-    $('<div id="modal-test" data-backdrop="false"><div class="modal-dialog" /></div>')
-      .on('shown.bs.modal', function () {
-        assert.ok($('#modal-test').is(':visible'), 'modal visible')
-        $(this).bootstrapModal('hide')
-      })
-      .on('hidden.bs.modal', function () {
-        assert.ok(!$('#modal-test').is(':visible'), 'modal hidden')
-        done()
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should close modal when clicking outside of modal-content', function (assert) {
-    assert.expect(3)
-    var done = assert.async()
-
-    $('<div id="modal-test"><div class="modal-dialog" /><div class="contents"/></div>')
-      .on('shown.bs.modal', function () {
-        assert.notEqual($('#modal-test').length, 0, 'modal inserted into dom')
-        $('.contents').trigger('click')
-        assert.ok($('#modal-test').is(':visible'), 'modal visible')
-        $('#modal-test').trigger('click')
-      })
-      .on('hidden.bs.modal', function () {
-        assert.ok(!$('#modal-test').is(':visible'), 'modal hidden')
-        done()
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should not close modal when clicking outside of modal-content if data-backdrop="true"', function (assert) {
-    assert.expect(1)
-    var done = assert.async()
-
-    $('<div id="modal-test" data-backdrop="false"><div class="modal-dialog" /><div class="contents"/></div>')
-      .on('shown.bs.modal', function () {
-        $('#modal-test').trigger('click')
-        assert.ok($('#modal-test').is(':visible'), 'modal not hidden')
-        done()
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should close modal when escape key is pressed via keydown', function (assert) {
-    assert.expect(3)
-    var done = assert.async()
-
-    var $div = $('<div id="modal-test"><div class="modal-dialog" /></div>')
-    $div
-      .on('shown.bs.modal', function () {
-        assert.ok($('#modal-test').length, 'modal inserted into dom')
-        assert.ok($('#modal-test').is(':visible'), 'modal visible')
-
-        var evt = document.createEvent('HTMLEvents')
-        evt.initEvent('keydown', true, true)
-        evt.which = 27
-
-        $div[0].dispatchEvent(evt)
-
-        setTimeout(function () {
-          assert.ok(!$('#modal-test').is(':visible'), 'modal hidden')
-          $div.remove()
-          done()
-        }, 0)
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should not close modal when escape key is pressed via keyup', function (assert) {
-    assert.expect(3)
-    var done = assert.async()
-
-    var $div = $('<div id="modal-test"><div class="modal-dialog" /></div>')
-    $div
-      .on('shown.bs.modal', function () {
-        assert.ok($('#modal-test').length, 'modal inserted into dom')
-        assert.ok($('#modal-test').is(':visible'), 'modal visible')
-        $div.trigger($.Event('keyup', {
-          which: 27
-        }))
-
-        setTimeout(function () {
-          assert.ok($div.is(':visible'), 'modal still visible')
-          $div.remove()
-          done()
-        }, 0)
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should trigger hide event once when clicking outside of modal-content', function (assert) {
-    assert.expect(1)
-    var done = assert.async()
-
-    var triggered
-
-    $('<div id="modal-test"><div class="modal-dialog" /><div class="contents"/></div>')
-      .on('shown.bs.modal', function () {
-        triggered = 0
-        $('#modal-test').trigger('click')
-      })
-      .on('hide.bs.modal', function () {
-        triggered += 1
-        assert.strictEqual(triggered, 1, 'modal hide triggered once')
-        done()
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should remove aria-hidden attribute when shown, add it back when hidden', function (assert) {
-    assert.expect(3)
-    var done = assert.async()
-
-    $('<div id="modal-test" aria-hidden="true"><div class="modal-dialog" /></div>')
-      .on('shown.bs.modal', function () {
-        assert.notOk($('#modal-test').is('[aria-hidden]'), 'aria-hidden attribute removed')
-        $(this).bootstrapModal('hide')
-      })
-      .on('hidden.bs.modal', function () {
-        assert.ok($('#modal-test').is('[aria-hidden]'), 'aria-hidden attribute added')
-        assert.strictEqual($('#modal-test').attr('aria-hidden'), 'true', 'correct aria-hidden="true" added')
-        done()
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should add aria-modal attribute when shown, remove it again when hidden', function (assert) {
-    assert.expect(3)
-    var done = assert.async()
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('shown.bs.modal', function () {
-        assert.ok($('#modal-test').is('[aria-modal]'), 'aria-modal attribute added')
-        assert.strictEqual($('#modal-test').attr('aria-modal'), 'true', 'correct aria-modal="true" added')
-        $(this).bootstrapModal('hide')
-      })
-      .on('hidden.bs.modal', function () {
-        assert.notOk($('#modal-test').is('[aria-modal]'), 'aria-modal attribute removed')
-        done()
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should close reopened modal with [data-dismiss="modal"] click', function (assert) {
-    assert.expect(2)
-    var done = assert.async()
-    var html = [
-      '<div id="modal-test">',
-      '  <div class="modal-dialog">',
-      '    <div class="contents"><div id="close" data-dismiss="modal"/></div>',
-      '  </div>',
-      '</div>'
-    ].join('')
-
-    $(html)
-      .one('shown.bs.modal', function () {
-        $('#close').trigger('click')
-      })
-      .one('hidden.bs.modal', function () {
-        // After one open-close cycle
-        assert.ok(!$('#modal-test').is(':visible'), 'modal hidden')
-
-        var $this = $(this)
-        setTimeout(function () {
-          $this
-            .one('shown.bs.modal', function () {
-              $('#close').trigger('click')
-            })
-            .one('hidden.bs.modal', function () {
-              assert.ok(!$('#modal-test').is(':visible'), 'modal hidden')
-              done()
-            })
-            .bootstrapModal('show')
-        }, 0)
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should restore focus to toggling element when modal is hidden after having been opened via data-api', function (assert) {
-    assert.expect(1)
-    var done = assert.async()
-    var $toggleBtn = $('<button data-toggle="modal" data-target="#modal-test"/>').appendTo('#qunit-fixture')
-    var html = [
-      '<div id="modal-test">',
-      '  <div class="modal-dialog">',
-      '    <div class="contents"><div id="close" data-dismiss="modal"/></div>',
-      '  </div>',
-      '</div>'
-    ].join('')
-
-    $(html)
-      .on('hidden.bs.modal', function () {
-        setTimeout(function () {
-          assert.ok($(document.activeElement).is($toggleBtn), 'toggling element is once again focused')
-          done()
-        }, 0)
-      })
-      .on('shown.bs.modal', function () {
-        $('#close').trigger('click')
-      })
-      .appendTo('#qunit-fixture')
-
-    $toggleBtn.trigger('click')
-  })
-
-  QUnit.test('should not restore focus to toggling element if the associated show event gets prevented', function (assert) {
-    assert.expect(1)
-    var done = assert.async()
-    var $toggleBtn = $('<button data-toggle="modal" data-target="#modal-test"/>').appendTo('#qunit-fixture')
-    var $otherBtn = $('<button id="other-btn"/>').appendTo('#qunit-fixture')
-    var html = [
-      '<div id="modal-test">',
-      '  <div class="modal-dialog">',
-      '    <div class="contents"><div id="close" data-dismiss="modal"/></div>',
-      '  </div>',
-      '</div>'
-    ].join('')
-
-    $(html)
-      .one('show.bs.modal', function (e) {
-        e.preventDefault()
-        $otherBtn.trigger('focus')
-        setTimeout($.proxy(function () {
-          $(this).bootstrapModal('show')
-        }, this), 0)
-      })
-      .on('hidden.bs.modal', function () {
-        setTimeout(function () {
-          assert.ok($(document.activeElement).is($otherBtn), 'focus returned to toggling element')
-          done()
-        }, 0)
-      })
-      .on('shown.bs.modal', function () {
-        $('#close').trigger('click')
-      })
-      .appendTo('#qunit-fixture')
-
-    $toggleBtn.trigger('click')
-  })
-
-  QUnit.test('should adjust the inline padding of the modal when opening', function (assert) {
-    assert.expect(1)
-    var done = assert.async()
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('shown.bs.modal', function () {
-        var expectedPadding = parseInt($(this).getScrollbarWidth(), 10)
-        var currentPadding = parseInt($(this).css('padding-right'), 10)
-        assert.strictEqual(currentPadding, expectedPadding, 'modal padding should be adjusted while opening')
-        done()
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should adjust the inline body padding when opening and restore when closing', function (assert) {
-    assert.expect(2)
-    var done = assert.async()
-    var $body = $(document.body)
-    var originalPadding = parseInt($body.css('padding-right'), 10)
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('hidden.bs.modal', function () {
-        var currentPadding = parseInt($body.css('padding-right'), 10)
-        assert.strictEqual(currentPadding, originalPadding, 'body padding should be reset after closing')
-        $body.removeAttr('style')
-        done()
-      })
-      .on('shown.bs.modal', function () {
-        var expectedPadding = parseInt(originalPadding, 10) + parseInt($(this).getScrollbarWidth(), 10)
-        var currentPadding = parseInt($body.css('padding-right'), 10)
-        assert.strictEqual(currentPadding, expectedPadding, 'body padding should be adjusted while opening')
-        $(this).bootstrapModal('hide')
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should store the original body padding in data-padding-right before showing', function (assert) {
-    assert.expect(2)
-    var done = assert.async()
-    var $body = $(document.body)
-    var originalPadding = '0px'
-    $body.css('padding-right', originalPadding)
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('hidden.bs.modal', function () {
-        assert.strictEqual(document.body.getAttribute('data-padding-right'), null, 'data-padding-right should be cleared after closing')
-        $body.removeAttr('style')
-        done()
-      })
-      .on('shown.bs.modal', function () {
-        assert.strictEqual($body.data('padding-right'), originalPadding, 'original body padding should be stored in data-padding-right')
-        $(this).bootstrapModal('hide')
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should not adjust the inline body padding when it does not overflow', function (assert) {
-    assert.expect(1)
-    var done = assert.async()
-    var $body = $(document.body)
-    var originalPadding = $body.css('padding-right')
-
-    // Hide scrollbars to prevent the body overflowing
-    $body.css('overflow', 'hidden') // Real scrollbar (for in-browser testing)
-    $('html').css('padding-right', '0px') // Simulated scrollbar (for PhantomJS)
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('shown.bs.modal', function () {
-        var currentPadding = $body.css('padding-right')
-        assert.strictEqual(currentPadding, originalPadding, 'body padding should not be adjusted')
-        $(this).bootstrapModal('hide')
-
-        // Restore scrollbars
-        $body.css('overflow', 'auto')
-        $('html').css('padding-right', '16px')
-        done()
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should adjust the inline padding of fixed elements when opening and restore when closing', function (assert) {
-    assert.expect(2)
-    var done = assert.async()
-    var $element = $('<div class="fixed-top"></div>').appendTo('#qunit-fixture')
-    var originalPadding = parseInt($element.css('padding-right'), 10)
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('hidden.bs.modal', function () {
-        var currentPadding = parseInt($element.css('padding-right'), 10)
-        assert.strictEqual(currentPadding, originalPadding, 'fixed element padding should be reset after closing')
-        $element.remove()
-        done()
-      })
-      .on('shown.bs.modal', function () {
-        var expectedPadding = parseFloat(originalPadding) + parseInt($(this).getScrollbarWidth(), 10)
-        var currentPadding = parseInt($element.css('padding-right'), 10)
-        assert.strictEqual(currentPadding, expectedPadding, 'fixed element padding should be adjusted while opening')
-        $(this).bootstrapModal('hide')
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should store the original padding of fixed elements in data-padding-right before showing', function (assert) {
-    assert.expect(2)
-    var done = assert.async()
-    var $element = $('<div class="fixed-top"></div>').appendTo('#qunit-fixture')
-    var originalPadding = '0px'
-    $element.css('padding-right', originalPadding)
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('hidden.bs.modal', function () {
-        assert.strictEqual($element[0].getAttribute('data-padding-right'), null, 'data-padding-right should be cleared after closing')
-        $element.remove()
-        done()
-      })
-      .on('shown.bs.modal', function () {
-        assert.strictEqual($element.data('padding-right'), originalPadding, 'original fixed element padding should be stored in data-padding-right')
-        $(this).bootstrapModal('hide')
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should adjust the inline margin of sticky elements when opening and restore when closing', function (assert) {
-    assert.expect(2)
-    var done = assert.async()
-    var $element = $('<div class="sticky-top"></div>').appendTo('#qunit-fixture')
-    var originalPadding = parseInt($element.css('margin-right'), 10)
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('hidden.bs.modal', function () {
-        var currentPadding = parseInt($element.css('margin-right'), 10)
-        assert.strictEqual(currentPadding, originalPadding, 'sticky element margin should be reset after closing')
-        $element.remove()
-        done()
-      })
-      .on('shown.bs.modal', function () {
-        var expectedPadding = parseFloat(originalPadding) - $(this).getScrollbarWidth()
-        var currentPadding = parseInt($element.css('margin-right'), 10)
-        assert.strictEqual(currentPadding, expectedPadding, 'sticky element margin should be adjusted while opening')
-        $(this).bootstrapModal('hide')
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should store the original margin of sticky elements in data-margin-right before showing', function (assert) {
-    assert.expect(2)
-    var done = assert.async()
-    var $element = $('<div class="sticky-top"></div>').appendTo('#qunit-fixture')
-    var originalPadding = '0px'
-    $element.css('margin-right', originalPadding)
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('hidden.bs.modal', function () {
-        assert.strictEqual($element[0].getAttribute('data-margin-right'), null, 'data-margin-right should be cleared after closing')
-        $element.remove()
-        done()
-      })
-      .on('shown.bs.modal', function () {
-        assert.strictEqual($element.data('margin-right'), originalPadding, 'original sticky element margin should be stored in data-margin-right')
-        $(this).bootstrapModal('hide')
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should ignore values set via CSS when trying to restore body padding after closing', function (assert) {
-    assert.expect(1)
-    var done = assert.async()
-    var $body = $(document.body)
-    var $style = $('<style>body { padding-right: 42px; }</style>').appendTo('head')
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('hidden.bs.modal', function () {
-        assert.strictEqual($body.css('padding-left'), '0px', 'body does not have inline padding set')
-        $style.remove()
-        done()
-      })
-      .on('shown.bs.modal', function () {
-        $(this).bootstrapModal('hide')
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should ignore other inline styles when trying to restore body padding after closing', function (assert) {
-    assert.expect(2)
-    var done = assert.async()
-    var $body = $(document.body)
-    var $style = $('<style>body { padding-right: 42px; }</style>').appendTo('head')
-
-    $body.css('color', 'red')
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('hidden.bs.modal', function () {
-        assert.strictEqual($body[0].style.paddingRight, '', 'body does not have inline padding set')
-        assert.strictEqual($body[0].style.color, 'red', 'body still has other inline styles set')
-        $body.removeAttr('style')
-        $style.remove()
-        done()
-      })
-      .on('shown.bs.modal', function () {
-        $(this).bootstrapModal('hide')
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should properly restore non-pixel inline body padding after closing', function (assert) {
-    assert.expect(1)
-    var done = assert.async()
-    var $body = $(document.body)
-
-    $body.css('padding-right', '5%')
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>')
-      .on('hidden.bs.modal', function () {
-        assert.strictEqual($body[0].style.paddingRight, '5%', 'body does not have inline padding set')
-        $body.removeAttr('style')
-        done()
-      })
-      .on('shown.bs.modal', function () {
-        $(this).bootstrapModal('hide')
-      })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should not follow link in area tag', function (assert) {
-    assert.expect(2)
-
-    $('<map><area id="test" shape="default" data-toggle="modal" data-target="#modal-test" href="demo.html"/></map>')
-      .appendTo('#qunit-fixture')
-
-    var modalHtml = [
-      '<div id="modal-test">',
-      '  <div class="modal-dialog">',
-      '    <div class="contents"><div id="close" data-dismiss="modal"/></div>',
-      '  </div>',
-      '</div>'
-    ].join('')
-
-    $(modalHtml)
-      .appendTo('#qunit-fixture')
-
-    // We need to use CustomEvent here to have a working preventDefault in IE tests.
-    var evt = new CustomEvent('click', {
-      bubbles: true,
-      cancelable: true
-    })
-
-    $('#test')
-      .on('click.bs.modal.data-api', function (event) {
-        assert.notOk(event.defaultPrevented, 'navigating to href will happen')
-      })
-
-    $('#test')[0].dispatchEvent(evt)
-    assert.ok(evt.defaultPrevented, 'model shown instead of navigating to href')
-  })
-
-  QUnit.test('should not try to open a modal which is already visible', function (assert) {
-    assert.expect(1)
-    var done = assert.async()
-    var count = 0
-
-    $('<div id="modal-test"><div class="modal-dialog" /></div>').on('shown.bs.modal', function () {
-      count++
-    }).on('hidden.bs.modal', function () {
-      assert.strictEqual(count, 1, 'show() runs only once')
-      done()
-    })
-      .bootstrapModal('show')
-      .bootstrapModal('show')
-      .bootstrapModal('hide')
-  })
-
-  QUnit.test('transition duration should be the modal-dialog duration before triggering shown event', function (assert) {
-    assert.expect(1)
-    var done = assert.async()
-    var style = [
-      '<style>',
-      '  .modal.fade .modal-dialog {',
-      '    transition: -webkit-transform .3s ease-out;',
-      '    transition: transform .3s ease-out;',
-      '    transition: transform .3s ease-out,-webkit-transform .3s ease-out;',
-      '    -webkit-transform: translate(0,-50px);',
-      '    transform: translate(0,-50px);',
-      '  }',
-      '</style>'
-    ].join('')
-
-    var $style = $(style).appendTo('head')
-    var modalHTML = [
-      '<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">',
-      '  <div class="modal-dialog" role="document">',
-      '    <div class="modal-content">',
-      '      <div class="modal-body">...</div>',
-      '    </div>',
-      '  </div>',
-      '</div>'
-    ].join('')
-
-    var $modal = $(modalHTML).appendTo('#qunit-fixture')
-
-    $modal.on('shown.bs.modal', function () {
-      $style.remove()
-      assert.ok(true)
-      done()
-    })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should dispose modal', function (assert) {
-    assert.expect(2)
-    var done = assert.async()
-
-    var $modal = $([
-      '<div id="modal-test">',
-      '  <div class="modal-dialog">',
-      '    <div class="modal-content">',
-      '      <div class="modal-body" />',
-      '    </div>',
-      '  </div>',
-      '</div>'
-    ].join('')).appendTo('#qunit-fixture')
-
-    $modal.on('shown.bs.modal', function () {
-      var modal = Modal._getInstance($modal[0])
-      var spy = sinon.spy($modal[0], 'removeEventListener')
-
-      modal.dispose()
-
-      assert.ok(!Modal._getInstance($modal[0]), 'modal data object was disposed')
-      assert.ok(spy.called)
-      done()
-    }).bootstrapModal('show')
-  })
-
-  QUnit.test('should enforce focus', function (assert) {
-    var isIE11 = Boolean(window.MSInputMethodContext) && Boolean(document.documentMode)
-
-    if (isIE11) {
-      assert.expect(1)
-    } else {
-      assert.expect(2)
-    }
-
-    var done = assert.async()
-
-    var $modal = $([
-      '<div id="modal-test" data-show="false">',
-      '  <div class="modal-dialog">',
-      '    <div class="modal-content">',
-      '      <div class="modal-body" />',
-      '    </div>',
-      '  </div>',
-      '</div>'
-    ].join(''))
-      .bootstrapModal()
-      .appendTo('#qunit-fixture')
-
-    var modal = Modal._getInstance($modal[0])
-    var spy = sinon.spy(modal, '_enforceFocus')
-
-    $modal.one('shown.bs.modal', function () {
-      assert.ok(spy.called, '_enforceFocus called')
-      var spyFocus = sinon.spy(modal._element, 'focus')
-
-      function focusInListener() {
-        assert.ok(spyFocus.called)
-        document.removeEventListener('focusin', focusInListener)
-        done()
-      }
-
-      if (isIE11) {
-        done()
-      } else {
-        document.addEventListener('focusin', focusInListener)
-
-        var focusInEvent = new Event('focusin')
-        Object.defineProperty(focusInEvent, 'target', {
-          value: $('#qunit-fixture')[0]
-        })
-
-        document.dispatchEvent(focusInEvent)
-      }
-    })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should scroll to top of the modal body if the modal has .modal-dialog-scrollable class', function (assert) {
-    assert.expect(2)
-    var done = assert.async()
-
-    var $modal = $([
-      '<div id="modal-test">',
-      '  <div class="modal-dialog modal-dialog-scrollable">',
-      '    <div class="modal-content">',
-      '      <div class="modal-body" style="height: 100px; overflow-y: auto;">',
-      '        <div style="height: 200px" />',
-      '      </div>',
-      '    </div>',
-      '  </div>',
-      '</div>'
-    ].join('')).appendTo('#qunit-fixture')
-
-    var $modalBody = $('.modal-body')
-    $modalBody.scrollTop(100)
-    assert.ok($modalBody.scrollTop() > 95 && $modalBody.scrollTop() <= 100)
-
-    $modal.on('shown.bs.modal', function () {
-      assert.strictEqual($modalBody.scrollTop(), 0, 'modal body scrollTop should be 0 when opened')
-      done()
-    })
-      .bootstrapModal('show')
-  })
-
-  QUnit.test('should return the version', function (assert) {
-    assert.expect(1)
-    assert.strictEqual(typeof Modal.VERSION, 'string')
-  })
-})