aboutsummaryrefslogtreecommitdiff
path: root/js/src/modal
diff options
context:
space:
mode:
authorJohann-S <[email protected]>2019-05-01 15:43:40 +0200
committerJohann-S <[email protected]>2019-07-23 14:23:50 +0200
commit1ac07a66ce56e6ee3d2b48a649e957417f56c83e (patch)
treec255b2c904d40986adaf799c0ad87e986936c9f5 /js/src/modal
parente916a9bc03ba916d10f9deef630f810fee918bef (diff)
downloadbootstrap-1ac07a66ce56e6ee3d2b48a649e957417f56c83e.tar.xz
bootstrap-1ac07a66ce56e6ee3d2b48a649e957417f56c83e.zip
rewrite modal unit tests
Diffstat (limited to 'js/src/modal')
-rw-r--r--js/src/modal/modal.js602
-rw-r--r--js/src/modal/modal.spec.js974
2 files changed, 1576 insertions, 0 deletions
diff --git a/js/src/modal/modal.js b/js/src/modal/modal.js
new file mode 100644
index 000000000..4c430a01f
--- /dev/null
+++ b/js/src/modal/modal.js
@@ -0,0 +1,602 @@
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v4.3.1): modal.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+import {
+ jQuery as $,
+ TRANSITION_END,
+ emulateTransitionEnd,
+ getSelectorFromElement,
+ getTransitionDurationFromElement,
+ isVisible,
+ 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'
+
+/**
+ * ------------------------------------------------------------------------
+ * Constants
+ * ------------------------------------------------------------------------
+ */
+
+const NAME = 'modal'
+const VERSION = '4.3.1'
+const DATA_KEY = 'bs.modal'
+const EVENT_KEY = `.${DATA_KEY}`
+const DATA_API_KEY = '.data-api'
+const ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key
+
+const Default = {
+ backdrop: true,
+ keyboard: true,
+ focus: true,
+ show: true
+}
+
+const DefaultType = {
+ backdrop: '(boolean|string)',
+ keyboard: 'boolean',
+ focus: 'boolean',
+ show: 'boolean'
+}
+
+const Event = {
+ HIDE: `hide${EVENT_KEY}`,
+ HIDDEN: `hidden${EVENT_KEY}`,
+ SHOW: `show${EVENT_KEY}`,
+ SHOWN: `shown${EVENT_KEY}`,
+ FOCUSIN: `focusin${EVENT_KEY}`,
+ RESIZE: `resize${EVENT_KEY}`,
+ CLICK_DISMISS: `click.dismiss${EVENT_KEY}`,
+ KEYDOWN_DISMISS: `keydown.dismiss${EVENT_KEY}`,
+ MOUSEUP_DISMISS: `mouseup.dismiss${EVENT_KEY}`,
+ MOUSEDOWN_DISMISS: `mousedown.dismiss${EVENT_KEY}`,
+ CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}`
+}
+
+const ClassName = {
+ SCROLLABLE: 'modal-dialog-scrollable',
+ SCROLLBAR_MEASURER: 'modal-scrollbar-measure',
+ BACKDROP: 'modal-backdrop',
+ OPEN: 'modal-open',
+ FADE: 'fade',
+ SHOW: 'show'
+}
+
+const Selector = {
+ DIALOG: '.modal-dialog',
+ MODAL_BODY: '.modal-body',
+ DATA_TOGGLE: '[data-toggle="modal"]',
+ DATA_DISMISS: '[data-dismiss="modal"]',
+ FIXED_CONTENT: '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',
+ STICKY_CONTENT: '.sticky-top'
+}
+
+/**
+ * ------------------------------------------------------------------------
+ * Class Definition
+ * ------------------------------------------------------------------------
+ */
+
+class Modal {
+ constructor(element, config) {
+ this._config = this._getConfig(config)
+ this._element = element
+ this._dialog = SelectorEngine.findOne(Selector.DIALOG, element)
+ this._backdrop = null
+ this._isShown = false
+ this._isBodyOverflowing = false
+ this._ignoreBackdropClick = false
+ this._isTransitioning = false
+ this._scrollbarWidth = 0
+ Data.setData(element, DATA_KEY, this)
+ }
+
+ // Getters
+
+ static get VERSION() {
+ return VERSION
+ }
+
+ static get Default() {
+ return Default
+ }
+
+ // Public
+
+ toggle(relatedTarget) {
+ return this._isShown ? this.hide() : this.show(relatedTarget)
+ }
+
+ show(relatedTarget) {
+ if (this._isShown || this._isTransitioning) {
+ return
+ }
+
+ if (this._element.classList.contains(ClassName.FADE)) {
+ this._isTransitioning = true
+ }
+
+ const showEvent = EventHandler.trigger(this._element, Event.SHOW, {
+ relatedTarget
+ })
+
+ if (this._isShown || showEvent.defaultPrevented) {
+ return
+ }
+
+ this._isShown = true
+
+ this._checkScrollbar()
+ this._setScrollbar()
+
+ this._adjustDialog()
+
+ this._setEscapeEvent()
+ this._setResizeEvent()
+
+ EventHandler.on(this._element,
+ Event.CLICK_DISMISS,
+ Selector.DATA_DISMISS,
+ event => this.hide(event)
+ )
+
+ EventHandler.on(this._dialog, Event.MOUSEDOWN_DISMISS, () => {
+ EventHandler.one(this._element, Event.MOUSEUP_DISMISS, event => {
+ if (event.target === this._element) {
+ this._ignoreBackdropClick = true
+ }
+ })
+ })
+
+ this._showBackdrop(() => this._showElement(relatedTarget))
+ }
+
+ hide(event) {
+ if (event) {
+ event.preventDefault()
+ }
+
+ if (!this._isShown || this._isTransitioning) {
+ return
+ }
+
+ const hideEvent = EventHandler.trigger(this._element, Event.HIDE)
+
+ if (hideEvent.defaultPrevented) {
+ return
+ }
+
+ this._isShown = false
+ const transition = this._element.classList.contains(ClassName.FADE)
+
+ if (transition) {
+ this._isTransitioning = true
+ }
+
+ this._setEscapeEvent()
+ this._setResizeEvent()
+
+ EventHandler.off(document, Event.FOCUSIN)
+
+ this._element.classList.remove(ClassName.SHOW)
+
+ EventHandler.off(this._element, Event.CLICK_DISMISS)
+ EventHandler.off(this._dialog, Event.MOUSEDOWN_DISMISS)
+
+ if (transition) {
+ const transitionDuration = getTransitionDurationFromElement(this._element)
+
+ EventHandler.one(this._element, TRANSITION_END, event => this._hideModal(event))
+ emulateTransitionEnd(this._element, transitionDuration)
+ } else {
+ this._hideModal()
+ }
+ }
+
+ dispose() {
+ [window, this._element, this._dialog]
+ .forEach(htmlElement => EventHandler.off(htmlElement, EVENT_KEY))
+
+ /**
+ * `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API`
+ * Do not move `document` in `htmlElements` array
+ * It will remove `Event.CLICK_DATA_API` event that should remain
+ */
+ EventHandler.off(document, Event.FOCUSIN)
+
+ Data.removeData(this._element, DATA_KEY)
+
+ this._config = null
+ this._element = null
+ this._dialog = null
+ this._backdrop = null
+ this._isShown = null
+ this._isBodyOverflowing = null
+ this._ignoreBackdropClick = null
+ this._isTransitioning = null
+ this._scrollbarWidth = null
+ }
+
+ handleUpdate() {
+ this._adjustDialog()
+ }
+
+ // Private
+
+ _getConfig(config) {
+ config = {
+ ...Default,
+ ...config
+ }
+ typeCheckConfig(NAME, config, DefaultType)
+ return config
+ }
+
+ _showElement(relatedTarget) {
+ const transition = this._element.classList.contains(ClassName.FADE)
+
+ if (!this._element.parentNode ||
+ this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {
+ // Don't move modal's DOM position
+ document.body.appendChild(this._element)
+ }
+
+ this._element.style.display = 'block'
+ this._element.removeAttribute('aria-hidden')
+ this._element.setAttribute('aria-modal', true)
+
+ if (this._dialog.classList.contains(ClassName.SCROLLABLE)) {
+ SelectorEngine.findOne(Selector.MODAL_BODY, this._dialog).scrollTop = 0
+ } else {
+ this._element.scrollTop = 0
+ }
+
+ if (transition) {
+ reflow(this._element)
+ }
+
+ this._element.classList.add(ClassName.SHOW)
+
+ if (this._config.focus) {
+ this._enforceFocus()
+ }
+
+ const transitionComplete = () => {
+ if (this._config.focus) {
+ this._element.focus()
+ }
+
+ this._isTransitioning = false
+ EventHandler.trigger(this._element, Event.SHOWN, {
+ relatedTarget
+ })
+ }
+
+ if (transition) {
+ const transitionDuration = getTransitionDurationFromElement(this._dialog)
+
+ EventHandler.one(this._dialog, TRANSITION_END, transitionComplete)
+ emulateTransitionEnd(this._dialog, transitionDuration)
+ } else {
+ transitionComplete()
+ }
+ }
+
+ _enforceFocus() {
+ EventHandler.off(document, Event.FOCUSIN) // guard against infinite focus loop
+ EventHandler.on(document, Event.FOCUSIN, event => {
+ if (document !== event.target &&
+ this._element !== event.target &&
+ !this._element.contains(event.target)) {
+ this._element.focus()
+ }
+ })
+ }
+
+ _setEscapeEvent() {
+ if (this._isShown && this._config.keyboard) {
+ EventHandler.on(this._element, Event.KEYDOWN_DISMISS, event => {
+ if (event.which === ESCAPE_KEYCODE) {
+ event.preventDefault()
+ this.hide()
+ }
+ })
+ } else {
+ EventHandler.off(this._element, Event.KEYDOWN_DISMISS)
+ }
+ }
+
+ _setResizeEvent() {
+ if (this._isShown) {
+ EventHandler.on(window, Event.RESIZE, () => this._adjustDialog())
+ } else {
+ EventHandler.off(window, Event.RESIZE)
+ }
+ }
+
+ _hideModal() {
+ this._element.style.display = 'none'
+ this._element.setAttribute('aria-hidden', true)
+ this._element.removeAttribute('aria-modal')
+ this._isTransitioning = false
+ this._showBackdrop(() => {
+ document.body.classList.remove(ClassName.OPEN)
+ this._resetAdjustments()
+ this._resetScrollbar()
+ EventHandler.trigger(this._element, Event.HIDDEN)
+ })
+ }
+
+ _removeBackdrop() {
+ this._backdrop.parentNode.removeChild(this._backdrop)
+ this._backdrop = null
+ }
+
+ _showBackdrop(callback) {
+ const animate = this._element.classList.contains(ClassName.FADE) ?
+ ClassName.FADE :
+ ''
+
+ if (this._isShown && this._config.backdrop) {
+ this._backdrop = document.createElement('div')
+ this._backdrop.className = ClassName.BACKDROP
+
+ if (animate) {
+ this._backdrop.classList.add(animate)
+ }
+
+ document.body.appendChild(this._backdrop)
+
+ EventHandler.on(this._element, Event.CLICK_DISMISS, event => {
+ if (this._ignoreBackdropClick) {
+ this._ignoreBackdropClick = false
+ return
+ }
+
+ if (event.target !== event.currentTarget) {
+ return
+ }
+
+ if (this._config.backdrop === 'static') {
+ this._element.focus()
+ } else {
+ this.hide()
+ }
+ })
+
+ if (animate) {
+ reflow(this._backdrop)
+ }
+
+ this._backdrop.classList.add(ClassName.SHOW)
+
+ if (!animate) {
+ callback()
+ return
+ }
+
+ const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop)
+
+ EventHandler.one(this._backdrop, TRANSITION_END, callback)
+ emulateTransitionEnd(this._backdrop, backdropTransitionDuration)
+ } else if (!this._isShown && this._backdrop) {
+ this._backdrop.classList.remove(ClassName.SHOW)
+
+ const callbackRemove = () => {
+ this._removeBackdrop()
+ callback()
+ }
+
+ if (this._element.classList.contains(ClassName.FADE)) {
+ const backdropTransitionDuration = getTransitionDurationFromElement(this._backdrop)
+ EventHandler.one(this._backdrop, TRANSITION_END, callbackRemove)
+ emulateTransitionEnd(this._backdrop, backdropTransitionDuration)
+ } else {
+ callbackRemove()
+ }
+ } else {
+ callback()
+ }
+ }
+
+ // ----------------------------------------------------------------------
+ // the following methods are used to handle overflowing modals
+ // ----------------------------------------------------------------------
+
+ _adjustDialog() {
+ const isModalOverflowing =
+ this._element.scrollHeight > document.documentElement.clientHeight
+
+ if (!this._isBodyOverflowing && isModalOverflowing) {
+ this._element.style.paddingLeft = `${this._scrollbarWidth}px`
+ }
+
+ if (this._isBodyOverflowing && !isModalOverflowing) {
+ this._element.style.paddingRight = `${this._scrollbarWidth}px`
+ }
+ }
+
+ _resetAdjustments() {
+ this._element.style.paddingLeft = ''
+ this._element.style.paddingRight = ''
+ }
+
+ _checkScrollbar() {
+ const rect = document.body.getBoundingClientRect()
+ this._isBodyOverflowing = rect.left + rect.right < window.innerWidth
+ this._scrollbarWidth = this._getScrollbarWidth()
+ }
+
+ _setScrollbar() {
+ if (this._isBodyOverflowing) {
+ // Note: DOMNode.style.paddingRight returns the actual value or '' if not set
+ // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set
+
+ // Adjust fixed content padding
+ makeArray(SelectorEngine.find(Selector.FIXED_CONTENT))
+ .forEach(element => {
+ const actualPadding = element.style.paddingRight
+ const calculatedPadding = window.getComputedStyle(element)['padding-right']
+ Manipulator.setDataAttribute(element, 'padding-right', actualPadding)
+ element.style.paddingRight = `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`
+ })
+
+ // Adjust sticky content margin
+ makeArray(SelectorEngine.find(Selector.STICKY_CONTENT))
+ .forEach(element => {
+ const actualMargin = element.style.marginRight
+ const calculatedMargin = window.getComputedStyle(element)['margin-right']
+ Manipulator.setDataAttribute(element, 'margin-right', actualMargin)
+ element.style.marginRight = `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`
+ })
+
+ // Adjust body padding
+ const actualPadding = document.body.style.paddingRight
+ const calculatedPadding = window.getComputedStyle(document.body)['padding-right']
+
+ Manipulator.setDataAttribute(document.body, 'padding-right', actualPadding)
+ document.body.style.paddingRight = `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`
+ }
+
+ document.body.classList.add(ClassName.OPEN)
+ }
+
+ _resetScrollbar() {
+ // Restore fixed content padding
+ makeArray(SelectorEngine.find(Selector.FIXED_CONTENT))
+ .forEach(element => {
+ const padding = Manipulator.getDataAttribute(element, 'padding-right')
+ if (typeof padding !== 'undefined') {
+ Manipulator.removeDataAttribute(element, 'padding-right')
+ element.style.paddingRight = padding
+ }
+ })
+
+ // Restore sticky content and navbar-toggler margin
+ makeArray(SelectorEngine.find(`${Selector.STICKY_CONTENT}`))
+ .forEach(element => {
+ const margin = Manipulator.getDataAttribute(element, 'margin-right')
+ if (typeof margin !== 'undefined') {
+ Manipulator.removeDataAttribute(element, 'margin-right')
+ element.style.marginRight = margin
+ }
+ })
+
+ // Restore body padding
+ const padding = Manipulator.getDataAttribute(document.body, 'padding-right')
+ if (typeof padding === 'undefined') {
+ document.body.style.paddingRight = ''
+ } else {
+ Manipulator.removeDataAttribute(document.body, 'padding-right')
+ document.body.style.paddingRight = padding
+ }
+ }
+
+ _getScrollbarWidth() { // thx d.walsh
+ const scrollDiv = document.createElement('div')
+ scrollDiv.className = ClassName.SCROLLBAR_MEASURER
+ document.body.appendChild(scrollDiv)
+ const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth
+ document.body.removeChild(scrollDiv)
+ return scrollbarWidth
+ }
+
+ // Static
+
+ static _jQueryInterface(config, relatedTarget) {
+ return this.each(function () {
+ let data = Data.getData(this, DATA_KEY)
+ const _config = {
+ ...Default,
+ ...Manipulator.getDataAttributes(this),
+ ...typeof config === 'object' && config ? config : {}
+ }
+
+ if (!data) {
+ data = new Modal(this, _config)
+ }
+
+ if (typeof config === 'string') {
+ if (typeof data[config] === 'undefined') {
+ throw new TypeError(`No method named "${config}"`)
+ }
+
+ data[config](relatedTarget)
+ } else if (_config.show) {
+ data.show(relatedTarget)
+ }
+ })
+ }
+
+ static _getInstance(element) {
+ return Data.getData(element, DATA_KEY)
+ }
+}
+
+/**
+ * ------------------------------------------------------------------------
+ * Data Api implementation
+ * ------------------------------------------------------------------------
+ */
+
+EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
+ const selector = getSelectorFromElement(this)
+ const target = SelectorEngine.findOne(selector)
+
+ if (this.tagName === 'A' || this.tagName === 'AREA') {
+ event.preventDefault()
+ }
+
+ EventHandler.one(target, Event.SHOW, showEvent => {
+ if (showEvent.defaultPrevented) {
+ // only register focus restorer if modal will actually get shown
+ return
+ }
+
+ EventHandler.one(target, Event.HIDDEN, () => {
+ if (isVisible(this)) {
+ this.focus()
+ }
+ })
+ })
+
+ let data = Data.getData(target, DATA_KEY)
+ if (!data) {
+ const config = {
+ ...Manipulator.getDataAttributes(target),
+ ...Manipulator.getDataAttributes(this)
+ }
+
+ data = new Modal(target, config)
+ }
+
+ data.show(this)
+})
+
+/**
+ * ------------------------------------------------------------------------
+ * 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
+ $.fn[NAME].Constructor = Modal
+ $.fn[NAME].noConflict = () => {
+ $.fn[NAME] = JQUERY_NO_CONFLICT
+ return Modal._jQueryInterface
+ }
+}
+
+export default Modal
diff --git a/js/src/modal/modal.spec.js b/js/src/modal/modal.spec.js
new file mode 100644
index 000000000..f1311c591
--- /dev/null
+++ b/js/src/modal/modal.spec.js
@@ -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)
+ })
+ })
+})