bootstrap-vue
Version:
BootstrapVue, with over 40 plugins and more than 80 custom components, custom directives, and over 300 icons, provides one of the most comprehensive implementations of Bootstrap v4 components and grid system for Vue.js. With extensive and automated WAI-AR
1,396 lines (1,138 loc) • 41.3 kB
JavaScript
import { mount, createLocalVue as CreateLocalVue } from '@vue/test-utils'
import { waitNT, waitRAF } from '../../../tests/utils'
import { BModal } from './modal'
import { BvModalEvent } from './helpers/bv-modal-event.class'
// The default Z-INDEX for modal backdrop
const DEFAULT_ZINDEX = 1040
describe('modal', () => {
const origGetBCR = Element.prototype.getBoundingClientRect
beforeEach(() => {
// Mock `getBCR()` so that the `isVisible(el)` test returns `true`
// Needed for z-index checks
Element.prototype.getBoundingClientRect = jest.fn(() => ({
width: 24,
height: 24,
top: 0,
left: 0,
bottom: 0,
right: 0
}))
})
afterEach(() => {
// Restore prototype
Element.prototype.getBoundingClientRect = origGetBCR
})
describe('structure', () => {
it('has expected default structure', async () => {
const wrapper = mount(BModal, {
attachToDocument: true,
propsData: {
static: true,
id: 'test'
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
// Main outer wrapper (has z-index, etc)... The stacker <div>
expect(wrapper.is('div')).toBe(true)
expect(wrapper.classes().length).toBe(0)
expect(wrapper.element.style.position).toEqual('absolute')
expect(wrapper.element.style.zIndex).toEqual(`${DEFAULT_ZINDEX}`)
// Should not have a backdrop
expect(wrapper.find('div.modal-backdrop').exists()).toBe(false)
// Main modal wrapper
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
expect($modal.attributes('id')).toBeDefined()
expect($modal.attributes('id')).toEqual('test')
expect($modal.attributes('role')).toBeDefined()
expect($modal.attributes('role')).toEqual('dialog')
expect($modal.attributes('aria-hidden')).toBeDefined()
expect($modal.attributes('aria-hidden')).toEqual('true')
expect($modal.classes()).toContain('modal')
expect($modal.element.style.display).toEqual('none')
// Modal dialog wrapper
const $dialog = $modal.find('div.modal-dialog')
expect($dialog.exists()).toBe(true)
// Modal content wrapper
const $content = $dialog.find('div.modal-content')
expect($content.exists()).toBe(true)
expect($content.attributes('tabindex')).toBeDefined()
expect($content.attributes('tabindex')).toEqual('-1')
expect($content.attributes('role')).toBeDefined()
expect($content.attributes('role')).toEqual('document')
wrapper.destroy()
})
it('has expected default structure when static and lazy', async () => {
const wrapper = mount(BModal, {
attachToDocument: true,
propsData: {
static: true,
lazy: true
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
expect(wrapper.isEmpty()).toBe(true)
expect(wrapper.element.nodeType).toEqual(Node.COMMENT_NODE)
wrapper.destroy()
})
it('has expected default structure when not static', async () => {
const wrapper = mount(BModal, {
attachToDocument: true,
propsData: {
static: false
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
expect(wrapper.isEmpty()).toBe(true)
expect(wrapper.element.nodeType).toEqual(Node.COMMENT_NODE)
wrapper.destroy()
})
it('has expected structure when initially open', async () => {
const wrapper = mount(BModal, {
attachToDocument: true,
stubs: {
// Disable the use of transitionStub fake transition
// as it doesn't run transition hooks
transition: false
},
propsData: {
static: true,
id: 'test',
visible: true
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitRAF()
// Main outer wrapper (has z-index, etc)... The stacker <div>
expect(wrapper.is('div')).toBe(true)
expect(wrapper.classes().length).toBe(0)
expect(wrapper.element.style.position).toEqual('absolute')
expect(wrapper.element.style.zIndex).toEqual(`${DEFAULT_ZINDEX}`)
// Main modal wrapper
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
expect($modal.attributes('id')).toBeDefined()
expect($modal.attributes('id')).toEqual('test')
expect($modal.attributes('role')).toBeDefined()
expect($modal.attributes('role')).toEqual('dialog')
expect($modal.attributes('aria-hidden')).not.toBeDefined()
expect($modal.attributes('aria-modal')).toBeDefined()
expect($modal.attributes('aria-modal')).toEqual('true')
expect($modal.classes()).toContain('modal')
expect($modal.element.style.display).toEqual('block')
// Should have a backdrop
const $backdrop = wrapper.find('div.modal-backdrop')
expect($backdrop.exists()).toBe(true)
// Modal dialog wrapper
const $dialog = $modal.find('div.modal-dialog')
expect($dialog.exists()).toBe(true)
// Modal content wrapper
const $content = $dialog.find('div.modal-content')
expect($content.exists()).toBe(true)
expect($content.attributes('tabindex')).toBeDefined()
expect($content.attributes('tabindex')).toEqual('-1')
expect($content.attributes('role')).toBeDefined()
expect($content.attributes('role')).toEqual('document')
wrapper.destroy()
})
it('renders appended to body when initially open and not static', async () => {
const wrapper = mount(BModal, {
attachToDocument: true,
stubs: {
// Disable the use of transitionStub fake transition
// as it doesn't run transition hooks
transition: false
},
propsData: {
static: false,
id: 'test-target',
visible: true
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitRAF()
expect(wrapper.isEmpty()).toBe(true)
expect(wrapper.element.nodeType).toEqual(Node.COMMENT_NODE)
const outer = document.getElementById('test-target___BV_modal_outer_')
expect(outer).toBeDefined()
expect(outer).not.toBe(null)
expect(outer.__vue__).toBeDefined() // Target
expect(outer.__vue__.$options.name).toBe('BTransporterTargetSingle')
expect(outer.parentElement).toBeDefined()
expect(outer.parentElement).toBe(document.body)
// Destroy modal
wrapper.destroy()
await waitNT(wrapper.vm)
await waitRAF()
// Should no longer be in document
expect(outer.parentElement).toEqual(null)
})
it('has expected structure when closed after being initially open', async () => {
const wrapper = mount(BModal, {
attachToDocument: true,
stubs: {
// Disable the use of transitionStub fake transition
// as it doesn't run transition hooks
transition: false
},
propsData: {
static: true,
id: 'test',
visible: true
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()
// Main outer wrapper (has z-index, etc)... The stacker <div>
expect(wrapper.is('div')).toBe(true)
expect(wrapper.classes().length).toBe(0)
expect(wrapper.element.style.position).toEqual('absolute')
expect(wrapper.element.style.zIndex).toEqual(`${DEFAULT_ZINDEX}`)
// Main modal wrapper
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
expect($modal.attributes('aria-hidden')).not.toBeDefined()
expect($modal.attributes('aria-modal')).toBeDefined()
expect($modal.attributes('aria-modal')).toEqual('true')
expect($modal.element.style.display).toEqual('block')
// Should have a backdrop
const $backdrop = wrapper.find('div.modal-backdrop')
expect($backdrop.exists()).toBe(true)
// Now we close the modal via the value prop
wrapper.setProps({
visible: false
})
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
expect($modal.attributes('aria-hidden')).toBeDefined()
expect($modal.attributes('aria-hidden')).toEqual('true')
expect($modal.attributes('aria-modal')).not.toBeDefined()
expect($modal.element.style.display).toEqual('none')
// Backdrop should be removed
expect(wrapper.find('div.modal-backdrop').exists()).toBe(false)
wrapper.destroy()
})
it('title-html prop works', async () => {
const wrapper = mount(BModal, {
attachToDocument: true,
propsData: {
static: true,
id: 'test',
titleHtml: '<em>title</em>'
}
})
expect(wrapper.isVueInstance()).toBe(true)
// Modal title
const $title = wrapper.find('.modal-title')
expect($title.exists()).toBe(true)
expect($title.html()).toContain('<em>title</em>')
wrapper.destroy()
})
})
describe('default button content, classes and attributes', () => {
// We may want to move these tests into individual files for manageability
it('default footer ok and cancel buttons', async () => {
const wrapper = mount(BModal, {
attachToDocument: true,
propsData: {
static: true
}
})
expect(wrapper).toBeDefined()
const $buttons = wrapper.findAll('footer button')
expect($buttons.length).toBe(2)
// Cancel button (left-most button)
const $cancel = $buttons.at(0)
expect($cancel.attributes('type')).toBe('button')
expect($cancel.classes()).toContain('btn')
expect($cancel.classes()).toContain('btn-secondary')
expect($cancel.text()).toContain('Cancel')
// OK button (right-most button)
const $ok = $buttons.at(1)
expect($ok.attributes('type')).toBe('button')
expect($ok.classes()).toContain('btn')
expect($ok.classes()).toContain('btn-primary')
expect($ok.text()).toContain('OK')
wrapper.destroy()
})
it('default header close button', async () => {
const wrapper = mount(BModal, {
attachToDocument: true,
propsData: {
static: true
}
})
expect(wrapper).toBeDefined()
const $buttons = wrapper.findAll('header button')
expect($buttons.length).toBe(1)
// Close button
const $close = $buttons.at(0)
expect($close.attributes('type')).toBe('button')
expect($close.attributes('aria-label')).toBe('Close')
expect($close.classes()).toContain('close')
wrapper.destroy()
})
it('ok-title-html and cancel-title-html works', async () => {
const wrapper = mount(BModal, {
attachToDocument: true,
propsData: {
static: true,
okTitleHtml: '<em>ok</em>',
cancelTitleHtml: '<em>cancel</em>'
}
})
expect(wrapper).toBeDefined()
const $buttons = wrapper.findAll('footer button')
expect($buttons.length).toBe(2)
// Cancel button (left-most button)
const $cancel = $buttons.at(0)
expect($cancel.attributes('type')).toBe('button')
expect($cancel.text()).toContain('cancel')
// `v-html` is applied to a span
expect($cancel.html()).toContain('<span><em>cancel</em></span>')
// OK button (right-most button)
const $ok = $buttons.at(1)
expect($ok.attributes('type')).toBe('button')
expect($ok.text()).toContain('ok')
// `v-html` is applied to a span
expect($ok.html()).toContain('<span><em>ok</em></span>')
wrapper.destroy()
})
})
describe('button and event functionality', () => {
it('header close button triggers modal close and is preventable', async () => {
let cancelHide = true
let trigger = null
let evt = null
const wrapper = mount(BModal, {
attachToDocument: true,
stubs: {
transition: false
},
propsData: {
static: true,
id: 'test',
visible: true
},
listeners: {
hide: bvEvent => {
if (cancelHide) {
bvEvent.preventDefault()
}
trigger = bvEvent.trigger
evt = bvEvent
}
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
expect($modal.element.style.display).toEqual('block')
const $buttons = wrapper.findAll('header button')
expect($buttons.length).toBe(1)
// Close button
const $close = $buttons.at(0)
expect($close.attributes('type')).toBe('button')
expect($close.attributes('aria-label')).toBe('Close')
expect($close.classes()).toContain('close')
expect(wrapper.emitted('hide')).not.toBeDefined()
expect(trigger).toEqual(null)
expect(evt).toEqual(null)
// Try and close modal (but we prevent it)
$close.trigger('click')
await waitNT(wrapper.vm)
expect(trigger).toEqual('headerclose')
expect(evt).toBeInstanceOf(BvModalEvent)
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should still be open
expect($modal.element.style.display).toEqual('block')
// Try and close modal (and not prevent it)
cancelHide = false
trigger = null
evt = null
$close.trigger('click')
await waitNT(wrapper.vm)
expect(trigger).toEqual('headerclose')
expect(evt).toBeInstanceOf(BvModalEvent)
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should now be closed
expect($modal.element.style.display).toEqual('none')
wrapper.destroy()
})
it('footer OK and CANCEL buttons trigger modal close and are preventable', async () => {
let cancelHide = true
let trigger = null
const wrapper = mount(BModal, {
attachToDocument: true,
stubs: {
transition: false
},
propsData: {
static: true,
id: 'test',
visible: true
},
listeners: {
hide: bvEvent => {
if (cancelHide) {
bvEvent.preventDefault()
}
trigger = bvEvent.trigger
}
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
expect($modal.element.style.display).toEqual('block')
const $buttons = wrapper.findAll('footer button')
expect($buttons.length).toBe(2)
// Cancel button (left-most button)
const $cancel = $buttons.at(0)
expect($cancel.text()).toContain('Cancel')
// OK button (right-most button)
const $ok = $buttons.at(1)
expect($ok.text()).toContain('OK')
expect(wrapper.emitted('hide')).not.toBeDefined()
expect(trigger).toEqual(null)
// Try and close modal (but we prevent it)
$ok.trigger('click')
await waitNT(wrapper.vm)
expect(trigger).toEqual('ok')
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should still be open
expect($modal.element.style.display).toEqual('block')
// Try and close modal (and not prevent it)
cancelHide = false
trigger = null
$cancel.trigger('click')
await waitNT(wrapper.vm)
expect(trigger).toEqual('cancel')
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should now be closed
expect($modal.element.style.display).toEqual('none')
// Modal should have emitted these events
expect(wrapper.emitted('ok')).toBeDefined()
expect(wrapper.emitted('ok').length).toBe(1)
expect(wrapper.emitted('cancel')).toBeDefined()
expect(wrapper.emitted('cancel').length).toBe(1)
expect(wrapper.emitted('hidden')).toBeDefined()
expect(wrapper.emitted('hidden').length).toBe(1)
wrapper.destroy()
})
it('pressing ESC closes modal', async () => {
let trigger = null
const wrapper = mount(BModal, {
attachToDocument: true,
stubs: {
transition: false
},
propsData: {
static: true,
id: 'test',
visible: true
},
listeners: {
hide: bvEvent => {
trigger = bvEvent.trigger
}
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
expect($modal.element.style.display).toEqual('block')
expect(wrapper.emitted('hide')).not.toBeDefined()
expect(trigger).toEqual(null)
// Try and close modal via ESC
$modal.trigger('keydown.esc')
await waitNT(wrapper.vm)
expect(trigger).toEqual('esc')
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should now be closed
expect($modal.element.style.display).toEqual('none')
// Modal should have emitted these events
expect(wrapper.emitted('hide')).toBeDefined()
expect(wrapper.emitted('hide').length).toBe(1)
expect(wrapper.emitted('hidden')).toBeDefined()
expect(wrapper.emitted('hidden').length).toBe(1)
expect(wrapper.emitted('ok')).not.toBeDefined()
expect(wrapper.emitted('cancel')).not.toBeDefined()
wrapper.destroy()
})
it('click outside closes modal', async () => {
let trigger = null
const wrapper = mount(BModal, {
attachToDocument: true,
stubs: {
transition: false
},
propsData: {
static: true,
id: 'test',
visible: true
},
listeners: {
hide: bvEvent => {
trigger = bvEvent.trigger
}
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
expect($modal.element.style.display).toEqual('block')
expect(wrapper.emitted('hide')).not.toBeDefined()
expect(trigger).toEqual(null)
// Try and close modal via click out
$modal.trigger('click')
await waitNT(wrapper.vm)
expect(trigger).toEqual('backdrop')
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should now be closed
expect($modal.element.style.display).toEqual('none')
// Modal should have emitted these events
expect(wrapper.emitted('hide')).toBeDefined()
expect(wrapper.emitted('hide').length).toBe(1)
expect(wrapper.emitted('hidden')).toBeDefined()
expect(wrapper.emitted('hidden').length).toBe(1)
expect(wrapper.emitted('ok')).not.toBeDefined()
expect(wrapper.emitted('cancel')).not.toBeDefined()
wrapper.destroy()
})
it('mousedown inside followed by mouse up outside (click) does not close modal', async () => {
let trigger = null
let called = false
const wrapper = mount(BModal, {
attachToDocument: true,
stubs: {
transition: false
},
propsData: {
static: true,
id: 'test',
visible: true
},
listeners: {
hide: bvEvent => {
called = true
trigger = bvEvent.trigger
}
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
const $dialog = wrapper.find('div.modal-dialog')
expect($dialog.exists()).toBe(true)
const $footer = wrapper.find('footer.modal-footer')
expect($footer.exists()).toBe(true)
expect($modal.element.style.display).toEqual('block')
expect(wrapper.emitted('hide')).not.toBeDefined()
expect(trigger).toEqual(null)
// Try and close modal via a "dragged" click out
// starting from inside modal and finishing on backdrop
$dialog.trigger('mousedown')
$modal.trigger('mouseup')
$modal.trigger('click')
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
expect(called).toEqual(false)
expect(trigger).toEqual(null)
// Modal should not be closed
expect($modal.element.style.display).toEqual('block')
// Try and close modal via a "dragged" click out
// starting from inside modal and finishing on backdrop
$footer.trigger('mousedown')
$modal.trigger('mouseup')
$modal.trigger('click')
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
expect(called).toEqual(false)
expect(trigger).toEqual(null)
// Modal should not be closed
expect($modal.element.style.display).toEqual('block')
// Try and close modal via click out
$modal.trigger('click')
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
expect(called).toEqual(true)
expect(trigger).toEqual('backdrop')
// Modal should now be closed
expect($modal.element.style.display).toEqual('none')
wrapper.destroy()
})
it('$root bv::show::modal and bv::hide::modal work', async () => {
const wrapper = mount(BModal, {
attachToDocument: true,
stubs: {
transition: false
},
propsData: {
static: true,
id: 'test',
visible: false
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
expect($modal.element.style.display).toEqual('none')
// Try and open modal via `bv::show::modal`
wrapper.vm.$root.$emit('bv::show::modal', 'test')
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should now be open
expect($modal.element.style.display).toEqual('block')
// Try and close modal via `bv::hide::modal`
wrapper.vm.$root.$emit('bv::hide::modal', 'test')
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should now be closed
expect($modal.element.style.display).toEqual('none')
wrapper.destroy()
})
it('$root bv::toggle::modal works', async () => {
const wrapper = mount(BModal, {
attachToDocument: true,
stubs: {
transition: false
},
propsData: {
static: true,
id: 'test',
visible: false
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
expect($modal.element.style.display).toEqual('none')
// Try and open modal via `bv::toggle::modal`
wrapper.vm.$root.$emit('bv::toggle::modal', 'test')
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should now be open
expect($modal.element.style.display).toEqual('block')
// Try and close modal via `bv::toggle::modal`
wrapper.vm.$root.$emit('bv::toggle::modal', 'test')
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should now be closed
expect($modal.element.style.display).toEqual('none')
// Try and open modal via `bv::toggle::modal` with wrong ID
wrapper.vm.$root.$emit('bv::toggle::modal', 'not-test')
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should not be open
expect($modal.element.style.display).toEqual('none')
wrapper.destroy()
})
it('show event is cancellable', async () => {
let prevent = true
let called = 0
const wrapper = mount(BModal, {
attachToDocument: true,
stubs: {
transition: false
},
propsData: {
static: true,
id: 'test',
visible: false
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
expect($modal.element.style.display).toEqual('none')
wrapper.vm.$on('show', bvEvt => {
called = true
if (prevent) {
bvEvt.preventDefault()
}
})
// Try and open modal via `bv::show::modal`
wrapper.vm.$root.$emit('bv::show::modal', 'test')
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should not open
expect(called).toBe(true)
expect($modal.element.style.display).toEqual('none')
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Allow modal to open
prevent = false
called = false
// Try and open modal via `bv::show::modal`
wrapper.vm.$root.$emit('bv::show::modal', 'test')
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should now be open
expect(called).toBe(true)
expect($modal.element.style.display).toEqual('block')
wrapper.destroy()
})
it('instance .toggle() methods works', async () => {
const wrapper = mount(BModal, {
attachToDocument: true,
stubs: {
transition: false
},
propsData: {
static: true,
id: 'test',
visible: false
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
expect($modal.element.style.display).toEqual('none')
// Try and open modal via `.toggle()` method
wrapper.vm.toggle()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should now be open
expect($modal.element.style.display).toEqual('block')
// Try and close modal via `.toggle()` method
wrapper.vm.toggle()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should now be closed
expect($modal.element.style.display).toEqual('none')
wrapper.destroy()
})
})
describe('focus management', () => {
const localVue = new CreateLocalVue()
it('returns focus to previous active element when return focus not set and not using v-b-toggle', async () => {
const App = localVue.extend({
render(h) {
return h('div', {}, [
h('button', { class: 'trigger', attrs: { id: 'trigger', type: 'button' } }, 'trigger'),
h(BModal, { props: { static: true, id: 'test', visible: false } }, 'modal content')
])
}
})
const wrapper = mount(App, {
attachToDocument: true,
localVue: localVue,
stubs: {
transition: false
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitNT(wrapper.vm)
const $button = wrapper.find('button.trigger')
expect($button.exists()).toBe(true)
expect($button.is('button')).toBe(true)
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
expect($modal.element.style.display).toEqual('none')
expect(document.activeElement).toBe(document.body)
// Set the active element to the button
$button.element.focus()
expect(document.activeElement).toBe($button.element)
// Try and open modal via `.toggle()` method
wrapper.find(BModal).vm.toggle()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should now be open
expect($modal.element.style.display).toEqual('block')
expect(document.activeElement).not.toBe(document.body)
expect(document.activeElement).not.toBe($button.element)
expect($modal.element.contains(document.activeElement)).toBe(true)
// Try and close modal via `.toggle()` method
wrapper.find(BModal).vm.toggle()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should now be closed
expect($modal.element.style.display).toEqual('none')
expect(document.activeElement).toBe($button.element)
wrapper.destroy()
})
it('returns focus to element specified in toggle() method', async () => {
const App = localVue.extend({
render(h) {
return h('div', {}, [
h('button', { class: 'trigger', attrs: { id: 'trigger', type: 'button' } }, 'trigger'),
h(
'button',
{ class: 'return-to', attrs: { id: 'return-to', type: 'button' } },
'trigger'
),
h(BModal, { props: { static: true, id: 'test', visible: false } }, 'modal content')
])
}
})
const wrapper = mount(App, {
attachToDocument: true,
localVue: localVue,
stubs: {
transition: false
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
const $button = wrapper.find('button.trigger')
expect($button.exists()).toBe(true)
expect($button.is('button')).toBe(true)
const $button2 = wrapper.find('button.return-to')
expect($button2.exists()).toBe(true)
expect($button2.is('button')).toBe(true)
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
expect($modal.element.style.display).toEqual('none')
expect(document.activeElement).toBe(document.body)
// Set the active element to the button
$button.element.focus()
expect(document.activeElement).toBe($button.element)
// Try and open modal via `.toggle()` method
wrapper.find(BModal).vm.toggle('button.return-to')
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should now be open
expect($modal.element.style.display).toEqual('block')
expect(document.activeElement).not.toBe(document.body)
expect(document.activeElement).not.toBe($button.element)
expect(document.activeElement).not.toBe($button2.element)
expect($modal.element.contains(document.activeElement)).toBe(true)
// Try and close modal via `.toggle()` method
wrapper.find(BModal).vm.toggle()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
// Modal should now be closed
expect($modal.element.style.display).toEqual('none')
expect(document.activeElement).toBe($button2.element)
wrapper.destroy()
})
it('if focus leaves modal it returns to modal', async () => {
const App = localVue.extend({
render(h) {
return h('div', {}, [
h('button', { attrs: { id: 'button', type: 'button' } }, 'Button'),
h(BModal, { props: { static: true, id: 'test', visible: true } }, 'Modal content')
])
}
})
const wrapper = mount(App, {
attachToDocument: true,
localVue: localVue,
stubs: {
transition: false
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
const $button = wrapper.find('#button')
expect($button.exists()).toBe(true)
expect($button.is('button')).toBe(true)
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
const $content = $modal.find('div.modal-content')
expect($content.exists()).toBe(true)
expect($modal.element.style.display).toEqual('block')
expect(document.activeElement).not.toBe(document.body)
expect(document.activeElement).toBe($content.element)
// Try and focus the external button
$button.element.focus()
$button.trigger('focusin')
expect(document.activeElement).not.toBe($button.element)
expect(document.activeElement).toBe($content.element)
// Emulate TAB by focusing the `bottomTrap` span element
// Should focus first button in modal (in the header)
const $bottomTrap = wrapper.find(BModal).find({ ref: 'bottomTrap' })
expect($bottomTrap.exists()).toBe(true)
expect($bottomTrap.is('span')).toBe(true)
// Find the close (x) button (it is the only one with the `.close` class)
const $closeButton = $modal.find('button.close')
expect($closeButton.exists()).toBe(true)
expect($closeButton.is('button')).toBe(true)
// Focus the tab trap
$bottomTrap.element.focus()
$bottomTrap.trigger('focusin')
await waitNT(wrapper.vm)
expect(document.activeElement).not.toBe($bottomTrap.element)
expect(document.activeElement).not.toBe($content.element)
// The close (x) button (first tabable in modal) should be focused
expect(document.activeElement).toBe($closeButton.element)
// Emulate CTRL-TAB by focusing the `topTrap` div element
// Should focus last button in modal (in the footer)
const $topTrap = wrapper.find(BModal).find({ ref: 'topTrap' })
expect($topTrap.exists()).toBe(true)
expect($topTrap.is('span')).toBe(true)
// Find the OK button (it is the only one with `.btn-primary` class)
const $okButton = $modal.find('button.btn.btn-primary')
expect($okButton.exists()).toBe(true)
expect($okButton.is('button')).toBe(true)
// Focus the tab trap
$topTrap.element.focus()
$topTrap.trigger('focusin')
await waitNT(wrapper.vm)
expect(document.activeElement).not.toBe($topTrap.element)
expect(document.activeElement).not.toBe($bottomTrap.element)
expect(document.activeElement).not.toBe($content.element)
// The OK button (last tabbable in modal) should be focused
expect(document.activeElement).toBe($okButton.element)
wrapper.destroy()
})
it('it allows focus for elements when "no-enforce-focus" enabled', async () => {
const App = localVue.extend({
render(h) {
return h('div', {}, [
h('button', { attrs: { id: 'button1', type: 'button' } }, 'Button 1'),
h('button', { attrs: { id: 'button2', type: 'button' } }, 'Button 2'),
h(
BModal,
{
props: {
static: true,
id: 'test',
visible: true,
noEnforceFocus: true
}
},
'Modal content'
)
])
}
})
const wrapper = mount(App, {
attachToDocument: true,
localVue: localVue,
stubs: {
transition: false
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
const $button1 = wrapper.find('#button1')
expect($button1.exists()).toBe(true)
expect($button1.is('button')).toBe(true)
const $button2 = wrapper.find('#button2')
expect($button2.exists()).toBe(true)
expect($button2.is('button')).toBe(true)
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
const $content = $modal.find('div.modal-content')
expect($content.exists()).toBe(true)
expect($modal.element.style.display).toEqual('block')
expect(document.activeElement).not.toBe(document.body)
expect(document.activeElement).toBe($content.element)
// Try to focus button1
$button1.element.focus()
$button1.trigger('focusin')
await waitNT(wrapper.vm)
expect(document.activeElement).toBe($button1.element)
expect(document.activeElement).not.toBe($content.element)
// Try to focus button2
$button2.element.focus()
$button2.trigger('focusin')
await waitNT(wrapper.vm)
expect(document.activeElement).toBe($button2.element)
expect(document.activeElement).not.toBe($content.element)
wrapper.destroy()
})
it('it allows focus for elements in "ignore-enforce-focus-selector" prop', async () => {
const App = localVue.extend({
render(h) {
return h('div', {}, [
h('button', { attrs: { id: 'button1', type: 'button' } }, 'Button 1'),
h('button', { attrs: { id: 'button2', type: 'button' } }, 'Button 2'),
h(
BModal,
{
props: {
static: true,
id: 'test',
visible: true,
ignoreEnforceFocusSelector: '#button1'
}
},
'Modal content'
)
])
}
})
const wrapper = mount(App, {
attachToDocument: true,
localVue: localVue,
stubs: {
transition: false
}
})
expect(wrapper.isVueInstance()).toBe(true)
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
await waitNT(wrapper.vm)
await waitRAF()
const $button1 = wrapper.find('#button1')
expect($button1.exists()).toBe(true)
expect($button1.is('button')).toBe(true)
const $button2 = wrapper.find('#button2')
expect($button2.exists()).toBe(true)
expect($button2.is('button')).toBe(true)
const $modal = wrapper.find('div.modal')
expect($modal.exists()).toBe(true)
const $content = $modal.find('div.modal-content')
expect($content.exists()).toBe(true)
expect($modal.element.style.display).toEqual('block')
expect(document.activeElement).not.toBe(document.body)
expect(document.activeElement).toBe($content.element)
// Try to focus button1
$button1.element.focus()
$button1.trigger('focusin')
await waitNT(wrapper.vm)
expect(document.activeElement).toBe($button1.element)
expect(document.activeElement).not.toBe($content.element)
// Try to focus button2
$button2.element.focus()
$button2.trigger('focusin')
await waitNT(wrapper.vm)
expect(document.activeElement).not.toBe($button2.element)
expect(document.activeElement).toBe($content.element)
wrapper.destroy()
})
})
})