diff --git a/js/tests/README.md b/js/tests/README.md index ca99c0ede0..79d05d444f 100644 --- a/js/tests/README.md +++ b/js/tests/README.md @@ -50,22 +50,24 @@ describe('getInstance', () => { }) // Asynchronous test -it('should show a tooltip without the animation', done => { - fixtureEl.innerHTML = '' +it('should show a tooltip without the animation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - animation: false + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + animation: false + }) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tip = document.querySelector('.tooltip') + + expect(tip).not.toBeNull() + expect(tip.classList.contains('fade')).toEqual(false) + resolve() + }) + + tooltip.show() }) - - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tip = document.querySelector('.tooltip') - - expect(tip).not.toBeNull() - expect(tip.classList.contains('fade')).toEqual(false) - done() - }) - - tooltip.show() }) ``` diff --git a/js/tests/unit/alert.spec.js b/js/tests/unit/alert.spec.js index 210ae9a25e..e2fe49246a 100644 --- a/js/tests/unit/alert.spec.js +++ b/js/tests/unit/alert.spec.js @@ -63,60 +63,66 @@ describe('Alert', () => { }) describe('close', () => { - it('should close an alert', done => { - const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) - fixtureEl.innerHTML = '
' + it('should close an alert', () => { + return new Promise(resolve => { + const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) + fixtureEl.innerHTML = '
' - const alertEl = document.querySelector('.alert') - const alert = new Alert(alertEl) + const alertEl = document.querySelector('.alert') + const alert = new Alert(alertEl) - alertEl.addEventListener('closed.bs.alert', () => { - expect(document.querySelectorAll('.alert')).toHaveSize(0) - expect(spy).not.toHaveBeenCalled() - done() + alertEl.addEventListener('closed.bs.alert', () => { + expect(document.querySelectorAll('.alert')).toHaveSize(0) + expect(spy).not.toHaveBeenCalled() + resolve() + }) + + alert.close() }) - - alert.close() }) - it('should close alert with fade class', done => { - fixtureEl.innerHTML = '
' + it('should close alert with fade class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const alertEl = document.querySelector('.alert') - const alert = new Alert(alertEl) + const alertEl = document.querySelector('.alert') + const alert = new Alert(alertEl) - alertEl.addEventListener('transitionend', () => { - expect().nothing() + alertEl.addEventListener('transitionend', () => { + expect().nothing() + }) + + alertEl.addEventListener('closed.bs.alert', () => { + expect(document.querySelectorAll('.alert')).toHaveSize(0) + resolve() + }) + + alert.close() }) - - alertEl.addEventListener('closed.bs.alert', () => { - expect(document.querySelectorAll('.alert')).toHaveSize(0) - done() - }) - - alert.close() }) - it('should not remove alert if close event is prevented', done => { - fixtureEl.innerHTML = '
' + it('should not remove alert if close event is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const getAlert = () => document.querySelector('.alert') - const alertEl = getAlert() - const alert = new Alert(alertEl) + const getAlert = () => document.querySelector('.alert') + const alertEl = getAlert() + const alert = new Alert(alertEl) - alertEl.addEventListener('close.bs.alert', event => { - event.preventDefault() - setTimeout(() => { - expect(getAlert()).not.toBeNull() - done() - }, 10) + alertEl.addEventListener('close.bs.alert', event => { + event.preventDefault() + setTimeout(() => { + expect(getAlert()).not.toBeNull() + resolve() + }, 10) + }) + + alertEl.addEventListener('closed.bs.alert', () => { + throw new Error('should not fire closed event') + }) + + alert.close() }) - - alertEl.addEventListener('closed.bs.alert', () => { - throw new Error('should not fire closed event') - }) - - alert.close() }) }) diff --git a/js/tests/unit/carousel.spec.js b/js/tests/unit/carousel.spec.js index 7b58b9de9e..1c91cebec4 100644 --- a/js/tests/unit/carousel.spec.js +++ b/js/tests/unit/carousel.spec.js @@ -63,94 +63,100 @@ describe('Carousel', () => { expect(carouselByElement._element).toEqual(carouselEl) }) - it('should go to next item if right arrow key is pressed', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should go to next item if right arrow key is pressed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { - keyboard: true + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, { + keyboard: true + }) + + spyOn(carousel, '_keydown').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) + expect(carousel._keydown).toHaveBeenCalled() + resolve() + }) + + const keydown = createEvent('keydown') + keydown.key = 'ArrowRight' + + carouselEl.dispatchEvent(keydown) }) - - spyOn(carousel, '_keydown').and.callThrough() - - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) - expect(carousel._keydown).toHaveBeenCalled() - done() - }) - - const keydown = createEvent('keydown') - keydown.key = 'ArrowRight' - - carouselEl.dispatchEvent(keydown) }) - it('should go to previous item if left arrow key is pressed', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should go to previous item if left arrow key is pressed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { - keyboard: true + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, { + keyboard: true + }) + + spyOn(carousel, '_keydown').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item1')) + expect(carousel._keydown).toHaveBeenCalled() + resolve() + }) + + const keydown = createEvent('keydown') + keydown.key = 'ArrowLeft' + + carouselEl.dispatchEvent(keydown) }) - - spyOn(carousel, '_keydown').and.callThrough() - - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item1')) - expect(carousel._keydown).toHaveBeenCalled() - done() - }) - - const keydown = createEvent('keydown') - keydown.key = 'ArrowLeft' - - carouselEl.dispatchEvent(keydown) }) - it('should not prevent keydown if key is not ARROW_LEFT or ARROW_RIGHT', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should not prevent keydown if key is not ARROW_LEFT or ARROW_RIGHT', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { - keyboard: true + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, { + keyboard: true + }) + + spyOn(carousel, '_keydown').and.callThrough() + + carouselEl.addEventListener('keydown', event => { + expect(carousel._keydown).toHaveBeenCalled() + expect(event.defaultPrevented).toBeFalse() + resolve() + }) + + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + + carouselEl.dispatchEvent(keydown) }) - - spyOn(carousel, '_keydown').and.callThrough() - - carouselEl.addEventListener('keydown', event => { - expect(carousel._keydown).toHaveBeenCalled() - expect(event.defaultPrevented).toBeFalse() - done() - }) - - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' - - carouselEl.dispatchEvent(keydown) }) it('should ignore keyboard events within s and ', - ' ', - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') - const textarea = fixtureEl.querySelector('textarea') - - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - input.focus() - const keydown = createEvent('keydown') - - keydown.key = 'ArrowUp' - input.dispatchEvent(keydown) - - expect(document.activeElement).toEqual(input, 'input still focused') - - textarea.focus() - textarea.dispatchEvent(keydown) - - expect(document.activeElement).toEqual(textarea, 'textarea still focused') - done() - }) - - triggerDropdown.click() - }) - - it('should skip disabled element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' - - triggerDropdown.dispatchEvent(keydown) - triggerDropdown.dispatchEvent(keydown) - - expect(document.activeElement).not.toHaveClass('disabled') - expect(document.activeElement.hasAttribute('disabled')).toBeFalse() - done() - }) - - triggerDropdown.click() - }) - - it('should skip hidden element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' - - triggerDropdown.dispatchEvent(keydown) - - expect(document.activeElement).not.toHaveClass('d-none') - expect(document.activeElement.style.display).not.toEqual('none') - expect(document.activeElement.style.visibility).not.toEqual('hidden') - - done() - }) - - triggerDropdown.click() - }) - - it('should focus next/previous element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const item1 = fixtureEl.querySelector('#item1') - const item2 = fixtureEl.querySelector('#item2') - - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydownArrowDown = createEvent('keydown') - keydownArrowDown.key = 'ArrowDown' - - triggerDropdown.dispatchEvent(keydownArrowDown) - expect(document.activeElement).toEqual(item1, 'item1 is focused') - - document.activeElement.dispatchEvent(keydownArrowDown) - expect(document.activeElement).toEqual(item2, 'item2 is focused') - - const keydownArrowUp = createEvent('keydown') - keydownArrowUp.key = 'ArrowUp' - - document.activeElement.dispatchEvent(keydownArrowUp) - expect(document.activeElement).toEqual(item1, 'item1 is focused') - - done() - }) - - triggerDropdown.click() - }) - - it('should open the dropdown and focus on the last item when using ArrowUp for the first time', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const lastItem = fixtureEl.querySelector('#item2') - - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - setTimeout(() => { - expect(document.activeElement).toEqual(lastItem, 'item2 is focused') - done() - }) - }) - - const keydown = createEvent('keydown') - keydown.key = 'ArrowUp' - triggerDropdown.dispatchEvent(keydown) - }) - - it('should open the dropdown and focus on the first item when using ArrowDown for the first time', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const firstItem = fixtureEl.querySelector('#item1') - - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - setTimeout(() => { - expect(document.activeElement).toEqual(firstItem, 'item1 is focused') - done() - }) - }) - - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' - triggerDropdown.dispatchEvent(keydown) - }) - - it('should not close the dropdown if the user clicks on a text field within dropdown-menu', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') - - input.addEventListener('click', () => { - expect(triggerDropdown).toHaveClass('show') - done() - }) - - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdown).toHaveClass('show') - input.dispatchEvent(createEvent('click')) - }) - - triggerDropdown.click() - }) - - it('should not close the dropdown if the user clicks on a textarea within dropdown-menu', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const textarea = fixtureEl.querySelector('textarea') - - textarea.addEventListener('click', () => { - expect(triggerDropdown).toHaveClass('show') - done() - }) - - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdown).toHaveClass('show') - textarea.dispatchEvent(createEvent('click')) - }) - - triggerDropdown.click() - }) - - it('should close the dropdown if the user clicks on a text field that is not contained within dropdown-menu', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') - - triggerDropdown.addEventListener('hidden.bs.dropdown', () => { - expect().nothing() - done() - }) - - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - input.dispatchEvent(createEvent('click', { - bubbles: true + btnDropdown.addEventListener('shown.bs.dropdown', event => setTimeout(() => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + expect(showEventTriggered).toBeTrue() + expect(event.relatedTarget).toEqual(btnDropdown) + document.body.click() })) - }) - triggerDropdown.click() + btnDropdown.addEventListener('hide.bs.dropdown', () => { + hideEventTriggered = true + }) + + btnDropdown.addEventListener('hidden.bs.dropdown', event => { + expect(btnDropdown).not.toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') + expect(hideEventTriggered).toBeTrue() + expect(event.relatedTarget).toEqual(btnDropdown) + resolve() + }) + + btnDropdown.click() + }) }) - it('should ignore keyboard events for s and ', - ' ', - '' - ].join('') + it('should not use "static" Popper in navbar', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') - const textarea = fixtureEl.querySelector('textarea') + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - const test = (eventKey, elementToDispatch) => { - const event = createEvent('keydown') - event.key = eventKey - elementToDispatch.focus() - elementToDispatch.dispatchEvent(event) - expect(document.activeElement).toEqual(elementToDispatch, `${elementToDispatch.tagName} still focused`) - } + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(dropdown._popper).not.toBeNull() + expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') + resolve() + }) - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' - - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - // Key Space - test('Space', input) - - test('Space', textarea) - - // Key ArrowUp - test('ArrowUp', input) - - test('ArrowUp', textarea) - - // Key ArrowDown - test('ArrowDown', input) - - test('ArrowDown', textarea) - - // Key Escape - input.focus() - input.dispatchEvent(keydownEscape) - - expect(triggerDropdown).not.toHaveClass('show') - done() + dropdown.show() }) - - triggerDropdown.click() }) - it('should not open dropdown if escape key was pressed on the toggle', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') + it('should not collapse the dropdown when clicking a select option nested in the dropdown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(triggerDropdown) - const button = fixtureEl.querySelector('button[data-bs-toggle="dropdown"]') + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - spyOn(dropdown, 'toggle') + const hideSpy = spyOn(dropdown, '_completeHide') - // Key escape - button.focus() - // Key escape - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' - button.dispatchEvent(keydownEscape) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + const clickEvent = new MouseEvent('click', { + bubbles: true + }) - setTimeout(() => { - expect(dropdown.toggle).not.toHaveBeenCalled() - expect(triggerDropdown).not.toHaveClass('show') - done() - }, 20) + dropdownMenu.querySelector('option').dispatchEvent(clickEvent) + }) + + dropdownMenu.addEventListener('click', event => { + expect(event.target.tagName).toMatch(/select|option/i) + + Dropdown.clearMenus(event) + + setTimeout(() => { + expect(hideSpy).not.toHaveBeenCalled() + resolve() + }, 10) + }) + + dropdown.show() + }) }) - it('should propagate escape key events if dropdown is closed', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') + it('should manage bs attribute `data-bs-popper`="static" when dropdown is in navbar', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const parent = fixtureEl.querySelector('.parent') - const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - const parentKeyHandler = jasmine.createSpy('parentKeyHandler') + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') + dropdown.hide() + }) - parent.addEventListener('keydown', parentKeyHandler) - parent.addEventListener('keyup', () => { - expect(parentKeyHandler).toHaveBeenCalled() - done() + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull() + resolve() + }) + + dropdown.show() }) - - const keydownEscape = createEvent('keydown', { bubbles: true }) - keydownEscape.key = 'Escape' - const keyupEscape = createEvent('keyup', { bubbles: true }) - keyupEscape.key = 'Escape' - - toggle.focus() - toggle.dispatchEvent(keydownEscape) - toggle.dispatchEvent(keyupEscape) }) - it('should close dropdown using `escape` button, and return focus to its trigger', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should not use Popper if display set to static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - toggle.addEventListener('shown.bs.dropdown', () => { - const keydownEvent = createEvent('keydown', { bubbles: true }) - keydownEvent.key = 'ArrowDown' - toggle.dispatchEvent(keydownEvent) - keydownEvent.key = 'Escape' - toggle.dispatchEvent(keydownEvent) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + // Popper adds this attribute when we use it + expect(dropdownMenu.getAttribute('data-popper-placement')).toBeNull() + resolve() + }) + + btnDropdown.click() }) - - toggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { - expect(document.activeElement).toEqual(toggle) - done() - })) - - toggle.click() }) - it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should manage bs attribute `data-bs-popper`="static" when display set to static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - const expectDropdownToBeOpened = () => setTimeout(() => { - expect(dropdownToggle).toHaveClass('show') - dropdownMenu.click() - }, 150) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') + dropdown.hide() + }) - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - document.documentElement.click() - expectDropdownToBeOpened() + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull() + resolve() + }) + + dropdown.show() }) - - dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { - expect(dropdownToggle).not.toHaveClass('show') - done() - })) - - dropdownToggle.click() }) - it('should close dropdown (only) by clicking outside the dropdown menu when it has data-attribute `data-bs-auto-close="outside"`', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should remove "show" class if tabbing outside of menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const expectDropdownToBeOpened = () => setTimeout(() => { - expect(dropdownToggle).toHaveClass('show') - document.documentElement.click() - }, 150) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - dropdownMenu.click() - expectDropdownToBeOpened() + const keyup = createEvent('keyup') + + keyup.key = 'Tab' + document.dispatchEvent(keyup) + }) + + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(btnDropdown).not.toHaveClass('show') + resolve() + }) + + btnDropdown.click() }) - - dropdownToggle.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownToggle).not.toHaveClass('show') - done() - }) - - dropdownToggle.click() }) - it('should not close dropdown by clicking inside or outside the dropdown menu when it has data-attribute `data-bs-auto-close="false"`', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should remove "show" class if body is clicked, with multiple dropdowns', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + ' ', + ' ', + ' ', + '
' + ].join('') - const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const triggerDropdownList = fixtureEl.querySelectorAll('[data-bs-toggle="dropdown"]') - const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => { - expect(dropdownToggle).toHaveClass('show') - if (shouldTriggerClick) { - document.documentElement.click() - } else { - done() + expect(triggerDropdownList).toHaveSize(2) + + const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList + + triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdownFirst).toHaveClass('show') + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) + document.body.click() + }) + + triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) + triggerDropdownLast.click() + }) + + triggerDropdownLast.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdownLast).toHaveClass('show') + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) + document.body.click() + }) + + triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) + resolve() + }) + + triggerDropdownFirst.click() + }) + }) + + it('should remove "show" class if body if tabbing outside of menu, with multiple dropdowns', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + ' ', + ' ', + ' ', + '
' + ].join('') + + const triggerDropdownList = fixtureEl.querySelectorAll('[data-bs-toggle="dropdown"]') + + expect(triggerDropdownList).toHaveSize(2) + + const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList + + triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdownFirst).toHaveClass('show') + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) + + const keyup = createEvent('keyup') + keyup.key = 'Tab' + + document.dispatchEvent(keyup) + }) + + triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) + triggerDropdownLast.click() + }) + + triggerDropdownLast.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdownLast).toHaveClass('show') + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) + + const keyup = createEvent('keyup') + keyup.key = 'Tab' + + document.dispatchEvent(keyup) + }) + + triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) + resolve() + }) + + triggerDropdownFirst.click() + }) + }) + + it('should fire hide and hidden event without a clickEvent if event type is not click', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + triggerDropdown.addEventListener('hide.bs.dropdown', event => { + expect(event.clickEvent).toBeUndefined() + }) + + triggerDropdown.addEventListener('hidden.bs.dropdown', event => { + expect(event.clickEvent).toBeUndefined() + resolve() + }) + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') + + keydown.key = 'Escape' + triggerDropdown.dispatchEvent(keydown) + }) + + triggerDropdown.click() + }) + }) + + it('should bubble up the events to the parent elements', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownParent = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(triggerDropdown) + + const showFunction = jasmine.createSpy('showFunction') + dropdownParent.addEventListener('show.bs.dropdown', showFunction) + + const shownFunction = jasmine.createSpy('shownFunction') + dropdownParent.addEventListener('shown.bs.dropdown', () => { + shownFunction() + dropdown.hide() + }) + + const hideFunction = jasmine.createSpy('hideFunction') + dropdownParent.addEventListener('hide.bs.dropdown', hideFunction) + + dropdownParent.addEventListener('hidden.bs.dropdown', () => { + expect(showFunction).toHaveBeenCalled() + expect(shownFunction).toHaveBeenCalled() + expect(hideFunction).toHaveBeenCalled() + resolve() + }) + + dropdown.show() + }) + }) + + it('should ignore keyboard events within s and ', + ' ', + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + input.focus() + const keydown = createEvent('keydown') + + keydown.key = 'ArrowUp' + input.dispatchEvent(keydown) + + expect(document.activeElement).toEqual(input, 'input still focused') + + textarea.focus() + textarea.dispatchEvent(keydown) + + expect(document.activeElement).toEqual(textarea, 'textarea still focused') + resolve() + }) + + triggerDropdown.click() + }) + }) + + it('should skip disabled element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + + triggerDropdown.dispatchEvent(keydown) + triggerDropdown.dispatchEvent(keydown) + + expect(document.activeElement).not.toHaveClass('disabled') + expect(document.activeElement.hasAttribute('disabled')).toBeFalse() + resolve() + }) + + triggerDropdown.click() + }) + }) + + it('should skip hidden element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + + triggerDropdown.dispatchEvent(keydown) + + expect(document.activeElement).not.toHaveClass('d-none') + expect(document.activeElement.style.display).not.toEqual('none') + expect(document.activeElement.style.visibility).not.toEqual('hidden') + + resolve() + }) + + triggerDropdown.click() + }) + }) + + it('should focus next/previous element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const item1 = fixtureEl.querySelector('#item1') + const item2 = fixtureEl.querySelector('#item2') + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydownArrowDown = createEvent('keydown') + keydownArrowDown.key = 'ArrowDown' + + triggerDropdown.dispatchEvent(keydownArrowDown) + expect(document.activeElement).toEqual(item1, 'item1 is focused') + + document.activeElement.dispatchEvent(keydownArrowDown) + expect(document.activeElement).toEqual(item2, 'item2 is focused') + + const keydownArrowUp = createEvent('keydown') + keydownArrowUp.key = 'ArrowUp' + + document.activeElement.dispatchEvent(keydownArrowUp) + expect(document.activeElement).toEqual(item1, 'item1 is focused') + + resolve() + }) + + triggerDropdown.click() + }) + }) + + it('should open the dropdown and focus on the last item when using ArrowUp for the first time', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const lastItem = fixtureEl.querySelector('#item2') + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + setTimeout(() => { + expect(document.activeElement).toEqual(lastItem, 'item2 is focused') + resolve() + }) + }) + + const keydown = createEvent('keydown') + keydown.key = 'ArrowUp' + triggerDropdown.dispatchEvent(keydown) + }) + }) + + it('should open the dropdown and focus on the first item when using ArrowDown for the first time', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const firstItem = fixtureEl.querySelector('#item1') + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + setTimeout(() => { + expect(document.activeElement).toEqual(firstItem, 'item1 is focused') + resolve() + }) + }) + + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + triggerDropdown.dispatchEvent(keydown) + }) + }) + + it('should not close the dropdown if the user clicks on a text field within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + + input.addEventListener('click', () => { + expect(triggerDropdown).toHaveClass('show') + resolve() + }) + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdown).toHaveClass('show') + input.dispatchEvent(createEvent('click')) + }) + + triggerDropdown.click() + }) + }) + + it('should not close the dropdown if the user clicks on a textarea within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const textarea = fixtureEl.querySelector('textarea') + + textarea.addEventListener('click', () => { + expect(triggerDropdown).toHaveClass('show') + resolve() + }) + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdown).toHaveClass('show') + textarea.dispatchEvent(createEvent('click')) + }) + + triggerDropdown.click() + }) + }) + + it('should close the dropdown if the user clicks on a text field that is not contained within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + + triggerDropdown.addEventListener('hidden.bs.dropdown', () => { + expect().nothing() + resolve() + }) + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + input.dispatchEvent(createEvent('click', { + bubbles: true + })) + }) + + triggerDropdown.click() + }) + }) + + it('should ignore keyboard events for s and ', + ' ', + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') + + const test = (eventKey, elementToDispatch) => { + const event = createEvent('keydown') + event.key = eventKey + elementToDispatch.focus() + elementToDispatch.dispatchEvent(event) + expect(document.activeElement).toEqual(elementToDispatch, `${elementToDispatch.tagName} still focused`) } - expectDropdownToBeOpened(false) - }, 150) + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - dropdownMenu.click() - expectDropdownToBeOpened() + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + // Key Space + test('Space', input) + + test('Space', textarea) + + // Key ArrowUp + test('ArrowUp', input) + + test('ArrowUp', textarea) + + // Key ArrowDown + test('ArrowDown', input) + + test('ArrowDown', textarea) + + // Key Escape + input.focus() + input.dispatchEvent(keydownEscape) + + expect(triggerDropdown).not.toHaveClass('show') + resolve() + }) + + triggerDropdown.click() }) + }) - dropdownToggle.click() + it('should not open dropdown if escape key was pressed on the toggle', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(triggerDropdown) + const button = fixtureEl.querySelector('button[data-bs-toggle="dropdown"]') + + spyOn(dropdown, 'toggle') + + // Key escape + button.focus() + // Key escape + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' + button.dispatchEvent(keydownEscape) + + setTimeout(() => { + expect(dropdown.toggle).not.toHaveBeenCalled() + expect(triggerDropdown).not.toHaveClass('show') + resolve() + }, 20) + }) + }) + + it('should propagate escape key events if dropdown is closed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') + + const parent = fixtureEl.querySelector('.parent') + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + const parentKeyHandler = jasmine.createSpy('parentKeyHandler') + + parent.addEventListener('keydown', parentKeyHandler) + parent.addEventListener('keyup', () => { + expect(parentKeyHandler).toHaveBeenCalled() + resolve() + }) + + const keydownEscape = createEvent('keydown', { bubbles: true }) + keydownEscape.key = 'Escape' + const keyupEscape = createEvent('keyup', { bubbles: true }) + keyupEscape.key = 'Escape' + + toggle.focus() + toggle.dispatchEvent(keydownEscape) + toggle.dispatchEvent(keyupEscape) + }) + }) + + it('should close dropdown using `escape` button, and return focus to its trigger', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + toggle.addEventListener('shown.bs.dropdown', () => { + const keydownEvent = createEvent('keydown', { bubbles: true }) + keydownEvent.key = 'ArrowDown' + toggle.dispatchEvent(keydownEvent) + keydownEvent.key = 'Escape' + toggle.dispatchEvent(keydownEvent) + }) + + toggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(document.activeElement).toEqual(toggle) + resolve() + })) + + toggle.click() + }) + }) + + it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + dropdownMenu.click() + }, 150) + + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + document.documentElement.click() + expectDropdownToBeOpened() + }) + + dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(dropdownToggle).not.toHaveClass('show') + resolve() + })) + + dropdownToggle.click() + }) + }) + + it('should close dropdown (only) by clicking outside the dropdown menu when it has data-attribute `data-bs-auto-close="outside"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + document.documentElement.click() + }, 150) + + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) + + dropdownToggle.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownToggle).not.toHaveClass('show') + resolve() + }) + + dropdownToggle.click() + }) + }) + + it('should not close dropdown by clicking inside or outside the dropdown menu when it has data-attribute `data-bs-auto-close="false"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + + const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + if (shouldTriggerClick) { + document.documentElement.click() + } else { + resolve() + } + + expectDropdownToBeOpened(false) + }, 150) + + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) + + dropdownToggle.click() + }) }) }) @@ -2030,52 +2142,54 @@ describe('Dropdown', () => { }) }) - it('should open dropdown when pressing keydown or keyup', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should open dropdown when pressing keydown or keyup', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = fixtureEl.querySelector('.dropdown') - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - const keyup = createEvent('keyup') - keyup.key = 'ArrowUp' + const keyup = createEvent('keyup') + keyup.key = 'ArrowUp' - const handleArrowDown = () => { - expect(triggerDropdown).toHaveClass('show') - expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') - setTimeout(() => { - dropdown.hide() - keydown.key = 'ArrowUp' - triggerDropdown.dispatchEvent(keyup) - }, 20) - } - - const handleArrowUp = () => { - expect(triggerDropdown).toHaveClass('show') - expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - } - - dropdown.addEventListener('shown.bs.dropdown', event => { - if (event.target.key === 'ArrowDown') { - handleArrowDown() - } else { - handleArrowUp() + const handleArrowDown = () => { + expect(triggerDropdown).toHaveClass('show') + expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') + setTimeout(() => { + dropdown.hide() + keydown.key = 'ArrowUp' + triggerDropdown.dispatchEvent(keyup) + }, 20) } - }) - triggerDropdown.dispatchEvent(keydown) + const handleArrowUp = () => { + expect(triggerDropdown).toHaveClass('show') + expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + } + + dropdown.addEventListener('shown.bs.dropdown', event => { + if (event.target.key === 'ArrowDown') { + handleArrowDown() + } else { + handleArrowUp() + } + }) + + triggerDropdown.dispatchEvent(keydown) + }) }) it('should allow `data-bs-toggle="dropdown"` click events to bubble up', () => { @@ -2101,27 +2215,29 @@ describe('Dropdown', () => { expect(delegatedClickListener).toHaveBeenCalled() }) - it('should open the dropdown when clicking the child element inside `data-bs-toggle="dropdown"`', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') + it('should open the dropdown when clicking the child element inside `data-bs-toggle="dropdown"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const childElement = fixtureEl.querySelector('#childElement') + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const childElement = fixtureEl.querySelector('#childElement') - btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => { - expect(btnDropdown).toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - })) + btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + })) - childElement.click() + childElement.click() + }) }) }) diff --git a/js/tests/unit/jquery.spec.js b/js/tests/unit/jquery.spec.js index 16781a3518..7da39d6303 100644 --- a/js/tests/unit/jquery.spec.js +++ b/js/tests/unit/jquery.spec.js @@ -12,7 +12,7 @@ import ScrollSpy from '../../src/scrollspy' import Tab from '../../src/tab' import Toast from '../../src/toast' import Tooltip from '../../src/tooltip' -import { getFixture, clearFixture } from '../helpers/fixture' +import { clearFixture, getFixture } from '../helpers/fixture' describe('jQuery', () => { let fixtureEl @@ -40,19 +40,21 @@ describe('jQuery', () => { expect(Tooltip.jQueryInterface).toEqual(jQuery.fn.tooltip) }) - it('should use jQuery event system', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') + it('should use jQuery event system', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') - $(fixtureEl).find('.alert') - .one('closed.bs.alert', () => { - expect($(fixtureEl).find('.alert')).toHaveSize(0) - done() - }) + $(fixtureEl).find('.alert') + .one('closed.bs.alert', () => { + expect($(fixtureEl).find('.alert')).toHaveSize(0) + resolve() + }) - $(fixtureEl).find('button').trigger('click') + $(fixtureEl).find('button').trigger('click') + }) }) }) diff --git a/js/tests/unit/modal.spec.js b/js/tests/unit/modal.spec.js index 84a95c86ad..bf796411b5 100644 --- a/js/tests/unit/modal.spec.js +++ b/js/tests/unit/modal.spec.js @@ -56,93 +56,101 @@ describe('Modal', () => { }) describe('toggle', () => { - it('should call ScrollBarHelper to handle scrollBar on body', done => { - fixtureEl.innerHTML = '' + it('should call ScrollBarHelper to handle scrollBar on body', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() - spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() + spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + + modalEl.addEventListener('shown.bs.modal', () => { + expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled() + modal.toggle() + }) + + modalEl.addEventListener('hidden.bs.modal', () => { + expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled() + resolve() + }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled() modal.toggle() }) - - modalEl.addEventListener('hidden.bs.modal', () => { - expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled() - done() - }) - - modal.toggle() }) }) describe('show', () => { - it('should show a modal', done => { - fixtureEl.innerHTML = '' + it('should show a modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('show.bs.modal', event => { - expect(event).toBeDefined() + modalEl.addEventListener('show.bs.modal', event => { + expect(event).toBeDefined() + }) + + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toEqual('true') + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toBeNull() + expect(modalEl.style.display).toEqual('block') + expect(document.querySelector('.modal-backdrop')).not.toBeNull() + resolve() + }) + + modal.show() }) - - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toEqual('true') - expect(modalEl.getAttribute('role')).toEqual('dialog') - expect(modalEl.getAttribute('aria-hidden')).toBeNull() - expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).not.toBeNull() - done() - }) - - modal.show() }) - it('should show a modal without backdrop', done => { - fixtureEl.innerHTML = '' + it('should show a modal without backdrop', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: false + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: false + }) + + modalEl.addEventListener('show.bs.modal', event => { + expect(event).toBeDefined() + }) + + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toEqual('true') + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toBeNull() + expect(modalEl.style.display).toEqual('block') + expect(document.querySelector('.modal-backdrop')).toBeNull() + resolve() + }) + + modal.show() }) - - modalEl.addEventListener('show.bs.modal', event => { - expect(event).toBeDefined() - }) - - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toEqual('true') - expect(modalEl.getAttribute('role')).toEqual('dialog') - expect(modalEl.getAttribute('aria-hidden')).toBeNull() - 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' + it('should show a modal and append the element', () => { + return new Promise(resolve => { + const modalEl = document.createElement('div') + const id = 'dynamicModal' - modalEl.setAttribute('id', id) - modalEl.classList.add('modal') - modalEl.innerHTML = '' + modalEl.setAttribute('id', id) + modalEl.classList.add('modal') + modalEl.innerHTML = '' - const modal = new Modal(modalEl) + const modal = new Modal(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - const dynamicModal = document.getElementById(id) - expect(dynamicModal).not.toBeNull() - dynamicModal.remove() - done() + modalEl.addEventListener('shown.bs.modal', () => { + const dynamicModal = document.getElementById(id) + expect(dynamicModal).not.toBeNull() + dynamicModal.remove() + resolve() + }) + + modal.show() }) - - modal.show() }) it('should do nothing if a modal is shown', () => { @@ -173,511 +181,551 @@ describe('Modal', () => { expect(EventHandler.trigger).not.toHaveBeenCalled() }) - it('should not fire shown event when show is prevented', done => { - fixtureEl.innerHTML = '' + it('should not fire shown event when show is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('show.bs.modal', event => { - event.preventDefault() + modalEl.addEventListener('show.bs.modal', event => { + event.preventDefault() - const expectedDone = () => { - expect().nothing() - done() - } + const expectedDone = () => { + expect().nothing() + resolve() + } - setTimeout(expectedDone, 10) + setTimeout(expectedDone, 10) + }) + + modalEl.addEventListener('shown.bs.modal', () => { + throw new Error('shown event triggered') + }) + + modal.show() }) - - modalEl.addEventListener('shown.bs.modal', () => { - throw new Error('shown event triggered') - }) - - modal.show() }) - it('should be shown after the first call to show() has been prevented while fading is enabled ', done => { - fixtureEl.innerHTML = '' + it('should be shown after the first call to show() has been prevented while fading is enabled ', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - let prevented = false - modalEl.addEventListener('show.bs.modal', event => { - if (!prevented) { - event.preventDefault() - prevented = true + let prevented = false + modalEl.addEventListener('show.bs.modal', event => { + if (!prevented) { + event.preventDefault() + prevented = true + + setTimeout(() => { + modal.show() + }) + } + }) + + modalEl.addEventListener('shown.bs.modal', () => { + expect(prevented).toBeTrue() + expect(modal._isAnimated()).toBeTrue() + resolve() + }) + + modal.show() + }) + }) + it('should set is transitioning if fade class is present', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + + modalEl.addEventListener('show.bs.modal', () => { + setTimeout(() => { + expect(modal._isTransitioning).toBeTrue() + }) + }) + + modalEl.addEventListener('shown.bs.modal', () => { + expect(modal._isTransitioning).toBeFalse() + resolve() + }) + + modal.show() + }) + }) + + it('should close modal when a click occurred on data-bs-dismiss="modal" inside modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const btnClose = fixtureEl.querySelector('[data-bs-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() + resolve() + }) + + modal.show() + }) + }) + + it('should close modal when a click occurred on a data-bs-dismiss="modal" with "bs-target" outside of modal element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const btnClose = fixtureEl.querySelector('[data-bs-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() + resolve() + }) + + modal.show() + }) + }) + + it('should set .modal\'s scroll top to 0', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.scrollTop).toEqual(0) + resolve() + }) + + modal.show() + }) + }) + + it('should set modal body scroll top to 0 if modal body do not exists', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const modalBody = modalEl.querySelector('.modal-body') + const modal = new Modal(modalEl) + + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalBody.scrollTop).toEqual(0) + resolve() + }) + + modal.show() + }) + }) + + it('should not trap focus if focus equal to false', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + focus: false + }) + + spyOn(modal._focustrap, 'activate').and.callThrough() + + modalEl.addEventListener('shown.bs.modal', () => { + expect(modal._focustrap.activate).not.toHaveBeenCalled() + resolve() + }) + + modal.show() + }) + }) + + it('should add listener when escape touch is pressed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + 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.key = 'Escape' + + modalEl.dispatchEvent(keydownEscape) + }) + + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modal.hide).toHaveBeenCalled() + resolve() + }) + + modal.show() + }) + }) + + it('should do nothing when the pressed key is not escape', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + + spyOn(modal, 'hide') + + const expectDone = () => { + expect(modal.hide).not.toHaveBeenCalled() + + resolve() + } + + modalEl.addEventListener('shown.bs.modal', () => { + const keydownTab = createEvent('keydown') + keydownTab.key = 'Tab' + + modalEl.dispatchEvent(keydownTab) + setTimeout(expectDone, 30) + }) + + modal.show() + }) + }) + + it('should adjust dialog on resize', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + + spyOn(modal, '_adjustDialog').and.callThrough() + + const expectDone = () => { + expect(modal._adjustDialog).toHaveBeenCalled() + + resolve() + } + + modalEl.addEventListener('shown.bs.modal', () => { + const resizeEvent = createEvent('resize') + + window.dispatchEvent(resizeEvent) + setTimeout(expectDone, 10) + }) + + modal.show() + }) + }) + + it('should not close modal when clicking on modal-content', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toEqual(true) + resolve() + }, 10) + } + + modalEl.addEventListener('shown.bs.modal', () => { + fixtureEl.querySelector('.modal-dialog').click() + fixtureEl.querySelector('.modal-content').click() + shownCallback() + }) + + modalEl.addEventListener('hidden.bs.modal', () => { + throw new Error('Should not hide a modal') + }) + + modal.show() + }) + }) + + it('should not close modal when clicking outside of modal-content if backdrop = false', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: false + }) + + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeTrue() + resolve() + }, 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 close modal when clicking outside of modal-content if backdrop = static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: 'static' + }) + + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeTrue() + resolve() + }, 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 close modal when escape key is pressed with keyboard = true and backdrop is static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: 'static', + keyboard: true + }) + + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeFalse() + resolve() + }, 10) + } + + modalEl.addEventListener('shown.bs.modal', () => { + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' + + modalEl.dispatchEvent(keydownEscape) + shownCallback() + }) + + modal.show() + }) + }) + + it('should not close modal when escape key is pressed with keyboard = false', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + keyboard: false + }) + + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeTrue() + resolve() + }, 10) + } + + modalEl.addEventListener('shown.bs.modal', () => { + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' + + modalEl.dispatchEvent(keydownEscape) + shownCallback() + }) + + modalEl.addEventListener('hidden.bs.modal', () => { + throw new Error('Should not hide a modal') + }) + + modal.show() + }) + }) + + it('should not overflow when clicking outside of modal-content if backdrop = static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: 'static' + }) + + modalEl.addEventListener('shown.bs.modal', () => { + modalEl.click() + setTimeout(() => { + expect(modalEl.clientHeight).toEqual(modalEl.scrollHeight) + resolve() + }, 20) + }) + + modal.show() + }) + }) + + it('should not queue multiple callbacks when clicking outside of modal-content and backdrop = static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: 'static' + }) + + modalEl.addEventListener('shown.bs.modal', () => { + const spy = spyOn(modal, '_queueCallback').and.callThrough() + + modalEl.click() + modalEl.click() setTimeout(() => { - modal.show() - }) - } - }) - - modalEl.addEventListener('shown.bs.modal', () => { - expect(prevented).toBeTrue() - expect(modal._isAnimated()).toBeTrue() - done() - }) - - modal.show() - }) - - it('should set is transitioning if fade class is present', done => { - fixtureEl.innerHTML = '' - - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - - modalEl.addEventListener('show.bs.modal', () => { - setTimeout(() => { - expect(modal._isTransitioning).toBeTrue() + expect(spy).toHaveBeenCalledTimes(1) + resolve() + }, 20) }) - }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal._isTransitioning).toBeFalse() - done() + modal.show() }) - - modal.show() }) - it('should close modal when a click occurred on data-bs-dismiss="modal" inside modal', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should trap focus', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - spyOn(modal, 'hide').and.callThrough() + spyOn(modal._focustrap, 'activate').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - btnClose.click() + modalEl.addEventListener('shown.bs.modal', () => { + expect(modal._focustrap.activate).toHaveBeenCalled() + resolve() + }) + + modal.show() }) - - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modal.hide).toHaveBeenCalled() - done() - }) - - modal.show() - }) - - it('should close modal when a click occurred on a data-bs-dismiss="modal" with "bs-target" outside of modal element', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') - - const modalEl = fixtureEl.querySelector('.modal') - const btnClose = fixtureEl.querySelector('[data-bs-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\'s scroll top to 0', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.scrollTop).toEqual(0) - done() - }) - - modal.show() - }) - - it('should set modal body scroll top to 0 if modal body do not exists', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const modalEl = fixtureEl.querySelector('.modal') - const modalBody = modalEl.querySelector('.modal-body') - const modal = new Modal(modalEl) - - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalBody.scrollTop).toEqual(0) - done() - }) - - modal.show() - }) - - it('should not trap focus if focus equal to false', done => { - fixtureEl.innerHTML = '' - - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - focus: false - }) - - spyOn(modal._focustrap, 'activate').and.callThrough() - - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal._focustrap.activate).not.toHaveBeenCalled() - done() - }) - - modal.show() - }) - - it('should add listener when escape touch is pressed', done => { - fixtureEl.innerHTML = '' - - 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.key = 'Escape' - - 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 = '' - - 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.key = 'Tab' - - modalEl.dispatchEvent(keydownTab) - setTimeout(expectDone, 30) - }) - - modal.show() - }) - - it('should adjust dialog on resize', done => { - fixtureEl.innerHTML = '' - - 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 on modal-content', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toEqual(true) - done() - }, 10) - } - - modalEl.addEventListener('shown.bs.modal', () => { - fixtureEl.querySelector('.modal-dialog').click() - fixtureEl.querySelector('.modal-content').click() - shownCallback() - }) - - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('Should not hide a modal') - }) - - modal.show() - }) - - it('should not close modal when clicking outside of modal-content if backdrop = false', done => { - fixtureEl.innerHTML = '' - - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: false - }) - - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toBeTrue() - 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 close modal when clicking outside of modal-content if backdrop = static', done => { - fixtureEl.innerHTML = '' - - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: 'static' - }) - - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toBeTrue() - 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 close modal when escape key is pressed with keyboard = true and backdrop is static', done => { - fixtureEl.innerHTML = '' - - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: 'static', - keyboard: true - }) - - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toBeFalse() - done() - }, 10) - } - - modalEl.addEventListener('shown.bs.modal', () => { - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' - - modalEl.dispatchEvent(keydownEscape) - shownCallback() - }) - - modal.show() - }) - - it('should not close modal when escape key is pressed with keyboard = false', done => { - fixtureEl.innerHTML = '' - - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - keyboard: false - }) - - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toBeTrue() - done() - }, 10) - } - - modalEl.addEventListener('shown.bs.modal', () => { - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' - - modalEl.dispatchEvent(keydownEscape) - shownCallback() - }) - - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('Should not hide a modal') - }) - - modal.show() - }) - - it('should not overflow when clicking outside of modal-content if backdrop = static', done => { - fixtureEl.innerHTML = '' - - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: 'static' - }) - - modalEl.addEventListener('shown.bs.modal', () => { - modalEl.click() - setTimeout(() => { - expect(modalEl.clientHeight).toEqual(modalEl.scrollHeight) - done() - }, 20) - }) - - modal.show() - }) - - it('should not queue multiple callbacks when clicking outside of modal-content and backdrop = static', done => { - fixtureEl.innerHTML = '' - - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: 'static' - }) - - modalEl.addEventListener('shown.bs.modal', () => { - const spy = spyOn(modal, '_queueCallback').and.callThrough() - - modalEl.click() - modalEl.click() - - setTimeout(() => { - expect(spy).toHaveBeenCalledTimes(1) - done() - }, 20) - }) - - modal.show() - }) - - it('should trap focus', done => { - fixtureEl.innerHTML = '' - - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - - spyOn(modal._focustrap, 'activate').and.callThrough() - - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal._focustrap.activate).toHaveBeenCalled() - done() - }) - - modal.show() }) }) describe('hide', () => { - it('should hide a modal', done => { - fixtureEl.innerHTML = '' + it('should hide a modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - const backdropSpy = spyOn(modal._backdrop, 'hide').and.callThrough() + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + const backdropSpy = spyOn(modal._backdrop, 'hide').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - modal.hide() + modalEl.addEventListener('shown.bs.modal', () => { + modal.hide() + }) + + modalEl.addEventListener('hide.bs.modal', event => { + expect(event).toBeDefined() + }) + + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toBeNull() + expect(modalEl.getAttribute('role')).toBeNull() + expect(modalEl.getAttribute('aria-hidden')).toEqual('true') + expect(modalEl.style.display).toEqual('none') + expect(backdropSpy).toHaveBeenCalled() + resolve() + }) + + modal.show() }) - - modalEl.addEventListener('hide.bs.modal', event => { - expect(event).toBeDefined() - }) - - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toBeNull() - expect(modalEl.getAttribute('role')).toBeNull() - expect(modalEl.getAttribute('aria-hidden')).toEqual('true') - expect(modalEl.style.display).toEqual('none') - expect(backdropSpy).toHaveBeenCalled() - done() - }) - - modal.show() }) - it('should close modal when clicking outside of modal-content', done => { - fixtureEl.innerHTML = '' + it('should close modal when clicking outside of modal-content', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - modalEl.click() + modalEl.addEventListener('shown.bs.modal', () => { + modalEl.click() + }) + + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toBeNull() + expect(modalEl.getAttribute('role')).toBeNull() + expect(modalEl.getAttribute('aria-hidden')).toEqual('true') + expect(modalEl.style.display).toEqual('none') + expect(document.querySelector('.modal-backdrop')).toBeNull() + resolve() + }) + + modal.show() }) - - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toBeNull() - expect(modalEl.getAttribute('role')).toBeNull() - 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', () => { @@ -703,52 +751,56 @@ describe('Modal', () => { expect().nothing() }) - it('should not hide a modal if hide is prevented', done => { - fixtureEl.innerHTML = '' + it('should not hide a modal if hide is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - modal.hide() + modalEl.addEventListener('shown.bs.modal', () => { + modal.hide() + }) + + const hideCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeTrue() + resolve() + }, 10) + } + + modalEl.addEventListener('hide.bs.modal', event => { + event.preventDefault() + hideCallback() + }) + + modalEl.addEventListener('hidden.bs.modal', () => { + throw new Error('should not trigger hidden') + }) + + modal.show() }) - - const hideCallback = () => { - setTimeout(() => { - expect(modal._isShown).toBeTrue() - done() - }, 10) - } - - modalEl.addEventListener('hide.bs.modal', event => { - event.preventDefault() - hideCallback() - }) - - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('should not trigger hidden') - }) - - modal.show() }) - it('should release focus trap', done => { - fixtureEl.innerHTML = '' + it('should release focus trap', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - spyOn(modal._focustrap, 'deactivate').and.callThrough() + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + spyOn(modal._focustrap, 'deactivate').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - modal.hide() + modalEl.addEventListener('shown.bs.modal', () => { + modal.hide() + }) + + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modal._focustrap.deactivate).toHaveBeenCalled() + resolve() + }) + + modal.show() }) - - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modal._focustrap.deactivate).toHaveBeenCalled() - done() - }) - - modal.show() }) }) @@ -789,246 +841,260 @@ describe('Modal', () => { }) describe('data-api', () => { - it('should toggle modal', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should toggle modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toEqual('true') - expect(modalEl.getAttribute('role')).toEqual('dialog') - expect(modalEl.getAttribute('aria-hidden')).toBeNull() - expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).not.toBeNull() - setTimeout(() => trigger.click(), 10) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toEqual('true') + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toBeNull() + expect(modalEl.style.display).toEqual('block') + expect(document.querySelector('.modal-backdrop')).not.toBeNull() + setTimeout(() => trigger.click(), 10) + }) + + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toBeNull() + expect(modalEl.getAttribute('role')).toBeNull() + expect(modalEl.getAttribute('aria-hidden')).toEqual('true') + expect(modalEl.style.display).toEqual('none') + expect(document.querySelector('.modal-backdrop')).toBeNull() + resolve() + }) + + trigger.click() }) - - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toBeNull() - expect(modalEl.getAttribute('role')).toBeNull() - expect(modalEl.getAttribute('aria-hidden')).toEqual('true') - expect(modalEl.style.display).toEqual('none') - expect(document.querySelector('.modal-backdrop')).toBeNull() - done() - }) - - trigger.click() }) - it('should not recreate a new modal', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should not recreate a new modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - spyOn(modal, 'show').and.callThrough() + spyOn(modal, 'show').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal.show).toHaveBeenCalled() - done() + modalEl.addEventListener('shown.bs.modal', () => { + expect(modal.show).toHaveBeenCalled() + resolve() + }) + + trigger.click() }) - - trigger.click() }) - it('should prevent default when the trigger is or ', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should prevent default when the trigger is or ', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - spyOn(Event.prototype, 'preventDefault').and.callThrough() + spyOn(Event.prototype, 'preventDefault').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toEqual('true') - expect(modalEl.getAttribute('role')).toEqual('dialog') - expect(modalEl.getAttribute('aria-hidden')).toBeNull() - expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).not.toBeNull() - expect(Event.prototype.preventDefault).toHaveBeenCalled() - done() + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toEqual('true') + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toBeNull() + expect(modalEl.style.display).toEqual('block') + expect(document.querySelector('.modal-backdrop')).not.toBeNull() + expect(Event.prototype.preventDefault).toHaveBeenCalled() + resolve() + }) + + trigger.click() }) - - trigger.click() }) - it('should focus the trigger on hide', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should focus the trigger on hide', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - spyOn(trigger, 'focus') + spyOn(trigger, 'focus') - modalEl.addEventListener('shown.bs.modal', () => { - const modal = Modal.getInstance(modalEl) + modalEl.addEventListener('shown.bs.modal', () => { + const modal = Modal.getInstance(modalEl) - modal.hide() + modal.hide() + }) + + const hideListener = () => { + setTimeout(() => { + expect(trigger.focus).toHaveBeenCalled() + resolve() + }, 20) + } + + modalEl.addEventListener('hidden.bs.modal', () => { + hideListener() + }) + + trigger.click() }) + }) + it('should not prevent default when a click occurred on data-bs-dismiss="modal" where tagName is DIFFERENT than or ', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const hideListener = () => { - setTimeout(() => { - expect(trigger.focus).toHaveBeenCalled() - done() - }, 20) - } + const modalEl = fixtureEl.querySelector('.modal') + const btnClose = fixtureEl.querySelector('button[data-bs-dismiss="modal"]') + const modal = new Modal(modalEl) - modalEl.addEventListener('hidden.bs.modal', () => { - hideListener() + spyOn(Event.prototype, 'preventDefault').and.callThrough() + + modalEl.addEventListener('shown.bs.modal', () => { + btnClose.click() + }) + + modalEl.addEventListener('hidden.bs.modal', () => { + expect(Event.prototype.preventDefault).not.toHaveBeenCalled() + resolve() + }) + + modal.show() }) - - trigger.click() }) - it('should not prevent default when a click occurred on data-bs-dismiss="modal" where tagName is DIFFERENT than or ', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should prevent default when a click occurred on data-bs-dismiss="modal" where tagName is or ', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const btnClose = fixtureEl.querySelector('button[data-bs-dismiss="modal"]') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const btnClose = fixtureEl.querySelector('a[data-bs-dismiss="modal"]') + const modal = new Modal(modalEl) - spyOn(Event.prototype, 'preventDefault').and.callThrough() + spyOn(Event.prototype, 'preventDefault').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - btnClose.click() + modalEl.addEventListener('shown.bs.modal', () => { + btnClose.click() + }) + + modalEl.addEventListener('hidden.bs.modal', () => { + expect(Event.prototype.preventDefault).toHaveBeenCalled() + resolve() + }) + + modal.show() }) + }) + it('should not focus the trigger if the modal is not visible', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - modalEl.addEventListener('hidden.bs.modal', () => { - expect(Event.prototype.preventDefault).not.toHaveBeenCalled() - done() + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-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() + resolve() + }, 20) + } + + modalEl.addEventListener('hidden.bs.modal', () => { + hideListener() + }) + + trigger.click() }) + }) + it('should not focus the trigger if the modal is not shown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - modal.show() + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + + spyOn(trigger, 'focus') + + const showListener = () => { + setTimeout(() => { + expect(trigger.focus).not.toHaveBeenCalled() + resolve() + }, 10) + } + + modalEl.addEventListener('show.bs.modal', event => { + event.preventDefault() + showListener() + }) + + trigger.click() + }) }) - it('should prevent default when a click occurred on data-bs-dismiss="modal" where tagName is or ', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should call hide first, if another modal is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '', + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const btnClose = fixtureEl.querySelector('a[data-bs-dismiss="modal"]') - const modal = new Modal(modalEl) + const trigger2 = fixtureEl.querySelector('button') + const modalEl1 = document.querySelector('#modal1') + const modalEl2 = document.querySelector('#modal2') + const modal1 = new Modal(modalEl1) - spyOn(Event.prototype, 'preventDefault').and.callThrough() - - modalEl.addEventListener('shown.bs.modal', () => { - btnClose.click() + modalEl1.addEventListener('shown.bs.modal', () => { + trigger2.click() + }) + modalEl1.addEventListener('hidden.bs.modal', () => { + expect(Modal.getInstance(modalEl2)).not.toBeNull() + expect(modalEl2).toHaveClass('show') + resolve() + }) + modal1.show() }) - - modalEl.addEventListener('hidden.bs.modal', () => { - expect(Event.prototype.preventDefault).toHaveBeenCalled() - done() - }) - - modal.show() - }) - - it('should not focus the trigger if the modal is not visible', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') - - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-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 = [ - '', - '' - ].join('') - - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - - spyOn(trigger, 'focus') - - const showListener = () => { - setTimeout(() => { - expect(trigger.focus).not.toHaveBeenCalled() - done() - }, 10) - } - - modalEl.addEventListener('show.bs.modal', event => { - event.preventDefault() - showListener() - }) - - trigger.click() - }) - - it('should call hide first, if another modal is open', done => { - fixtureEl.innerHTML = [ - '', - '', - '' - ].join('') - - const trigger2 = fixtureEl.querySelector('button') - const modalEl1 = document.querySelector('#modal1') - const modalEl2 = document.querySelector('#modal2') - const modal1 = new Modal(modalEl1) - - modalEl1.addEventListener('shown.bs.modal', () => { - trigger2.click() - }) - modalEl1.addEventListener('hidden.bs.modal', () => { - expect(Modal.getInstance(modalEl2)).not.toBeNull() - expect(modalEl2).toHaveClass('show') - done() - }) - modal1.show() }) }) - describe('jQueryInterface', () => { it('should create a modal', () => { fixtureEl.innerHTML = '' diff --git a/js/tests/unit/offcanvas.spec.js b/js/tests/unit/offcanvas.spec.js index 36ef45dceb..852ffa5560 100644 --- a/js/tests/unit/offcanvas.spec.js +++ b/js/tests/unit/offcanvas.spec.js @@ -147,83 +147,91 @@ describe('Offcanvas', () => { }) describe('options', () => { - it('if scroll is enabled, should allow body to scroll while offcanvas is open', done => { - fixtureEl.innerHTML = '
' + it('if scroll is enabled, should allow body to scroll while offcanvas is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() - spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl, { scroll: true }) + spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() + spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { scroll: true }) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.hide).not.toHaveBeenCalled() - offCanvas.hide() + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(ScrollBarHelper.prototype.hide).not.toHaveBeenCalled() + offCanvas.hide() + }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(ScrollBarHelper.prototype.reset).not.toHaveBeenCalled() + resolve() + }) + offCanvas.show() }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.reset).not.toHaveBeenCalled() - done() - }) - offCanvas.show() }) - it('if scroll is disabled, should call ScrollBarHelper to handle scrollBar on body', done => { - fixtureEl.innerHTML = '
' + it('if scroll is disabled, should call ScrollBarHelper to handle scrollBar on body', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() - spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl, { scroll: false }) + spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() + spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { scroll: false }) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled() - offCanvas.hide() + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled() + offCanvas.hide() + }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled() + resolve() + }) + offCanvas.show() }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled() - done() - }) - offCanvas.show() }) - it('should hide a shown element if user click on backdrop', done => { - fixtureEl.innerHTML = '
' + it('should hide a shown element if user click on backdrop', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl, { backdrop: true }) + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl, { backdrop: true }) - const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true }) - spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough() + const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true }) + spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvas._backdrop._config.clickCallback).toEqual(jasmine.any(Function)) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvas._backdrop._config.clickCallback).toEqual(jasmine.any(Function)) - offCanvas._backdrop._getElement().dispatchEvent(clickEvent) + offCanvas._backdrop._getElement().dispatchEvent(clickEvent) + }) + + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvas._backdrop._config.clickCallback).toHaveBeenCalled() + resolve() + }) + + offCanvas.show() }) - - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvas._backdrop._config.clickCallback).toHaveBeenCalled() - done() - }) - - offCanvas.show() }) - it('should not trap focus if scroll is allowed', done => { - fixtureEl.innerHTML = '
' + it('should not trap focus if scroll is allowed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl, { - scroll: true + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { + scroll: true + }) + + spyOn(offCanvas._focustrap, 'activate').and.callThrough() + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvas._focustrap.activate).not.toHaveBeenCalled() + resolve() + }) + + offCanvas.show() }) - - spyOn(offCanvas._focustrap, 'activate').and.callThrough() - - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvas._focustrap.activate).not.toHaveBeenCalled() - done() - }) - - offCanvas.show() }) }) @@ -241,44 +249,48 @@ describe('Offcanvas', () => { expect(offCanvas.show).toHaveBeenCalled() }) - it('should call hide method if show class is present', done => { - fixtureEl.innerHTML = '
' + it('should call hide method if show class is present', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl) + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvasEl).toHaveClass('show') - spyOn(offCanvas, 'hide') + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).toHaveClass('show') + spyOn(offCanvas, 'hide') - offCanvas.toggle() + offCanvas.toggle() - expect(offCanvas.hide).toHaveBeenCalled() - done() + expect(offCanvas.hide).toHaveBeenCalled() + resolve() + }) + + offCanvas.show() }) - - offCanvas.show() }) }) describe('show', () => { - it('should add `showing` class during opening and `show` class on end', done => { - fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl) + it('should add `showing` class during opening and `show` class on end', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) - offCanvasEl.addEventListener('show.bs.offcanvas', () => { - expect(offCanvasEl).not.toHaveClass('show') + offCanvasEl.addEventListener('show.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('show') + }) + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('showing') + expect(offCanvasEl).toHaveClass('show') + resolve() + }) + + offCanvas.show() + expect(offCanvasEl).toHaveClass('showing') }) - - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvasEl).not.toHaveClass('showing') - expect(offCanvasEl).toHaveClass('show') - done() - }) - - offCanvas.show() - expect(offCanvasEl).toHaveClass('showing') }) it('should do nothing if already shown', () => { @@ -298,104 +310,114 @@ describe('Offcanvas', () => { expect(offCanvas._backdrop.show).not.toHaveBeenCalled() }) - it('should show a hidden element', done => { - fixtureEl.innerHTML = '
' + it('should show a hidden element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'show').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + spyOn(offCanvas._backdrop, 'show').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvasEl).toHaveClass('show') - expect(offCanvas._backdrop.show).toHaveBeenCalled() - done() + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).toHaveClass('show') + expect(offCanvas._backdrop.show).toHaveBeenCalled() + resolve() + }) + + offCanvas.show() }) - - offCanvas.show() }) - it('should not fire shown when show is prevented', done => { - fixtureEl.innerHTML = '
' + it('should not fire shown when show is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'show').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + spyOn(offCanvas._backdrop, 'show').and.callThrough() - const expectEnd = () => { - setTimeout(() => { - expect(offCanvas._backdrop.show).not.toHaveBeenCalled() - done() - }, 10) - } + const expectEnd = () => { + setTimeout(() => { + expect(offCanvas._backdrop.show).not.toHaveBeenCalled() + resolve() + }, 10) + } - offCanvasEl.addEventListener('show.bs.offcanvas', event => { - event.preventDefault() - expectEnd() + offCanvasEl.addEventListener('show.bs.offcanvas', event => { + event.preventDefault() + expectEnd() + }) + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + throw new Error('should not fire shown event') + }) + + offCanvas.show() }) - - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - throw new Error('should not fire shown event') - }) - - offCanvas.show() }) - it('on window load, should make visible an offcanvas element, if its markup contains class "show"', done => { - fixtureEl.innerHTML = '
' + it('on window load, should make visible an offcanvas element, if its markup contains class "show"', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - spyOn(Offcanvas.prototype, 'show').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('div') + spyOn(Offcanvas.prototype, 'show').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - done() + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + resolve() + }) + + window.dispatchEvent(createEvent('load')) + + const instance = Offcanvas.getInstance(offCanvasEl) + expect(instance).not.toBeNull() + expect(Offcanvas.prototype.show).toHaveBeenCalled() }) - - window.dispatchEvent(createEvent('load')) - - const instance = Offcanvas.getInstance(offCanvasEl) - expect(instance).not.toBeNull() - expect(Offcanvas.prototype.show).toHaveBeenCalled() }) - it('should trap focus', done => { - fixtureEl.innerHTML = '
' + it('should trap focus', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl) + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._focustrap, 'activate').and.callThrough() + spyOn(offCanvas._focustrap, 'activate').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvas._focustrap.activate).toHaveBeenCalled() - done() + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvas._focustrap.activate).toHaveBeenCalled() + resolve() + }) + + offCanvas.show() }) - - offCanvas.show() }) }) describe('hide', () => { - it('should add `hiding` class during closing and remover `show` & `hiding` classes on end', done => { - fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl) + it('should add `hiding` class during closing and remover `show` & `hiding` classes on end', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) - offCanvasEl.addEventListener('hide.bs.offcanvas', () => { - expect(offCanvasEl).not.toHaveClass('showing') - expect(offCanvasEl).toHaveClass('show') - }) + offCanvasEl.addEventListener('hide.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('showing') + expect(offCanvasEl).toHaveClass('show') + }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvasEl).not.toHaveClass('hiding') - expect(offCanvasEl).not.toHaveClass('show') - done() - }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('hiding') + expect(offCanvasEl).not.toHaveClass('show') + resolve() + }) - offCanvas.show() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - offCanvas.hide() - expect(offCanvasEl).not.toHaveClass('showing') - expect(offCanvasEl).toHaveClass('hiding') + offCanvas.show() + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + offCanvas.hide() + expect(offCanvasEl).not.toHaveClass('showing') + expect(offCanvasEl).toHaveClass('hiding') + }) }) }) @@ -413,65 +435,71 @@ describe('Offcanvas', () => { expect(EventHandler.trigger).not.toHaveBeenCalled() }) - it('should hide a shown element', done => { - fixtureEl.innerHTML = '
' + it('should hide a shown element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'hide').and.callThrough() - offCanvas.show() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + spyOn(offCanvas._backdrop, 'hide').and.callThrough() + offCanvas.show() - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvasEl).not.toHaveClass('show') - expect(offCanvas._backdrop.hide).toHaveBeenCalled() - done() + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('show') + expect(offCanvas._backdrop.hide).toHaveBeenCalled() + resolve() + }) + + offCanvas.hide() }) - - offCanvas.hide() }) - it('should not fire hidden when hide is prevented', done => { - fixtureEl.innerHTML = '
' + it('should not fire hidden when hide is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'hide').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + spyOn(offCanvas._backdrop, 'hide').and.callThrough() - offCanvas.show() + offCanvas.show() - const expectEnd = () => { - setTimeout(() => { - expect(offCanvas._backdrop.hide).not.toHaveBeenCalled() - done() - }, 10) - } + const expectEnd = () => { + setTimeout(() => { + expect(offCanvas._backdrop.hide).not.toHaveBeenCalled() + resolve() + }, 10) + } - offCanvasEl.addEventListener('hide.bs.offcanvas', event => { - event.preventDefault() - expectEnd() + offCanvasEl.addEventListener('hide.bs.offcanvas', event => { + event.preventDefault() + expectEnd() + }) + + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + throw new Error('should not fire hidden event') + }) + + offCanvas.hide() }) - - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - throw new Error('should not fire hidden event') - }) - - offCanvas.hide() }) - it('should release focus trap', done => { - fixtureEl.innerHTML = '
' + it('should release focus trap', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._focustrap, 'deactivate').and.callThrough() - offCanvas.show() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + spyOn(offCanvas._focustrap, 'deactivate').and.callThrough() + offCanvas.show() - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvas._focustrap.deactivate).toHaveBeenCalled() - done() + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvas._focustrap.deactivate).toHaveBeenCalled() + resolve() + }) + + offCanvas.hide() }) - - offCanvas.hide() }) }) @@ -501,22 +529,24 @@ describe('Offcanvas', () => { }) describe('data-api', () => { - it('should not prevent event for input', done => { - fixtureEl.innerHTML = [ - '', - '
' - ].join('') + it('should not prevent event for input', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') - const target = fixtureEl.querySelector('input') - const offCanvasEl = fixtureEl.querySelector('#offcanvasdiv1') + const target = fixtureEl.querySelector('input') + const offCanvasEl = fixtureEl.querySelector('#offcanvasdiv1') - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvasEl).toHaveClass('show') - expect(target.checked).toBeTrue() - done() + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).toHaveClass('show') + expect(target.checked).toBeTrue() + resolve() + }) + + target.click() }) - - target.click() }) it('should not call toggle on disabled elements', () => { @@ -534,76 +564,82 @@ describe('Offcanvas', () => { expect(Offcanvas.prototype.toggle).not.toHaveBeenCalled() }) - it('should call hide first, if another offcanvas is open', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
' - ].join('') + it('should call hide first, if another offcanvas is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
' + ].join('') - const trigger2 = fixtureEl.querySelector('#btn2') - const offcanvasEl1 = document.querySelector('#offcanvas1') - const offcanvasEl2 = document.querySelector('#offcanvas2') - const offcanvas1 = new Offcanvas(offcanvasEl1) + const trigger2 = fixtureEl.querySelector('#btn2') + const offcanvasEl1 = document.querySelector('#offcanvas1') + const offcanvasEl2 = document.querySelector('#offcanvas2') + const offcanvas1 = new Offcanvas(offcanvasEl1) - offcanvasEl1.addEventListener('shown.bs.offcanvas', () => { - trigger2.click() + offcanvasEl1.addEventListener('shown.bs.offcanvas', () => { + trigger2.click() + }) + offcanvasEl1.addEventListener('hidden.bs.offcanvas', () => { + expect(Offcanvas.getInstance(offcanvasEl2)).not.toBeNull() + resolve() + }) + offcanvas1.show() }) - offcanvasEl1.addEventListener('hidden.bs.offcanvas', () => { - expect(Offcanvas.getInstance(offcanvasEl2)).not.toBeNull() - done() - }) - offcanvas1.show() }) - it('should focus on trigger element after closing offcanvas', done => { - fixtureEl.innerHTML = [ - '', - '
' - ].join('') + it('should focus on trigger element after closing offcanvas', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') - const trigger = fixtureEl.querySelector('#btn') - const offcanvasEl = fixtureEl.querySelector('#offcanvas') - const offcanvas = new Offcanvas(offcanvasEl) - spyOn(trigger, 'focus') + const trigger = fixtureEl.querySelector('#btn') + const offcanvasEl = fixtureEl.querySelector('#offcanvas') + const offcanvas = new Offcanvas(offcanvasEl) + spyOn(trigger, 'focus') - offcanvasEl.addEventListener('shown.bs.offcanvas', () => { - offcanvas.hide() + offcanvasEl.addEventListener('shown.bs.offcanvas', () => { + offcanvas.hide() + }) + offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { + setTimeout(() => { + expect(trigger.focus).toHaveBeenCalled() + resolve() + }, 5) + }) + + trigger.click() }) - offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { - setTimeout(() => { - expect(trigger.focus).toHaveBeenCalled() - done() - }, 5) - }) - - trigger.click() }) - it('should not focus on trigger element after closing offcanvas, if it is not visible', done => { - fixtureEl.innerHTML = [ - '', - '
' - ].join('') + it('should not focus on trigger element after closing offcanvas, if it is not visible', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') - const trigger = fixtureEl.querySelector('#btn') - const offcanvasEl = fixtureEl.querySelector('#offcanvas') - const offcanvas = new Offcanvas(offcanvasEl) - spyOn(trigger, 'focus') + const trigger = fixtureEl.querySelector('#btn') + const offcanvasEl = fixtureEl.querySelector('#offcanvas') + const offcanvas = new Offcanvas(offcanvasEl) + spyOn(trigger, 'focus') - offcanvasEl.addEventListener('shown.bs.offcanvas', () => { - trigger.style.display = 'none' - offcanvas.hide() + offcanvasEl.addEventListener('shown.bs.offcanvas', () => { + trigger.style.display = 'none' + offcanvas.hide() + }) + offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { + setTimeout(() => { + expect(isVisible(trigger)).toBeFalse() + expect(trigger.focus).not.toHaveBeenCalled() + resolve() + }, 5) + }) + + trigger.click() }) - offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { - setTimeout(() => { - expect(isVisible(trigger)).toBeFalse() - expect(trigger.focus).not.toHaveBeenCalled() - done() - }, 5) - }) - - trigger.click() }) }) diff --git a/js/tests/unit/popover.spec.js b/js/tests/unit/popover.spec.js index a04bd21c60..7bbd52b1db 100644 --- a/js/tests/unit/popover.spec.js +++ b/js/tests/unit/popover.spec.js @@ -62,113 +62,125 @@ describe('Popover', () => { }) describe('show', () => { - it('should show a popover', done => { - fixtureEl.innerHTML = 'BS twitter' + it('should show a popover', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'BS twitter' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl) + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl) - popoverEl.addEventListener('shown.bs.popover', () => { - expect(document.querySelector('.popover')).not.toBeNull() - done() + popoverEl.addEventListener('shown.bs.popover', () => { + expect(document.querySelector('.popover')).not.toBeNull() + resolve() + }) + + popover.show() }) - - popover.show() }) - it('should set title and content from functions', done => { - fixtureEl.innerHTML = 'BS twitter' + it('should set title and content from functions', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'BS twitter' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl, { - title: () => 'Bootstrap', - content: () => 'loves writing tests (╯°□°)╯︵ ┻━┻' + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + title: () => 'Bootstrap', + content: () => 'loves writing tests (╯°□°)╯︵ ┻━┻' + }) + + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') + + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Bootstrap') + expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('loves writing tests (╯°□°)╯︵ ┻━┻') + resolve() + }) + + popover.show() }) - - popoverEl.addEventListener('shown.bs.popover', () => { - const popoverDisplayed = document.querySelector('.popover') - - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Bootstrap') - expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('loves writing tests (╯°□°)╯︵ ┻━┻') - done() - }) - - popover.show() }) - it('should show a popover with just content without having header', done => { - fixtureEl.innerHTML = 'Nice link' + it('should show a popover with just content without having header', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'Nice link' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl, { - content: 'Some beautiful content :)' + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + content: 'Some beautiful content :)' + }) + + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') + + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-header')).toBeNull() + expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Some beautiful content :)') + resolve() + }) + + popover.show() }) - - popoverEl.addEventListener('shown.bs.popover', () => { - const popoverDisplayed = document.querySelector('.popover') - - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-header')).toBeNull() - expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Some beautiful content :)') - done() - }) - - popover.show() }) - it('should show a popover with just title without having body', done => { - fixtureEl.innerHTML = 'Nice link' + it('should show a popover with just title without having body', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'Nice link' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl, { - title: 'Title which does not require content' + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + title: 'Title which does not require content' + }) + + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') + + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-body')).toBeNull() + expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Title which does not require content') + resolve() + }) + + popover.show() }) - - popoverEl.addEventListener('shown.bs.popover', () => { - const popoverDisplayed = document.querySelector('.popover') - - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-body')).toBeNull() - expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Title which does not require content') - done() - }) - - popover.show() }) - it('should show a popover with just title without having body using data-attribute to get config', done => { - fixtureEl.innerHTML = 'Nice link' + it('should show a popover with just title without having body using data-attribute to get config', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'Nice link' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl) + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl) - popoverEl.addEventListener('shown.bs.popover', () => { - const popoverDisplayed = document.querySelector('.popover') + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-body')).toBeNull() - expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Title which does not require content') - done() + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-body')).toBeNull() + expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Title which does not require content') + resolve() + }) + + popover.show() }) - - popover.show() }) - it('should NOT show a popover without `title` and `content`', done => { - fixtureEl.innerHTML = 'Nice link' + it('should NOT show a popover without `title` and `content`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'Nice link' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl, { animation: false }) - spyOn(EventHandler, 'trigger').and.callThrough() + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { animation: false }) + spyOn(EventHandler, 'trigger').and.callThrough() - setTimeout(() => { - expect(EventHandler.trigger).not.toHaveBeenCalled() - expect(document.querySelector('.popover')).toBeNull() - done() + setTimeout(() => { + expect(EventHandler.trigger).not.toHaveBeenCalled() + expect(document.querySelector('.popover')).toBeNull() + resolve() + }) + + popover.show() }) - - popover.show() }) it('"setContent" should keep the initial template', () => { @@ -187,72 +199,78 @@ describe('Popover', () => { expect(tip.querySelector('.popover-body')).not.toBeNull() }) - it('should call setContent once', done => { - fixtureEl.innerHTML = 'BS twitter' + it('should call setContent once', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'BS twitter' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl, { - content: 'Popover content' - }) - expect(popover._templateFactory).toBeNull() - let spy = null - let times = 1 + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + content: 'Popover content' + }) + expect(popover._templateFactory).toBeNull() + let spy = null + let times = 1 - popoverEl.addEventListener('hidden.bs.popover', () => { + popoverEl.addEventListener('hidden.bs.popover', () => { + popover.show() + }) + + popoverEl.addEventListener('shown.bs.popover', () => { + spy = spy || spyOn(popover._templateFactory, 'constructor').and.callThrough() + const popoverDisplayed = document.querySelector('.popover') + + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Popover content') + expect(spy).toHaveBeenCalledTimes(0) + if (times > 1) { + resolve() + } + + times++ + popover.hide() + }) popover.show() }) - - popoverEl.addEventListener('shown.bs.popover', () => { - spy = spy || spyOn(popover._templateFactory, 'constructor').and.callThrough() - const popoverDisplayed = document.querySelector('.popover') - - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Popover content') - expect(spy).toHaveBeenCalledTimes(0) - if (times > 1) { - done() - } - - times++ - popover.hide() - }) - popover.show() }) - it('should show a popover with provided custom class', done => { - fixtureEl.innerHTML = 'BS twitter' + it('should show a popover with provided custom class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'BS twitter' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl) + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl) - popoverEl.addEventListener('shown.bs.popover', () => { - const tip = document.querySelector('.popover') - expect(tip).not.toBeNull() - expect(tip).toHaveClass('custom-class') - done() + popoverEl.addEventListener('shown.bs.popover', () => { + const tip = document.querySelector('.popover') + expect(tip).not.toBeNull() + expect(tip).toHaveClass('custom-class') + resolve() + }) + + popover.show() }) - - popover.show() }) }) describe('hide', () => { - it('should hide a popover', done => { - fixtureEl.innerHTML = 'BS twitter' + it('should hide a popover', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'BS twitter' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl) + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl) - popoverEl.addEventListener('shown.bs.popover', () => { - popover.hide() + popoverEl.addEventListener('shown.bs.popover', () => { + popover.hide() + }) + + popoverEl.addEventListener('hidden.bs.popover', () => { + expect(document.querySelector('.popover')).toBeNull() + resolve() + }) + + popover.show() }) - - popoverEl.addEventListener('hidden.bs.popover', () => { - expect(document.querySelector('.popover')).toBeNull() - done() - }) - - popover.show() }) }) diff --git a/js/tests/unit/scrollspy.spec.js b/js/tests/unit/scrollspy.spec.js index 5c044e697a..291654d9be 100644 --- a/js/tests/unit/scrollspy.spec.js +++ b/js/tests/unit/scrollspy.spec.js @@ -1,6 +1,6 @@ import ScrollSpy from '../../src/scrollspy' import Manipulator from '../../src/dom/manipulator' -import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture' +import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture' describe('ScrollSpy', () => { let fixtureEl @@ -85,418 +85,436 @@ describe('ScrollSpy', () => { expect(scrollSpy._targets).toHaveSize(2) }) - it('should only switch "active" class on current target', done => { - fixtureEl.innerHTML = [ - '
', - '
', - '
', - '
', - ' ', - '
', - '
', - '
', - '
', - '
', - '

Overview

', - '

', - '
', - '
', - '

Detail

', - '

', - '
', - '
', - '
' - ].join('') + it('should only switch "active" class on current target', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + '
', + '
', + '
', + ' ', + '
', + '
', + '
', + '
', + '
', + '

Overview

', + '

', + '
', + '
', + '

Detail

', + '

', + '
', + '
', + '
' + ].join('') - const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example') - const rootEl = fixtureEl.querySelector('#root') - const scrollSpy = new ScrollSpy(scrollSpyEl, { - target: 'ss-target' - }) + const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example') + const rootEl = fixtureEl.querySelector('#root') + const scrollSpy = new ScrollSpy(scrollSpyEl, { + target: 'ss-target' + }) - spyOn(scrollSpy, '_process').and.callThrough() + spyOn(scrollSpy, '_process').and.callThrough() - scrollSpyEl.addEventListener('scroll', () => { - expect(rootEl).toHaveClass('active') - expect(scrollSpy._process).toHaveBeenCalled() - done() - }) + scrollSpyEl.addEventListener('scroll', () => { + expect(rootEl).toHaveClass('active') + expect(scrollSpy._process).toHaveBeenCalled() + resolve() + }) - scrollSpyEl.scrollTop = 350 - }) - - it('should only switch "active" class on current target specified w element', done => { - fixtureEl.innerHTML = [ - '
', - '
', - '
', - '
', - ' ', - '
', - '
', - '
', - '
', - '
', - '

Overview

', - '

', - '
', - '
', - '

Detail

', - '

', - '
', - '
', - '
' - ].join('') - - const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example') - const rootEl = fixtureEl.querySelector('#root') - const scrollSpy = new ScrollSpy(scrollSpyEl, { - target: fixtureEl.querySelector('#ss-target') - }) - - spyOn(scrollSpy, '_process').and.callThrough() - - scrollSpyEl.addEventListener('scroll', () => { - expect(rootEl).toHaveClass('active') - expect(scrollSpy._process).toHaveBeenCalled() - done() - }) - - scrollSpyEl.scrollTop = 350 - }) - - it('should correctly select middle navigation option when large offset is used', done => { - fixtureEl.innerHTML = [ - '', - '', - '
', - '
', - '
', - '
', - '
' - ].join('') - - const contentEl = fixtureEl.querySelector('#content') - const scrollSpy = new ScrollSpy(contentEl, { - target: '#navigation', - offset: Manipulator.position(contentEl).top - }) - - spyOn(scrollSpy, '_process').and.callThrough() - - contentEl.addEventListener('scroll', () => { - expect(fixtureEl.querySelector('#one-link')).not.toHaveClass('active') - expect(fixtureEl.querySelector('#two-link')).toHaveClass('active') - expect(fixtureEl.querySelector('#three-link')).not.toHaveClass('active') - expect(scrollSpy._process).toHaveBeenCalled() - done() - }) - - contentEl.scrollTop = 550 - }) - - it('should add the active class to the correct element', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
div 1
', - '
div 2
', - '
' - ].join('') - - const contentEl = fixtureEl.querySelector('.content') - const scrollSpy = new ScrollSpy(contentEl, { - offset: 0, - target: '.navbar' - }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() - - testElementIsActiveAfterScroll({ - elementSelector: '#a-1', - targetSelector: '#div-1', - contentEl, - scrollSpy, - spy, - cb: () => { - testElementIsActiveAfterScroll({ - elementSelector: '#a-2', - targetSelector: '#div-2', - contentEl, - scrollSpy, - spy, - cb: () => done() - }) - } + scrollSpyEl.scrollTop = 350 }) }) - it('should add the active class to the correct element (nav markup)', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
div 1
', - '
div 2
', - '
' - ].join('') + it('should only switch "active" class on current target specified w element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + '
', + '
', + '
', + ' ', + '
', + '
', + '
', + '
', + '
', + '

Overview

', + '

', + '
', + '
', + '

Detail

', + '

', + '
', + '
', + '
' + ].join('') - const contentEl = fixtureEl.querySelector('.content') - const scrollSpy = new ScrollSpy(contentEl, { - offset: 0, - target: '.navbar' - }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() + const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example') + const rootEl = fixtureEl.querySelector('#root') + const scrollSpy = new ScrollSpy(scrollSpyEl, { + target: fixtureEl.querySelector('#ss-target') + }) - testElementIsActiveAfterScroll({ - elementSelector: '#a-1', - targetSelector: '#div-1', - contentEl, - scrollSpy, - spy, - cb: () => { - testElementIsActiveAfterScroll({ - elementSelector: '#a-2', - targetSelector: '#div-2', - contentEl, - scrollSpy, - spy, - cb: () => done() - }) - } + spyOn(scrollSpy, '_process').and.callThrough() + + scrollSpyEl.addEventListener('scroll', () => { + expect(rootEl).toHaveClass('active') + expect(scrollSpy._process).toHaveBeenCalled() + resolve() + }) + + scrollSpyEl.scrollTop = 350 }) }) - it('should add the active class to the correct element (list-group markup)', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
div 1
', - '
div 2
', - '
' - ].join('') + it('should correctly select middle navigation option when large offset is used', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '', + '
', + '
', + '
', + '
', + '
' + ].join('') - const contentEl = fixtureEl.querySelector('.content') - const scrollSpy = new ScrollSpy(contentEl, { - offset: 0, - target: '.navbar' - }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() + const contentEl = fixtureEl.querySelector('#content') + const scrollSpy = new ScrollSpy(contentEl, { + target: '#navigation', + offset: Manipulator.position(contentEl).top + }) - testElementIsActiveAfterScroll({ - elementSelector: '#a-1', - targetSelector: '#div-1', - contentEl, - scrollSpy, - spy, - cb: () => { - testElementIsActiveAfterScroll({ - elementSelector: '#a-2', - targetSelector: '#div-2', - contentEl, - scrollSpy, - spy, - cb: () => done() - }) - } + spyOn(scrollSpy, '_process').and.callThrough() + + contentEl.addEventListener('scroll', () => { + expect(fixtureEl.querySelector('#one-link')).not.toHaveClass('active') + expect(fixtureEl.querySelector('#two-link')).toHaveClass('active') + expect(fixtureEl.querySelector('#three-link')).not.toHaveClass('active') + expect(scrollSpy._process).toHaveBeenCalled() + resolve() + }) + + contentEl.scrollTop = 550 }) }) - it('should clear selection if above the first section', done => { - fixtureEl.innerHTML = [ - '', - '', - '
', - '
', - '
', - '
', - '
', - '
', - '
' - ].join('') + it('should add the active class to the correct element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
div 1
', + '
div 2
', + '
' + ].join('') - const contentEl = fixtureEl.querySelector('#content') - const scrollSpy = new ScrollSpy(contentEl, { - target: '#navigation', - offset: Manipulator.position(contentEl).top + const contentEl = fixtureEl.querySelector('.content') + const scrollSpy = new ScrollSpy(contentEl, { + offset: 0, + target: '.navbar' + }) + const spy = spyOn(scrollSpy, '_process').and.callThrough() + + testElementIsActiveAfterScroll({ + elementSelector: '#a-1', + targetSelector: '#div-1', + contentEl, + scrollSpy, + spy, + cb: () => { + testElementIsActiveAfterScroll({ + elementSelector: '#a-2', + targetSelector: '#div-2', + contentEl, + scrollSpy, + spy, + cb: resolve + }) + } + }) }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() - - let firstTime = true - - contentEl.addEventListener('scroll', () => { - const active = fixtureEl.querySelector('.active') - - expect(spy).toHaveBeenCalled() - spy.calls.reset() - if (firstTime) { - expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) - expect(active.getAttribute('id')).toEqual('two-link') - firstTime = false - contentEl.scrollTop = 0 - } else { - expect(active).toBeNull() - done() - } - }) - - contentEl.scrollTop = 201 }) - it('should not clear selection if above the first section and first section is at the top', done => { - fixtureEl.innerHTML = [ - '', - '', - '
', - '
', - '
', - '
', - '
', - '
' - ].join('') + it('should add the active class to the correct element (nav markup)', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
div 1
', + '
div 2
', + '
' + ].join('') - const negativeHeight = -10 - const startOfSectionTwo = 101 - const contentEl = fixtureEl.querySelector('#content') - const scrollSpy = new ScrollSpy(contentEl, { - target: '#navigation', - offset: contentEl.offsetTop + const contentEl = fixtureEl.querySelector('.content') + const scrollSpy = new ScrollSpy(contentEl, { + offset: 0, + target: '.navbar' + }) + const spy = spyOn(scrollSpy, '_process').and.callThrough() + + testElementIsActiveAfterScroll({ + elementSelector: '#a-1', + targetSelector: '#div-1', + contentEl, + scrollSpy, + spy, + cb: () => { + testElementIsActiveAfterScroll({ + elementSelector: '#a-2', + targetSelector: '#div-2', + contentEl, + scrollSpy, + spy, + cb: resolve + }) + } + }) }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() - - let firstTime = true - - contentEl.addEventListener('scroll', () => { - const active = fixtureEl.querySelector('.active') - - expect(spy).toHaveBeenCalled() - spy.calls.reset() - if (firstTime) { - expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) - expect(active.getAttribute('id')).toEqual('two-link') - firstTime = false - contentEl.scrollTop = negativeHeight - } else { - expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) - expect(active.getAttribute('id')).toEqual('one-link') - done() - } - }) - - contentEl.scrollTop = startOfSectionTwo }) - it('should correctly select navigation element on backward scrolling when each target section height is 100%', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
div 1
', - '
div 2
', - '
div 3
', - '
div 4
', - '
div 5
', - '
' - ].join('') + it('should add the active class to the correct element (list-group markup)', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
div 1
', + '
div 2
', + '
' + ].join('') - const contentEl = fixtureEl.querySelector('.content') - const scrollSpy = new ScrollSpy(contentEl, { - offset: 0, - target: '.navbar' + const contentEl = fixtureEl.querySelector('.content') + const scrollSpy = new ScrollSpy(contentEl, { + offset: 0, + target: '.navbar' + }) + const spy = spyOn(scrollSpy, '_process').and.callThrough() + + testElementIsActiveAfterScroll({ + elementSelector: '#a-1', + targetSelector: '#div-1', + contentEl, + scrollSpy, + spy, + cb: () => { + testElementIsActiveAfterScroll({ + elementSelector: '#a-2', + targetSelector: '#div-2', + contentEl, + scrollSpy, + spy, + cb: resolve + }) + } + }) }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() + }) - testElementIsActiveAfterScroll({ - elementSelector: '#li-100-5', - targetSelector: '#div-100-5', - scrollSpy, - spy, - contentEl, - cb() { - contentEl.scrollTop = 0 - testElementIsActiveAfterScroll({ - elementSelector: '#li-100-4', - targetSelector: '#div-100-4', - scrollSpy, - spy, - contentEl, - cb() { - contentEl.scrollTop = 0 - testElementIsActiveAfterScroll({ - elementSelector: '#li-100-3', - targetSelector: '#div-100-3', - scrollSpy, - spy, - contentEl, - cb() { - contentEl.scrollTop = 0 - testElementIsActiveAfterScroll({ - elementSelector: '#li-100-2', - targetSelector: '#div-100-2', - scrollSpy, - spy, - contentEl, - cb() { - contentEl.scrollTop = 0 - testElementIsActiveAfterScroll({ - elementSelector: '#li-100-1', - targetSelector: '#div-100-1', - scrollSpy, - spy, - contentEl, - cb: done - }) - } - }) - } - }) - } - }) - } + it('should clear selection if above the first section', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '', + '
', + '
', + '
', + '
', + '
', + '
', + '
' + ].join('') + + const contentEl = fixtureEl.querySelector('#content') + const scrollSpy = new ScrollSpy(contentEl, { + target: '#navigation', + offset: Manipulator.position(contentEl).top + }) + const spy = spyOn(scrollSpy, '_process').and.callThrough() + + let firstTime = true + + contentEl.addEventListener('scroll', () => { + const active = fixtureEl.querySelector('.active') + + expect(spy).toHaveBeenCalled() + spy.calls.reset() + if (firstTime) { + expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) + expect(active.getAttribute('id')).toEqual('two-link') + firstTime = false + contentEl.scrollTop = 0 + } else { + expect(active).toBeNull() + resolve() + } + }) + + contentEl.scrollTop = 201 + }) + }) + + it('should not clear selection if above the first section and first section is at the top', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '', + '
', + '
', + '
', + '
', + '
', + '
' + ].join('') + + const negativeHeight = -10 + const startOfSectionTwo = 101 + const contentEl = fixtureEl.querySelector('#content') + const scrollSpy = new ScrollSpy(contentEl, { + target: '#navigation', + offset: contentEl.offsetTop + }) + const spy = spyOn(scrollSpy, '_process').and.callThrough() + + let firstTime = true + + contentEl.addEventListener('scroll', () => { + const active = fixtureEl.querySelector('.active') + + expect(spy).toHaveBeenCalled() + spy.calls.reset() + if (firstTime) { + expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) + expect(active.getAttribute('id')).toEqual('two-link') + firstTime = false + contentEl.scrollTop = negativeHeight + } else { + expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) + expect(active.getAttribute('id')).toEqual('one-link') + resolve() + } + }) + + contentEl.scrollTop = startOfSectionTwo + }) + }) + + it('should correctly select navigation element on backward scrolling when each target section height is 100%', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
div 1
', + '
div 2
', + '
div 3
', + '
div 4
', + '
div 5
', + '
' + ].join('') + + const contentEl = fixtureEl.querySelector('.content') + const scrollSpy = new ScrollSpy(contentEl, { + offset: 0, + target: '.navbar' + }) + const spy = spyOn(scrollSpy, '_process').and.callThrough() + + testElementIsActiveAfterScroll({ + elementSelector: '#li-100-5', + targetSelector: '#div-100-5', + scrollSpy, + spy, + contentEl, + cb() { + contentEl.scrollTop = 0 + testElementIsActiveAfterScroll({ + elementSelector: '#li-100-4', + targetSelector: '#div-100-4', + scrollSpy, + spy, + contentEl, + cb() { + contentEl.scrollTop = 0 + testElementIsActiveAfterScroll({ + elementSelector: '#li-100-3', + targetSelector: '#div-100-3', + scrollSpy, + spy, + contentEl, + cb() { + contentEl.scrollTop = 0 + testElementIsActiveAfterScroll({ + elementSelector: '#li-100-2', + targetSelector: '#div-100-2', + scrollSpy, + spy, + contentEl, + cb() { + contentEl.scrollTop = 0 + testElementIsActiveAfterScroll({ + elementSelector: '#li-100-1', + targetSelector: '#div-100-1', + scrollSpy, + spy, + contentEl, + cb: resolve + }) + } + }) + } + }) + } + }) + } + }) }) }) diff --git a/js/tests/unit/tab.spec.js b/js/tests/unit/tab.spec.js index bafa085526..43adee53b2 100644 --- a/js/tests/unit/tab.spec.js +++ b/js/tests/unit/tab.spec.js @@ -1,5 +1,5 @@ import Tab from '../../src/tab' -import { getFixture, clearFixture, jQueryMock } from '../helpers/fixture' +import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture' describe('Tab', () => { let fixtureEl @@ -39,314 +39,336 @@ describe('Tab', () => { }) describe('show', () => { - it('should activate element by tab id (using buttons, the preferred semantic way)', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should activate element by tab id (using buttons, the preferred semantic way)', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile')).toHaveClass('active') - expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') - done() + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') + resolve() + }) + + tab.show() }) - - tab.show() }) - it('should activate element by tab id (using links for tabs - not ideal, but still supported)', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should activate element by tab id (using links for tabs - not ideal, but still supported)', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile')).toHaveClass('active') - expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') - done() + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') + resolve() + }) + + tab.show() }) - - tab.show() }) - it('should activate element by tab id in ordered list', done => { - fixtureEl.innerHTML = [ - '', - '
    ', - '
  1. ', - '
  2. ', - '
' - ].join('') + it('should activate element by tab id in ordered list', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
    ', + '
  1. ', + '
  2. ', + '
' + ].join('') - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile')).toHaveClass('active') - done() + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + resolve() + }) + + tab.show() }) - - tab.show() }) - it('should activate element by tab id in nav list', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
', - '
', - '
' - ].join('') + it('should activate element by tab id in nav list', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
', + '
', + '
' + ].join('') - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile')).toHaveClass('active') - done() + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + resolve() + }) + + tab.show() }) - - tab.show() }) - it('should activate element by tab id in list group', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - ' ', - '
', - '
', - '
', - '
', - '
' - ].join('') + it('should activate element by tab id in list group', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + ' ', + '
', + '
', + '
', + '
', + '
' + ].join('') - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile')).toHaveClass('active') - done() + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + resolve() + }) + + tab.show() }) - - tab.show() }) - it('should not fire shown when show is prevented', done => { - fixtureEl.innerHTML = '' + it('should not fire shown when show is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const navEl = fixtureEl.querySelector('div') - const tab = new Tab(navEl) - const expectDone = () => { + const navEl = fixtureEl.querySelector('div') + const tab = new Tab(navEl) + const expectDone = () => { + setTimeout(() => { + expect().nothing() + resolve() + }, 30) + } + + navEl.addEventListener('show.bs.tab', ev => { + ev.preventDefault() + expectDone() + }) + + navEl.addEventListener('shown.bs.tab', () => { + throw new Error('should not trigger shown event') + }) + + tab.show() + }) + }) + + it('should not fire shown when tab is already active', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
', + '
', + '
' + ].join('') + + const triggerActive = fixtureEl.querySelector('button.active') + const tab = new Tab(triggerActive) + + triggerActive.addEventListener('shown.bs.tab', () => { + throw new Error('should not trigger shown event') + }) + + tab.show() setTimeout(() => { expect().nothing() - done() + resolve() }, 30) - } - - navEl.addEventListener('show.bs.tab', ev => { - ev.preventDefault() - expectDone() }) - - navEl.addEventListener('shown.bs.tab', () => { - throw new Error('should not trigger shown event') - }) - - tab.show() }) - it('should not fire shown when tab is already active', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
', - '
', - '
' - ].join('') + it('show and shown events should reference correct relatedTarget', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
', + '
', + '
' + ].join('') - const triggerActive = fixtureEl.querySelector('button.active') - const tab = new Tab(triggerActive) + const secondTabTrigger = fixtureEl.querySelector('#triggerProfile') + const secondTab = new Tab(secondTabTrigger) - triggerActive.addEventListener('shown.bs.tab', () => { - throw new Error('should not trigger shown event') - }) + secondTabTrigger.addEventListener('show.bs.tab', ev => { + expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home') + }) - tab.show() - setTimeout(() => { - expect().nothing() - done() - }, 30) - }) + secondTabTrigger.addEventListener('shown.bs.tab', ev => { + expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home') + expect(secondTabTrigger.getAttribute('aria-selected')).toEqual('true') + expect(fixtureEl.querySelector('button:not(.active)').getAttribute('aria-selected')).toEqual('false') + resolve() + }) - it('show and shown events should reference correct relatedTarget', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
', - '
', - '
' - ].join('') - - const secondTabTrigger = fixtureEl.querySelector('#triggerProfile') - const secondTab = new Tab(secondTabTrigger) - - secondTabTrigger.addEventListener('show.bs.tab', ev => { - expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home') - }) - - secondTabTrigger.addEventListener('shown.bs.tab', ev => { - expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home') - expect(secondTabTrigger.getAttribute('aria-selected')).toEqual('true') - expect(fixtureEl.querySelector('button:not(.active)').getAttribute('aria-selected')).toEqual('false') - done() - }) - - secondTab.show() - }) - - it('should fire hide and hidden events', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const triggerList = fixtureEl.querySelectorAll('button') - const firstTab = new Tab(triggerList[0]) - const secondTab = new Tab(triggerList[1]) - - let hideCalled = false - triggerList[0].addEventListener('shown.bs.tab', () => { secondTab.show() }) - - triggerList[0].addEventListener('hide.bs.tab', ev => { - hideCalled = true - expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile') - }) - - triggerList[0].addEventListener('hidden.bs.tab', ev => { - expect(hideCalled).toBeTrue() - expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile') - done() - }) - - firstTab.show() }) - it('should not fire hidden when hide is prevented', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should fire hide and hidden events', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerList = fixtureEl.querySelectorAll('button') - const firstTab = new Tab(triggerList[0]) - const secondTab = new Tab(triggerList[1]) - const expectDone = () => { - setTimeout(() => { - expect().nothing() - done() - }, 30) - } + const triggerList = fixtureEl.querySelectorAll('button') + const firstTab = new Tab(triggerList[0]) + const secondTab = new Tab(triggerList[1]) - triggerList[0].addEventListener('shown.bs.tab', () => { - secondTab.show() + let hideCalled = false + triggerList[0].addEventListener('shown.bs.tab', () => { + secondTab.show() + }) + + triggerList[0].addEventListener('hide.bs.tab', ev => { + hideCalled = true + expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile') + }) + + triggerList[0].addEventListener('hidden.bs.tab', ev => { + expect(hideCalled).toBeTrue() + expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile') + resolve() + }) + + firstTab.show() }) - - triggerList[0].addEventListener('hide.bs.tab', ev => { - ev.preventDefault() - expectDone() - }) - - triggerList[0].addEventListener('hidden.bs.tab', () => { - throw new Error('should not trigger hidden') - }) - - firstTab.show() }) - it('should handle removed tabs', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
test 1
', - '
test 2
', - '
test 3
', - '
' - ].join('') + it('should not fire hidden when hide is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const secondNavEl = fixtureEl.querySelector('#secondNav') - const btnCloseEl = fixtureEl.querySelector('#btnClose') - const secondNavTab = new Tab(secondNavEl) + const triggerList = fixtureEl.querySelectorAll('button') + const firstTab = new Tab(triggerList[0]) + const secondTab = new Tab(triggerList[1]) + const expectDone = () => { + setTimeout(() => { + expect().nothing() + resolve() + }, 30) + } - secondNavEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelectorAll('.nav-tab')).toHaveSize(2) - done() + triggerList[0].addEventListener('shown.bs.tab', () => { + secondTab.show() + }) + + triggerList[0].addEventListener('hide.bs.tab', ev => { + ev.preventDefault() + expectDone() + }) + + triggerList[0].addEventListener('hidden.bs.tab', () => { + throw new Error('should not trigger hidden') + }) + + firstTab.show() }) + }) - btnCloseEl.addEventListener('click', () => { - const linkEl = btnCloseEl.parentNode - const liEl = linkEl.parentNode - const tabId = linkEl.getAttribute('href') - const tabIdEl = fixtureEl.querySelector(tabId) + it('should handle removed tabs', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
test 1
', + '
test 2
', + '
test 3
', + '
' + ].join('') - liEl.remove() - tabIdEl.remove() - secondNavTab.show() + const secondNavEl = fixtureEl.querySelector('#secondNav') + const btnCloseEl = fixtureEl.querySelector('#btnClose') + const secondNavTab = new Tab(secondNavEl) + + secondNavEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelectorAll('.nav-tab')).toHaveSize(2) + resolve() + }) + + btnCloseEl.addEventListener('click', () => { + const linkEl = btnCloseEl.parentNode + const liEl = linkEl.parentNode + const tabId = linkEl.getAttribute('href') + const tabIdEl = fixtureEl.querySelector(tabId) + + liEl.remove() + tabIdEl.remove() + secondNavTab.show() + }) + + btnCloseEl.click() }) - - btnCloseEl.click() }) }) @@ -464,27 +486,29 @@ describe('Tab', () => { }) describe('data-api', () => { - it('should create dynamically a tab', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
', - '
', - '
' - ].join('') + it('should create dynamically a tab', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
', + '
', + '
' + ].join('') - const secondTabTrigger = fixtureEl.querySelector('#triggerProfile') + const secondTabTrigger = fixtureEl.querySelector('#triggerProfile') - secondTabTrigger.addEventListener('shown.bs.tab', () => { - expect(secondTabTrigger).toHaveClass('active') - expect(fixtureEl.querySelector('#profile')).toHaveClass('active') - done() + secondTabTrigger.addEventListener('shown.bs.tab', () => { + expect(secondTabTrigger).toHaveClass('active') + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + resolve() + }) + + secondTabTrigger.click() }) - - secondTabTrigger.click() }) it('selected tab should deactivate previous selected link in dropdown', () => { @@ -567,202 +591,216 @@ describe('Tab', () => { expect(fixtureEl.querySelector('.nav-link')).not.toHaveClass('active') }) - it('should handle nested tabs', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
', - ' ', - '
', - '
Nested Tab1 Content
', - '
Nested Tab2 Content
', - '
', - '
', - '
Tab2 Content
', - '
Tab3 Content
', - '
' - ].join('') + it('should handle nested tabs', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
', + ' ', + '
', + '
Nested Tab1 Content
', + '
Nested Tab2 Content
', + '
', + '
', + '
Tab2 Content
', + '
Tab3 Content
', + '
' + ].join('') - const tab1El = fixtureEl.querySelector('#tab1') - const tabNested2El = fixtureEl.querySelector('#tabNested2') - const xTab1El = fixtureEl.querySelector('#x-tab1') + const tab1El = fixtureEl.querySelector('#tab1') + const tabNested2El = fixtureEl.querySelector('#tabNested2') + const xTab1El = fixtureEl.querySelector('#x-tab1') - tabNested2El.addEventListener('shown.bs.tab', () => { - expect(xTab1El).toHaveClass('active') - done() - }) - - tab1El.addEventListener('shown.bs.tab', () => { - expect(xTab1El).toHaveClass('active') - tabNested2El.click() - }) - - tab1El.click() - }) - - it('should not remove fade class if no active pane is present', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
', - '
', - '
' - ].join('') - - const triggerTabProfileEl = fixtureEl.querySelector('#tab-profile') - const triggerTabHomeEl = fixtureEl.querySelector('#tab-home') - const tabProfileEl = fixtureEl.querySelector('#profile') - const tabHomeEl = fixtureEl.querySelector('#home') - - triggerTabProfileEl.addEventListener('shown.bs.tab', () => { - expect(tabProfileEl).toHaveClass('fade') - expect(tabProfileEl).toHaveClass('show') - - triggerTabHomeEl.addEventListener('shown.bs.tab', () => { - expect(tabProfileEl).toHaveClass('fade') - expect(tabProfileEl).not.toHaveClass('show') - - expect(tabHomeEl).toHaveClass('fade') - expect(tabHomeEl).toHaveClass('show') - - done() + tabNested2El.addEventListener('shown.bs.tab', () => { + expect(xTab1El).toHaveClass('active') + resolve() }) - triggerTabHomeEl.click() - }) + tab1El.addEventListener('shown.bs.tab', () => { + expect(xTab1El).toHaveClass('active') + tabNested2El.click() + }) - triggerTabProfileEl.click() + tab1El.click() + }) }) - it('should not add show class to tab panes if there is no `.fade` class', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
test 1
', - '
test 2
', - '
' - ].join('') + it('should not remove fade class if no active pane is present', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
', + '
', + '
' + ].join('') - const secondNavEl = fixtureEl.querySelector('#secondNav') + const triggerTabProfileEl = fixtureEl.querySelector('#tab-profile') + const triggerTabHomeEl = fixtureEl.querySelector('#tab-home') + const tabProfileEl = fixtureEl.querySelector('#profile') + const tabHomeEl = fixtureEl.querySelector('#home') - secondNavEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelectorAll('.show')).toHaveSize(0) - done() + triggerTabProfileEl.addEventListener('shown.bs.tab', () => { + expect(tabProfileEl).toHaveClass('fade') + expect(tabProfileEl).toHaveClass('show') + + triggerTabHomeEl.addEventListener('shown.bs.tab', () => { + expect(tabProfileEl).toHaveClass('fade') + expect(tabProfileEl).not.toHaveClass('show') + + expect(tabHomeEl).toHaveClass('fade') + expect(tabHomeEl).toHaveClass('show') + + resolve() + }) + + triggerTabHomeEl.click() + }) + + triggerTabProfileEl.click() }) - - secondNavEl.click() }) - it('should add show class to tab panes if there is a `.fade` class', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
test 1
', - '
test 2
', - '
' - ].join('') + it('should not add show class to tab panes if there is no `.fade` class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
test 1
', + '
test 2
', + '
' + ].join('') - const secondNavEl = fixtureEl.querySelector('#secondNav') + const secondNavEl = fixtureEl.querySelector('#secondNav') - secondNavEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelectorAll('.show')).toHaveSize(1) - done() + secondNavEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelectorAll('.show')).toHaveSize(0) + resolve() + }) + + secondNavEl.click() }) - - secondNavEl.click() }) - it('should prevent default when the trigger is or ', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should add show class to tab panes if there is a `.fade` class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
test 1
', + '
test 2
', + '
' + ].join('') - const tabEl = fixtureEl.querySelector('[href="#test2"]') - spyOn(Event.prototype, 'preventDefault').and.callThrough() + const secondNavEl = fixtureEl.querySelector('#secondNav') - tabEl.addEventListener('shown.bs.tab', () => { - expect(tabEl).toHaveClass('active') - expect(Event.prototype.preventDefault).toHaveBeenCalled() - done() + secondNavEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelectorAll('.show')).toHaveSize(1) + resolve() + }) + + secondNavEl.click() }) - - tabEl.click() }) - it('should not fire shown when tab has disabled attribute', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
', - '
', - '
' - ].join('') + it('should prevent default when the trigger is or ', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDisabled = fixtureEl.querySelector('button[disabled]') - triggerDisabled.addEventListener('shown.bs.tab', () => { - throw new Error('should not trigger shown event') + const tabEl = fixtureEl.querySelector('[href="#test2"]') + spyOn(Event.prototype, 'preventDefault').and.callThrough() + + tabEl.addEventListener('shown.bs.tab', () => { + expect(tabEl).toHaveClass('active') + expect(Event.prototype.preventDefault).toHaveBeenCalled() + resolve() + }) + + tabEl.click() }) - - triggerDisabled.click() - setTimeout(() => { - expect().nothing() - done() - }, 30) }) - it('should not fire shown when tab has disabled class', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
', - '
', - '
' - ].join('') + it('should not fire shown when tab has disabled attribute', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
', + '
', + '
' + ].join('') - const triggerDisabled = fixtureEl.querySelector('a.disabled') + const triggerDisabled = fixtureEl.querySelector('button[disabled]') + triggerDisabled.addEventListener('shown.bs.tab', () => { + throw new Error('should not trigger shown event') + }) - triggerDisabled.addEventListener('shown.bs.tab', () => { - throw new Error('should not trigger shown event') + triggerDisabled.click() + setTimeout(() => { + expect().nothing() + resolve() + }, 30) }) + }) - triggerDisabled.click() - setTimeout(() => { - expect().nothing() - done() - }, 30) + it('should not fire shown when tab has disabled class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
', + '
', + '
' + ].join('') + + const triggerDisabled = fixtureEl.querySelector('a.disabled') + + triggerDisabled.addEventListener('shown.bs.tab', () => { + throw new Error('should not trigger shown event') + }) + + triggerDisabled.click() + setTimeout(() => { + expect().nothing() + resolve() + }, 30) + }) }) }) }) diff --git a/js/tests/unit/toast.spec.js b/js/tests/unit/toast.spec.js index c4ea43808c..9134a8410d 100644 --- a/js/tests/unit/toast.spec.js +++ b/js/tests/unit/toast.spec.js @@ -1,5 +1,5 @@ import Toast from '../../src/toast' -import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture' +import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture' describe('Toast', () => { let fixtureEl @@ -36,52 +36,56 @@ describe('Toast', () => { expect(toastByElement._element).toEqual(toastEl) }) - it('should allow to config in js', done => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') + it('should allow to config in js', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + '
', + ' a simple toast', + '
', + '
' + ].join('') - const toastEl = fixtureEl.querySelector('div') - const toast = new Toast(toastEl, { - delay: 1 + const toastEl = fixtureEl.querySelector('div') + const toast = new Toast(toastEl, { + delay: 1 + }) + + toastEl.addEventListener('shown.bs.toast', () => { + expect(toastEl).toHaveClass('show') + resolve() + }) + + toast.show() }) - - toastEl.addEventListener('shown.bs.toast', () => { - expect(toastEl).toHaveClass('show') - done() - }) - - toast.show() }) - it('should close toast when close element with data-bs-dismiss attribute is set', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') + it('should close toast when close element with data-bs-dismiss attribute is set', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') - const toastEl = fixtureEl.querySelector('div') - const toast = new Toast(toastEl) + const toastEl = fixtureEl.querySelector('div') + const toast = new Toast(toastEl) - toastEl.addEventListener('shown.bs.toast', () => { - expect(toastEl).toHaveClass('show') + toastEl.addEventListener('shown.bs.toast', () => { + expect(toastEl).toHaveClass('show') - const button = toastEl.querySelector('.btn-close') + const button = toastEl.querySelector('.btn-close') - button.click() + button.click() + }) + + toastEl.addEventListener('hidden.bs.toast', () => { + expect(toastEl).not.toHaveClass('show') + resolve() + }) + + toast.show() }) - - toastEl.addEventListener('hidden.bs.toast', () => { - expect(toastEl).not.toHaveClass('show') - done() - }) - - toast.show() }) }) @@ -111,304 +115,324 @@ describe('Toast', () => { }) describe('show', () => { - it('should auto hide', done => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') + it('should auto hide', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + '
', + ' a simple toast', + '
', + '
' + ].join('') - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - toastEl.addEventListener('hidden.bs.toast', () => { - expect(toastEl).not.toHaveClass('show') - done() - }) - - toast.show() - }) - - it('should not add fade class', done => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - - toastEl.addEventListener('shown.bs.toast', () => { - expect(toastEl).not.toHaveClass('fade') - done() - }) - - toast.show() - }) - - it('should not trigger shown if show is prevented', done => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - - const assertDone = () => { - setTimeout(() => { + toastEl.addEventListener('hidden.bs.toast', () => { expect(toastEl).not.toHaveClass('show') - done() - }, 20) - } - - toastEl.addEventListener('show.bs.toast', event => { - event.preventDefault() - assertDone() - }) - - toastEl.addEventListener('shown.bs.toast', () => { - throw new Error('shown event should not be triggered if show is prevented') - }) - - toast.show() - }) - - it('should clear timeout if toast is shown again before it is hidden', done => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - - setTimeout(() => { - toast._config.autohide = false - toastEl.addEventListener('shown.bs.toast', () => { - expect(toast._clearTimeout).toHaveBeenCalled() - expect(toast._timeout).toBeNull() - done() + resolve() }) + toast.show() - }, toast._config.delay / 2) - - spyOn(toast, '_clearTimeout').and.callThrough() - - toast.show() + }) }) - it('should clear timeout if toast is interacted with mouse', done => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') + it('should not add fade class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + '
', + ' a simple toast', + '
', + '
' + ].join('') - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - const spy = spyOn(toast, '_clearTimeout').and.callThrough() + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - setTimeout(() => { - spy.calls.reset() - - toastEl.addEventListener('mouseover', () => { - expect(toast._clearTimeout).toHaveBeenCalledTimes(1) - expect(toast._timeout).toBeNull() - done() + toastEl.addEventListener('shown.bs.toast', () => { + expect(toastEl).not.toHaveClass('fade') + resolve() }) - const mouseOverEvent = createEvent('mouseover') - toastEl.dispatchEvent(mouseOverEvent) - }, toast._config.delay / 2) - - toast.show() + toast.show() + }) }) - it('should clear timeout if toast is interacted with keyboard', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
', - ' a simple toast', - ' ', - '
', - '
' - ].join('') + it('should not trigger shown if show is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + '
', + ' a simple toast', + '
', + '
' + ].join('') - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - const spy = spyOn(toast, '_clearTimeout').and.callThrough() + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - setTimeout(() => { - spy.calls.reset() + const assertDone = () => { + setTimeout(() => { + expect(toastEl).not.toHaveClass('show') + resolve() + }, 20) + } - toastEl.addEventListener('focusin', () => { - expect(toast._clearTimeout).toHaveBeenCalledTimes(1) - expect(toast._timeout).toBeNull() - done() + toastEl.addEventListener('show.bs.toast', event => { + event.preventDefault() + assertDone() }) - const insideFocusable = toastEl.querySelector('button') - insideFocusable.focus() - }, toast._config.delay / 2) + toastEl.addEventListener('shown.bs.toast', () => { + throw new Error('shown event should not be triggered if show is prevented') + }) - toast.show() + toast.show() + }) }) - it('should still auto hide after being interacted with mouse and keyboard', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
', - ' a simple toast', - ' ', - '
', - '
' - ].join('') + it('should clear timeout if toast is shown again before it is hidden', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + '
', + ' a simple toast', + '
', + '
' + ].join('') - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) + + setTimeout(() => { + toast._config.autohide = false + toastEl.addEventListener('shown.bs.toast', () => { + expect(toast._clearTimeout).toHaveBeenCalled() + expect(toast._timeout).toBeNull() + resolve() + }) + toast.show() + }, toast._config.delay / 2) + + spyOn(toast, '_clearTimeout').and.callThrough() + + toast.show() + }) + }) + + it('should clear timeout if toast is interacted with mouse', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + '
', + ' a simple toast', + '
', + '
' + ].join('') + + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) + const spy = spyOn(toast, '_clearTimeout').and.callThrough() + + setTimeout(() => { + spy.calls.reset() + + toastEl.addEventListener('mouseover', () => { + expect(toast._clearTimeout).toHaveBeenCalledTimes(1) + expect(toast._timeout).toBeNull() + resolve() + }) + + const mouseOverEvent = createEvent('mouseover') + toastEl.dispatchEvent(mouseOverEvent) + }, toast._config.delay / 2) + + toast.show() + }) + }) + + it('should clear timeout if toast is interacted with keyboard', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
', + ' a simple toast', + ' ', + '
', + '
' + ].join('') + + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) + const spy = spyOn(toast, '_clearTimeout').and.callThrough() + + setTimeout(() => { + spy.calls.reset() + + toastEl.addEventListener('focusin', () => { + expect(toast._clearTimeout).toHaveBeenCalledTimes(1) + expect(toast._timeout).toBeNull() + resolve() + }) - setTimeout(() => { - toastEl.addEventListener('mouseover', () => { const insideFocusable = toastEl.querySelector('button') insideFocusable.focus() - }) + }, toast._config.delay / 2) - toastEl.addEventListener('focusin', () => { - const mouseOutEvent = createEvent('mouseout') - toastEl.dispatchEvent(mouseOutEvent) - }) - - toastEl.addEventListener('mouseout', () => { - const outsideFocusable = document.getElementById('outside-focusable') - outsideFocusable.focus() - }) - - toastEl.addEventListener('focusout', () => { - expect(toast._timeout).not.toBeNull() - done() - }) - - const mouseOverEvent = createEvent('mouseover') - toastEl.dispatchEvent(mouseOverEvent) - }, toast._config.delay / 2) - - toast.show() + toast.show() + }) }) - it('should not auto hide if focus leaves but mouse pointer remains inside', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
', - ' a simple toast', - ' ', - '
', - '
' - ].join('') + it('should still auto hide after being interacted with mouse and keyboard', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
', + ' a simple toast', + ' ', + '
', + '
' + ].join('') - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - setTimeout(() => { - toastEl.addEventListener('mouseover', () => { - const insideFocusable = toastEl.querySelector('button') - insideFocusable.focus() - }) + setTimeout(() => { + toastEl.addEventListener('mouseover', () => { + const insideFocusable = toastEl.querySelector('button') + insideFocusable.focus() + }) - toastEl.addEventListener('focusin', () => { - const outsideFocusable = document.getElementById('outside-focusable') - outsideFocusable.focus() - }) + toastEl.addEventListener('focusin', () => { + const mouseOutEvent = createEvent('mouseout') + toastEl.dispatchEvent(mouseOutEvent) + }) - toastEl.addEventListener('focusout', () => { - expect(toast._timeout).toBeNull() - done() - }) + toastEl.addEventListener('mouseout', () => { + const outsideFocusable = document.getElementById('outside-focusable') + outsideFocusable.focus() + }) - const mouseOverEvent = createEvent('mouseover') - toastEl.dispatchEvent(mouseOverEvent) - }, toast._config.delay / 2) + toastEl.addEventListener('focusout', () => { + expect(toast._timeout).not.toBeNull() + resolve() + }) - toast.show() + const mouseOverEvent = createEvent('mouseover') + toastEl.dispatchEvent(mouseOverEvent) + }, toast._config.delay / 2) + + toast.show() + }) }) - it('should not auto hide if mouse pointer leaves but focus remains inside', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
', - ' a simple toast', - ' ', - '
', - '
' - ].join('') + it('should not auto hide if focus leaves but mouse pointer remains inside', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
', + ' a simple toast', + ' ', + '
', + '
' + ].join('') - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - setTimeout(() => { - toastEl.addEventListener('mouseover', () => { - const insideFocusable = toastEl.querySelector('button') - insideFocusable.focus() - }) + setTimeout(() => { + toastEl.addEventListener('mouseover', () => { + const insideFocusable = toastEl.querySelector('button') + insideFocusable.focus() + }) - toastEl.addEventListener('focusin', () => { - const mouseOutEvent = createEvent('mouseout') - toastEl.dispatchEvent(mouseOutEvent) - }) + toastEl.addEventListener('focusin', () => { + const outsideFocusable = document.getElementById('outside-focusable') + outsideFocusable.focus() + }) - toastEl.addEventListener('mouseout', () => { - expect(toast._timeout).toBeNull() - done() - }) + toastEl.addEventListener('focusout', () => { + expect(toast._timeout).toBeNull() + resolve() + }) - const mouseOverEvent = createEvent('mouseover') - toastEl.dispatchEvent(mouseOverEvent) - }, toast._config.delay / 2) + const mouseOverEvent = createEvent('mouseover') + toastEl.dispatchEvent(mouseOverEvent) + }, toast._config.delay / 2) - toast.show() + toast.show() + }) + }) + + it('should not auto hide if mouse pointer leaves but focus remains inside', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
', + ' a simple toast', + ' ', + '
', + '
' + ].join('') + + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) + + setTimeout(() => { + toastEl.addEventListener('mouseover', () => { + const insideFocusable = toastEl.querySelector('button') + insideFocusable.focus() + }) + + toastEl.addEventListener('focusin', () => { + const mouseOutEvent = createEvent('mouseout') + toastEl.dispatchEvent(mouseOutEvent) + }) + + toastEl.addEventListener('mouseout', () => { + expect(toast._timeout).toBeNull() + resolve() + }) + + const mouseOverEvent = createEvent('mouseover') + toastEl.dispatchEvent(mouseOverEvent) + }, toast._config.delay / 2) + + toast.show() + }) }) }) describe('hide', () => { - it('should allow to hide toast manually', done => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') + it('should allow to hide toast manually', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + '
', + ' a simple toast', + '
', + '
' + ].join('') - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - toastEl.addEventListener('shown.bs.toast', () => { - toast.hide() + toastEl.addEventListener('shown.bs.toast', () => { + toast.hide() + }) + + toastEl.addEventListener('hidden.bs.toast', () => { + expect(toastEl).not.toHaveClass('show') + resolve() + }) + + toast.show() }) - - toastEl.addEventListener('hidden.bs.toast', () => { - expect(toastEl).not.toHaveClass('show') - done() - }) - - toast.show() }) it('should do nothing when we call hide on a non shown toast', () => { @@ -424,39 +448,41 @@ describe('Toast', () => { expect(toastEl.classList.contains).toHaveBeenCalled() }) - it('should not trigger hidden if hide is prevented', done => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') + it('should not trigger hidden if hide is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + '
', + ' a simple toast', + '
', + '
' + ].join('') - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - const assertDone = () => { - setTimeout(() => { - expect(toastEl).toHaveClass('show') - done() - }, 20) - } + const assertDone = () => { + setTimeout(() => { + expect(toastEl).toHaveClass('show') + resolve() + }, 20) + } - toastEl.addEventListener('shown.bs.toast', () => { - toast.hide() + toastEl.addEventListener('shown.bs.toast', () => { + toast.hide() + }) + + toastEl.addEventListener('hide.bs.toast', event => { + event.preventDefault() + assertDone() + }) + + toastEl.addEventListener('hidden.bs.toast', () => { + throw new Error('hidden event should not be triggered if hide is prevented') + }) + + toast.show() }) - - toastEl.addEventListener('hide.bs.toast', event => { - event.preventDefault() - assertDone() - }) - - toastEl.addEventListener('hidden.bs.toast', () => { - throw new Error('hidden event should not be triggered if hide is prevented') - }) - - toast.show() }) }) @@ -475,34 +501,36 @@ describe('Toast', () => { expect(Toast.getInstance(toastEl)).toBeNull() }) - it('should allow to destroy toast and hide it before that', done => { - fixtureEl.innerHTML = [ - '
', - '
', - ' a simple toast', - '
', - '
' - ].join('') + it('should allow to destroy toast and hide it before that', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + '
', + ' a simple toast', + '
', + '
' + ].join('') - const toastEl = fixtureEl.querySelector('div') - const toast = new Toast(toastEl) - const expected = () => { - expect(toastEl).toHaveClass('show') - expect(Toast.getInstance(toastEl)).not.toBeNull() + const toastEl = fixtureEl.querySelector('div') + const toast = new Toast(toastEl) + const expected = () => { + expect(toastEl).toHaveClass('show') + expect(Toast.getInstance(toastEl)).not.toBeNull() - toast.dispose() + toast.dispose() - expect(Toast.getInstance(toastEl)).toBeNull() - expect(toastEl).not.toHaveClass('show') + expect(Toast.getInstance(toastEl)).toBeNull() + expect(toastEl).not.toHaveClass('show') - done() - } + resolve() + } - toastEl.addEventListener('shown.bs.toast', () => { - setTimeout(expected, 1) + toastEl.addEventListener('shown.bs.toast', () => { + setTimeout(expected, 1) + }) + + toast.show() }) - - toast.show() }) }) diff --git a/js/tests/unit/tooltip.spec.js b/js/tests/unit/tooltip.spec.js index 3feacf7b44..4a9a6d3bb0 100644 --- a/js/tests/unit/tooltip.spec.js +++ b/js/tests/unit/tooltip.spec.js @@ -94,52 +94,56 @@ describe('Tooltip', () => { expect(tooltip._config.content).toEqual('7') }) - it('should enable selector delegation', done => { - fixtureEl.innerHTML = '
' + it('should enable selector delegation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const containerEl = fixtureEl.querySelector('div') - const tooltipContainer = new Tooltip(containerEl, { - selector: 'a[rel="tooltip"]', - trigger: 'click' + const containerEl = fixtureEl.querySelector('div') + const tooltipContainer = new Tooltip(containerEl, { + selector: 'a[rel="tooltip"]', + trigger: 'click' + }) + + containerEl.innerHTML = '' + + const tooltipInContainerEl = containerEl.querySelector('a') + + tooltipInContainerEl.addEventListener('shown.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).not.toBeNull() + tooltipContainer.dispose() + resolve() + }) + + tooltipInContainerEl.click() }) - - containerEl.innerHTML = '' - - const tooltipInContainerEl = containerEl.querySelector('a') - - tooltipInContainerEl.addEventListener('shown.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).not.toBeNull() - tooltipContainer.dispose() - done() - }) - - tooltipInContainerEl.click() }) - it('should create offset modifier when offset is passed as a function', done => { - fixtureEl.innerHTML = '' + it('should create offset modifier when offset is passed as a function', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20]) - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - offset: getOffset, - popperConfig: { - onFirstUpdate: state => { - expect(getOffset).toHaveBeenCalledWith({ - popper: state.rects.popper, - reference: state.rects.reference, - placement: state.placement - }, tooltipEl) - done() + const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20]) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + offset: getOffset, + popperConfig: { + onFirstUpdate: state => { + expect(getOffset).toHaveBeenCalledWith({ + popper: state.rects.popper, + reference: state.rects.reference, + placement: state.placement + }, tooltipEl) + resolve() + } } - } + }) + + const offset = tooltip._getOffset() + + expect(offset).toEqual(jasmine.any(Function)) + + tooltip.show() }) - - const offset = tooltip._getOffset() - - expect(offset).toEqual(jasmine.any(Function)) - - tooltip.show() }) it('should create offset modifier when offset option is passed in data attribute', () => { @@ -192,42 +196,46 @@ describe('Tooltip', () => { }) describe('enable', () => { - it('should enable a tooltip', done => { - fixtureEl.innerHTML = '' + it('should enable a tooltip', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltip.enable() + tooltip.enable() - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).not.toBeNull() - done() + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).not.toBeNull() + resolve() + }) + + tooltip.show() }) - - tooltip.show() }) }) describe('disable', () => { - it('should disable tooltip', done => { - fixtureEl.innerHTML = '' + it('should disable tooltip', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltip.disable() + tooltip.disable() - tooltipEl.addEventListener('show.bs.tooltip', () => { - throw new Error('should not show a disabled tooltip') + tooltipEl.addEventListener('show.bs.tooltip', () => { + throw new Error('should not show a disabled tooltip') + }) + + tooltip.show() + + setTimeout(() => { + expect().nothing() + resolve() + }, 10) }) - - tooltip.show() - - setTimeout(() => { - expect().nothing() - done() - }, 10) }) }) @@ -247,96 +255,106 @@ describe('Tooltip', () => { }) describe('toggle', () => { - it('should do nothing if disabled', done => { - fixtureEl.innerHTML = '' + it('should do nothing if disabled', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltip.disable() + tooltip.disable() - tooltipEl.addEventListener('show.bs.tooltip', () => { - throw new Error('should not show a disabled tooltip') + tooltipEl.addEventListener('show.bs.tooltip', () => { + throw new Error('should not show a disabled tooltip') + }) + + tooltip.toggle() + + setTimeout(() => { + expect().nothing() + resolve() + }, 10) }) - - tooltip.toggle() - - setTimeout(() => { - expect().nothing() - done() - }, 10) }) - it('should show a tooltip', done => { - fixtureEl.innerHTML = '' + it('should show a tooltip', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).not.toBeNull() - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).not.toBeNull() + resolve() + }) - tooltip.toggle() - }) - - it('should call toggle and show the tooltip when trigger is "click"', done => { - fixtureEl.innerHTML = '' - - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - trigger: 'click' - }) - - spyOn(tooltip, 'toggle').and.callThrough() - - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(tooltip.toggle).toHaveBeenCalled() - done() - }) - - tooltipEl.click() - }) - - it('should hide a tooltip', done => { - fixtureEl.innerHTML = '' - - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) - - tooltipEl.addEventListener('shown.bs.tooltip', () => { tooltip.toggle() }) - - tooltipEl.addEventListener('hidden.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).toBeNull() - done() - }) - - tooltip.toggle() }) - it('should call toggle and hide the tooltip when trigger is "click"', done => { - fixtureEl.innerHTML = '' + it('should call toggle and show the tooltip when trigger is "click"', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - trigger: 'click' - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + trigger: 'click' + }) - spyOn(tooltip, 'toggle').and.callThrough() + spyOn(tooltip, 'toggle').and.callThrough() + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(tooltip.toggle).toHaveBeenCalled() + resolve() + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { tooltipEl.click() }) + }) - tooltipEl.addEventListener('hidden.bs.tooltip', () => { - expect(tooltip.toggle).toHaveBeenCalled() - done() + it('should hide a tooltip', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + tooltip.toggle() + }) + + tooltipEl.addEventListener('hidden.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).toBeNull() + resolve() + }) + + tooltip.toggle() }) + }) - tooltipEl.click() + it('should call toggle and hide the tooltip when trigger is "click"', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + trigger: 'click' + }) + + spyOn(tooltip, 'toggle').and.callThrough() + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + tooltipEl.click() + }) + + tooltipEl.addEventListener('hidden.bs.tooltip', () => { + expect(tooltip.toggle).toHaveBeenCalled() + resolve() + }) + + tooltipEl.click() + }) }) }) @@ -367,239 +385,263 @@ describe('Tooltip', () => { expect(removeEventSpy.calls.allArgs()).toEqual(expectedArgs) }) - it('should destroy a tooltip after it is shown and hidden', done => { - fixtureEl.innerHTML = '' + it('should destroy a tooltip after it is shown and hidden', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - tooltip.hide() + tooltipEl.addEventListener('shown.bs.tooltip', () => { + tooltip.hide() + }) + tooltipEl.addEventListener('hidden.bs.tooltip', () => { + tooltip.dispose() + expect(tooltip.tip).toBeNull() + expect(Tooltip.getInstance(tooltipEl)).toBeNull() + resolve() + }) + + tooltip.show() }) - tooltipEl.addEventListener('hidden.bs.tooltip', () => { - tooltip.dispose() - expect(tooltip.tip).toBeNull() - expect(Tooltip.getInstance(tooltipEl)).toBeNull() - done() - }) - - tooltip.show() }) - it('should destroy a tooltip and remove it from the dom', done => { - fixtureEl.innerHTML = '' + it('should destroy a tooltip and remove it from the dom', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).not.toBeNull() + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).not.toBeNull() - tooltip.dispose() + tooltip.dispose() - expect(document.querySelector('.tooltip')).toBeNull() - done() + expect(document.querySelector('.tooltip')).toBeNull() + resolve() + }) + + tooltip.show() }) - - tooltip.show() }) }) describe('show', () => { - it('should show a tooltip', done => { - fixtureEl.innerHTML = '' + it('should show a tooltip', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) - - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tooltipShown = document.querySelector('.tooltip') - - expect(tooltipShown).not.toBeNull() - expect(tooltipEl.getAttribute('aria-describedby')).toEqual(tooltipShown.getAttribute('id')) - expect(tooltipShown.getAttribute('id')).toContain('tooltip') - done() - }) - - tooltip.show() - }) - - it('should show a tooltip when hovering a child element', done => { - fixtureEl.innerHTML = [ - '', - ' ', - ' ', - ' ', - ' ', - '' - ].join('') - - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) - - spyOn(tooltip, 'show') - - tooltipEl.querySelector('rect').dispatchEvent(createEvent('mouseover', { bubbles: true })) - - setTimeout(() => { - expect(tooltip.show).toHaveBeenCalled() - done() - }, 0) - }) - - it('should show a tooltip on mobile', done => { - fixtureEl.innerHTML = '' - - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) - document.documentElement.ontouchstart = noop - - spyOn(EventHandler, 'on').and.callThrough() - - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).not.toBeNull() - expect(EventHandler.on).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) - document.documentElement.ontouchstart = undefined - done() - }) - - tooltip.show() - }) - - it('should show a tooltip relative to placement option', done => { - fixtureEl.innerHTML = '' - - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - placement: 'bottom' - }) - - tooltipEl.addEventListener('inserted.bs.tooltip', () => { - expect(tooltip._getTipElement()).toHaveClass('bs-tooltip-auto') - }) - - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(tooltip._getTipElement()).toHaveClass('bs-tooltip-auto') - expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('bottom') - done() - }) - - tooltip.show() - }) - - it('should not error when trying to show a tooltip that has been removed from the dom', done => { - fixtureEl.innerHTML = '' - - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) - - const firstCallback = () => { - tooltipEl.removeEventListener('shown.bs.tooltip', firstCallback) - let tooltipShown = document.querySelector('.tooltip') - - tooltipShown.remove() + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) tooltipEl.addEventListener('shown.bs.tooltip', () => { - tooltipShown = document.querySelector('.tooltip') + const tooltipShown = document.querySelector('.tooltip') expect(tooltipShown).not.toBeNull() - done() + expect(tooltipEl.getAttribute('aria-describedby')).toEqual(tooltipShown.getAttribute('id')) + expect(tooltipShown.getAttribute('id')).toContain('tooltip') + resolve() }) tooltip.show() - } - - tooltipEl.addEventListener('shown.bs.tooltip', firstCallback) - - tooltip.show() + }) }) - it('should show a tooltip with a dom element container', done => { - fixtureEl.innerHTML = '' + it('should show a tooltip when hovering a child element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + ' ', + ' ', + ' ', + ' ', + '' + ].join('') - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - container: fixtureEl + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + + spyOn(tooltip, 'show') + + tooltipEl.querySelector('rect').dispatchEvent(createEvent('mouseover', { bubbles: true })) + + setTimeout(() => { + expect(tooltip.show).toHaveBeenCalled() + resolve() + }, 0) }) - - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(fixtureEl.querySelector('.tooltip')).not.toBeNull() - done() - }) - - tooltip.show() }) - it('should show a tooltip with a jquery element container', done => { - fixtureEl.innerHTML = '' + it('should show a tooltip on mobile', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - container: { - 0: fixtureEl, - jquery: 'jQuery' + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + document.documentElement.ontouchstart = noop + + spyOn(EventHandler, 'on').and.callThrough() + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).not.toBeNull() + expect(EventHandler.on).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) + document.documentElement.ontouchstart = undefined + resolve() + }) + + tooltip.show() + }) + }) + + it('should show a tooltip relative to placement option', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + placement: 'bottom' + }) + + tooltipEl.addEventListener('inserted.bs.tooltip', () => { + expect(tooltip._getTipElement()).toHaveClass('bs-tooltip-auto') + }) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(tooltip._getTipElement()).toHaveClass('bs-tooltip-auto') + expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('bottom') + resolve() + }) + + tooltip.show() + }) + }) + + it('should not error when trying to show a tooltip that has been removed from the dom', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + + const firstCallback = () => { + tooltipEl.removeEventListener('shown.bs.tooltip', firstCallback) + let tooltipShown = document.querySelector('.tooltip') + + tooltipShown.remove() + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + tooltipShown = document.querySelector('.tooltip') + + expect(tooltipShown).not.toBeNull() + resolve() + }) + + tooltip.show() } - }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(fixtureEl.querySelector('.tooltip')).not.toBeNull() - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', firstCallback) - tooltip.show() + tooltip.show() + }) }) - it('should show a tooltip with a selector in container', done => { - fixtureEl.innerHTML = '' + it('should show a tooltip with a dom element container', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - container: '#fixture' + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + container: fixtureEl + }) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(fixtureEl.querySelector('.tooltip')).not.toBeNull() + resolve() + }) + + tooltip.show() }) - - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(fixtureEl.querySelector('.tooltip')).not.toBeNull() - done() - }) - - tooltip.show() }) - it('should show a tooltip with placement as a function', done => { - fixtureEl.innerHTML = '' + it('should show a tooltip with a jquery element container', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const spy = jasmine.createSpy('placement').and.returnValue('top') - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - placement: spy + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + container: { + 0: fixtureEl, + jquery: 'jQuery' + } + }) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(fixtureEl.querySelector('.tooltip')).not.toBeNull() + resolve() + }) + + tooltip.show() }) - - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).not.toBeNull() - expect(spy).toHaveBeenCalled() - done() - }) - - tooltip.show() }) - it('should show a tooltip without the animation', done => { - fixtureEl.innerHTML = '' + it('should show a tooltip with a selector in container', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - animation: false + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + container: '#fixture' + }) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(fixtureEl.querySelector('.tooltip')).not.toBeNull() + resolve() + }) + + tooltip.show() }) + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tip = document.querySelector('.tooltip') + it('should show a tooltip with placement as a function', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - expect(tip).not.toBeNull() - expect(tip).not.toHaveClass('fade') - done() + const spy = jasmine.createSpy('placement').and.returnValue('top') + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + placement: spy + }) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).not.toBeNull() + expect(spy).toHaveBeenCalled() + resolve() + }) + + tooltip.show() }) + }) - tooltip.show() + it('should show a tooltip without the animation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + animation: false + }) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tip = document.querySelector('.tooltip') + + expect(tip).not.toBeNull() + expect(tip).not.toHaveClass('fade') + resolve() + }) + + tooltip.show() + }) }) it('should throw an error the element is not visible', () => { @@ -615,352 +657,382 @@ describe('Tooltip', () => { } }) - it('should not show a tooltip if show.bs.tooltip is prevented', done => { - fixtureEl.innerHTML = '' + it('should not show a tooltip if show.bs.tooltip is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - const expectedDone = () => { - setTimeout(() => { - expect(document.querySelector('.tooltip')).toBeNull() - done() - }, 10) - } - - tooltipEl.addEventListener('show.bs.tooltip', ev => { - ev.preventDefault() - expectedDone() - }) - - tooltipEl.addEventListener('shown.bs.tooltip', () => { - throw new Error('Tooltip should not be shown') - }) - - tooltip.show() - }) - - it('should show tooltip if leave event hasn\'t occurred before delay expires', done => { - fixtureEl.innerHTML = '' - - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - delay: 150 - }) - - spyOn(tooltip, 'show') - - setTimeout(() => { - expect(tooltip.show).not.toHaveBeenCalled() - }, 100) - - setTimeout(() => { - expect(tooltip.show).toHaveBeenCalled() - done() - }, 200) - - tooltipEl.dispatchEvent(createEvent('mouseover')) - }) - - it('should not show tooltip if leave event occurs before delay expires', done => { - fixtureEl.innerHTML = '' - - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - delay: 150 - }) - - spyOn(tooltip, 'show') - - setTimeout(() => { - expect(tooltip.show).not.toHaveBeenCalled() - tooltipEl.dispatchEvent(createEvent('mouseover')) - }, 100) - - setTimeout(() => { - expect(tooltip.show).toHaveBeenCalled() - expect(document.querySelectorAll('.tooltip')).toHaveSize(0) - done() - }, 200) - - tooltipEl.dispatchEvent(createEvent('mouseover')) - }) - - it('should not hide tooltip if leave event occurs and enter event occurs within the hide delay', done => { - fixtureEl.innerHTML = '' - - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - delay: { - show: 0, - hide: 150 + const expectedDone = () => { + setTimeout(() => { + expect(document.querySelector('.tooltip')).toBeNull() + resolve() + }, 10) } - }) - setTimeout(() => { - expect(tooltip._getTipElement()).toHaveClass('show') - tooltipEl.dispatchEvent(createEvent('mouseout')) - - setTimeout(() => { - expect(tooltip._getTipElement()).toHaveClass('show') - tooltipEl.dispatchEvent(createEvent('mouseover')) - }, 100) - - setTimeout(() => { - expect(tooltip._getTipElement()).toHaveClass('show') - expect(document.querySelectorAll('.tooltip')).toHaveSize(1) - done() - }, 200) - }, 10) - - tooltipEl.dispatchEvent(createEvent('mouseover')) - }) - - it('should not hide tooltip if leave event occurs and interaction remains inside trigger', done => { - fixtureEl.innerHTML = [ - '', - 'Trigger', - 'the tooltip', - '' - ].join('') - - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) - const triggerChild = tooltipEl.querySelector('b') - - spyOn(tooltip, 'hide').and.callThrough() - - tooltipEl.addEventListener('mouseover', () => { - const moveMouseToChildEvent = createEvent('mouseout') - Object.defineProperty(moveMouseToChildEvent, 'relatedTarget', { - value: triggerChild + tooltipEl.addEventListener('show.bs.tooltip', ev => { + ev.preventDefault() + expectedDone() }) - tooltipEl.dispatchEvent(moveMouseToChildEvent) - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + throw new Error('Tooltip should not be shown') + }) - tooltipEl.addEventListener('mouseout', () => { - expect(tooltip.hide).not.toHaveBeenCalled() - done() + tooltip.show() }) - - tooltipEl.dispatchEvent(createEvent('mouseover')) }) - it('should properly maintain tooltip state if leave event occurs and enter event occurs during hide transition', done => { - // Style this tooltip to give it plenty of room for popper to do what it wants - fixtureEl.innerHTML = 'Trigger' + it('should show tooltip if leave event hasn\'t occurred before delay expires', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + delay: 150 + }) - spyOn(window, 'getComputedStyle').and.returnValue({ - transitionDuration: '0.15s', - transitionDelay: '0s' - }) - - setTimeout(() => { - expect(tooltip._popper).not.toBeNull() - expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('top') - tooltipEl.dispatchEvent(createEvent('mouseout')) + spyOn(tooltip, 'show') setTimeout(() => { - expect(tooltip._getTipElement()).not.toHaveClass('show') + expect(tooltip.show).not.toHaveBeenCalled() + }, 100) + + setTimeout(() => { + expect(tooltip.show).toHaveBeenCalled() + resolve() + }, 200) + + tooltipEl.dispatchEvent(createEvent('mouseover')) + }) + }) + + it('should not show tooltip if leave event occurs before delay expires', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + delay: 150 + }) + + spyOn(tooltip, 'show') + + setTimeout(() => { + expect(tooltip.show).not.toHaveBeenCalled() tooltipEl.dispatchEvent(createEvent('mouseover')) }, 100) + setTimeout(() => { + expect(tooltip.show).toHaveBeenCalled() + expect(document.querySelectorAll('.tooltip')).toHaveSize(0) + resolve() + }, 200) + + tooltipEl.dispatchEvent(createEvent('mouseover')) + }) + }) + + it('should not hide tooltip if leave event occurs and enter event occurs within the hide delay', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + delay: { + show: 0, + hide: 150 + } + }) + + setTimeout(() => { + expect(tooltip._getTipElement()).toHaveClass('show') + tooltipEl.dispatchEvent(createEvent('mouseout')) + + setTimeout(() => { + expect(tooltip._getTipElement()).toHaveClass('show') + tooltipEl.dispatchEvent(createEvent('mouseover')) + }, 100) + + setTimeout(() => { + expect(tooltip._getTipElement()).toHaveClass('show') + expect(document.querySelectorAll('.tooltip')).toHaveSize(1) + resolve() + }, 200) + }, 10) + + tooltipEl.dispatchEvent(createEvent('mouseover')) + }) + }) + + it('should not hide tooltip if leave event occurs and interaction remains inside trigger', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + 'Trigger', + 'the tooltip', + '' + ].join('') + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + const triggerChild = tooltipEl.querySelector('b') + + spyOn(tooltip, 'hide').and.callThrough() + + tooltipEl.addEventListener('mouseover', () => { + const moveMouseToChildEvent = createEvent('mouseout') + Object.defineProperty(moveMouseToChildEvent, 'relatedTarget', { + value: triggerChild + }) + + tooltipEl.dispatchEvent(moveMouseToChildEvent) + }) + + tooltipEl.addEventListener('mouseout', () => { + expect(tooltip.hide).not.toHaveBeenCalled() + resolve() + }) + + tooltipEl.dispatchEvent(createEvent('mouseover')) + }) + }) + + it('should properly maintain tooltip state if leave event occurs and enter event occurs during hide transition', () => { + return new Promise(resolve => { + // Style this tooltip to give it plenty of room for popper to do what it wants + fixtureEl.innerHTML = 'Trigger' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.15s', + transitionDelay: '0s' + }) + setTimeout(() => { expect(tooltip._popper).not.toBeNull() expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('top') - done() - }, 200) - }, 10) + tooltipEl.dispatchEvent(createEvent('mouseout')) - tooltipEl.dispatchEvent(createEvent('mouseover')) + setTimeout(() => { + expect(tooltip._getTipElement()).not.toHaveClass('show') + tooltipEl.dispatchEvent(createEvent('mouseover')) + }, 100) + + setTimeout(() => { + expect(tooltip._popper).not.toBeNull() + expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('top') + resolve() + }, 200) + }, 10) + + tooltipEl.dispatchEvent(createEvent('mouseover')) + }) }) - it('should only trigger inserted event if a new tooltip element was created', done => { - fixtureEl.innerHTML = '' + it('should only trigger inserted event if a new tooltip element was created', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - spyOn(window, 'getComputedStyle').and.returnValue({ - transitionDuration: '0.15s', - transitionDelay: '0s' - }) + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.15s', + transitionDelay: '0s' + }) - const insertedFunc = jasmine.createSpy() - tooltipEl.addEventListener('inserted.bs.tooltip', insertedFunc) - - setTimeout(() => { - expect(insertedFunc).toHaveBeenCalledTimes(1) - tooltip.hide() - - setTimeout(() => { - tooltip.show() - }, 100) + const insertedFunc = jasmine.createSpy() + tooltipEl.addEventListener('inserted.bs.tooltip', insertedFunc) setTimeout(() => { expect(insertedFunc).toHaveBeenCalledTimes(1) - done() - }, 200) - }, 0) + tooltip.hide() - tooltip.show() + setTimeout(() => { + tooltip.show() + }, 100) + + setTimeout(() => { + expect(insertedFunc).toHaveBeenCalledTimes(1) + resolve() + }, 200) + }, 0) + + tooltip.show() + }) }) - it('should show a tooltip with custom class provided in data attributes', done => { - fixtureEl.innerHTML = '' + it('should show a tooltip with custom class provided in data attributes', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tip = document.querySelector('.tooltip') - expect(tip).not.toBeNull() - expect(tip).toHaveClass('custom-class') - done() + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tip = document.querySelector('.tooltip') + expect(tip).not.toBeNull() + expect(tip).toHaveClass('custom-class') + resolve() + }) + + tooltip.show() }) - - tooltip.show() }) - it('should show a tooltip with custom class provided as a string in config', done => { - fixtureEl.innerHTML = '' + it('should show a tooltip with custom class provided as a string in config', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - customClass: 'custom-class custom-class-2' + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + customClass: 'custom-class custom-class-2' + }) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tip = document.querySelector('.tooltip') + expect(tip).not.toBeNull() + expect(tip).toHaveClass('custom-class') + expect(tip).toHaveClass('custom-class-2') + resolve() + }) + + tooltip.show() }) - - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tip = document.querySelector('.tooltip') - expect(tip).not.toBeNull() - expect(tip).toHaveClass('custom-class') - expect(tip).toHaveClass('custom-class-2') - done() - }) - - tooltip.show() }) - it('should show a tooltip with custom class provided as a function in config', done => { - fixtureEl.innerHTML = '' + it('should show a tooltip with custom class provided as a function in config', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const spy = jasmine.createSpy('customClass').and.returnValue('custom-class') - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - customClass: spy + const spy = jasmine.createSpy('customClass').and.returnValue('custom-class') + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + customClass: spy + }) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tip = document.querySelector('.tooltip') + expect(tip).not.toBeNull() + expect(spy).toHaveBeenCalled() + expect(tip).toHaveClass('custom-class') + resolve() + }) + + tooltip.show() }) - - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tip = document.querySelector('.tooltip') - expect(tip).not.toBeNull() - expect(spy).toHaveBeenCalled() - expect(tip).toHaveClass('custom-class') - done() - }) - - tooltip.show() }) - it('should remove `title` attribute if exists', done => { - fixtureEl.innerHTML = '' + it('should remove `title` attribute if exists', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(tooltipEl.getAttribute('title')).toBeNull() - done() + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(tooltipEl.getAttribute('title')).toBeNull() + resolve() + }) + tooltip.show() }) - tooltip.show() }) }) describe('hide', () => { - it('should hide a tooltip', done => { - fixtureEl.innerHTML = '' + it('should hide a tooltip', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => tooltip.hide()) - tooltipEl.addEventListener('hidden.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).toBeNull() - expect(tooltipEl.getAttribute('aria-describedby')).toBeNull() - done() + tooltipEl.addEventListener('shown.bs.tooltip', () => tooltip.hide()) + tooltipEl.addEventListener('hidden.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).toBeNull() + expect(tooltipEl.getAttribute('aria-describedby')).toBeNull() + resolve() + }) + + tooltip.show() }) - - tooltip.show() }) - it('should hide a tooltip on mobile', done => { - fixtureEl.innerHTML = '' + it('should hide a tooltip on mobile', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - document.documentElement.ontouchstart = noop - spyOn(EventHandler, 'off') - tooltip.hide() + tooltipEl.addEventListener('shown.bs.tooltip', () => { + document.documentElement.ontouchstart = noop + spyOn(EventHandler, 'off') + tooltip.hide() + }) + + tooltipEl.addEventListener('hidden.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).toBeNull() + expect(EventHandler.off).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) + document.documentElement.ontouchstart = undefined + resolve() + }) + + tooltip.show() }) - - tooltipEl.addEventListener('hidden.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).toBeNull() - expect(EventHandler.off).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) - document.documentElement.ontouchstart = undefined - done() - }) - - tooltip.show() }) - it('should hide a tooltip without animation', done => { - fixtureEl.innerHTML = '' + it('should hide a tooltip without animation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - animation: false + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + animation: false + }) + + tooltipEl.addEventListener('shown.bs.tooltip', () => tooltip.hide()) + tooltipEl.addEventListener('hidden.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).toBeNull() + expect(tooltipEl.getAttribute('aria-describedby')).toBeNull() + resolve() + }) + + tooltip.show() }) - - tooltipEl.addEventListener('shown.bs.tooltip', () => tooltip.hide()) - tooltipEl.addEventListener('hidden.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).toBeNull() - expect(tooltipEl.getAttribute('aria-describedby')).toBeNull() - done() - }) - - tooltip.show() }) - it('should not hide a tooltip if hide event is prevented', done => { - fixtureEl.innerHTML = '' + it('should not hide a tooltip if hide event is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const assertDone = () => { - setTimeout(() => { - expect(document.querySelector('.tooltip')).not.toBeNull() - done() - }, 20) - } + const assertDone = () => { + setTimeout(() => { + expect(document.querySelector('.tooltip')).not.toBeNull() + resolve() + }, 20) + } - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - animation: false + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + animation: false + }) + + tooltipEl.addEventListener('shown.bs.tooltip', () => tooltip.hide()) + tooltipEl.addEventListener('hide.bs.tooltip', event => { + event.preventDefault() + assertDone() + }) + tooltipEl.addEventListener('hidden.bs.tooltip', () => { + throw new Error('should not trigger hidden event') + }) + + tooltip.show() }) - - tooltipEl.addEventListener('shown.bs.tooltip', () => tooltip.hide()) - tooltipEl.addEventListener('hide.bs.tooltip', event => { - event.preventDefault() - assertDone() - }) - tooltipEl.addEventListener('hidden.bs.tooltip', () => { - throw new Error('should not trigger hidden event') - }) - - tooltip.show() }) it('should not throw error running hide if popper hasn\'t been shown', () => { @@ -979,22 +1051,24 @@ describe('Tooltip', () => { }) describe('update', () => { - it('should call popper update', done => { - fixtureEl.innerHTML = '' + it('should call popper update', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - spyOn(tooltip._popper, 'update') + tooltipEl.addEventListener('shown.bs.tooltip', () => { + spyOn(tooltip._popper, 'update') - tooltip.update() + tooltip.update() - expect(tooltip._popper.update).toHaveBeenCalled() - done() + expect(tooltip._popper.update).toHaveBeenCalled() + resolve() + }) + + tooltip.show() }) - - tooltip.show() }) it('should do nothing if the tooltip is not shown', () => { @@ -1272,55 +1346,61 @@ describe('Tooltip', () => { }) describe('aria-label', () => { - it('should add the aria-label attribute for referencing original title', done => { - fixtureEl.innerHTML = '' + it('should add the aria-label attribute for referencing original title', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tooltipShown = document.querySelector('.tooltip') + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tooltipShown = document.querySelector('.tooltip') - expect(tooltipShown).not.toBeNull() - expect(tooltipEl.getAttribute('aria-label')).toEqual('Another tooltip') - done() + expect(tooltipShown).not.toBeNull() + expect(tooltipEl.getAttribute('aria-label')).toEqual('Another tooltip') + resolve() + }) + + tooltip.show() }) - - tooltip.show() }) - it('should not add the aria-label attribute if the attribute already exists', done => { - fixtureEl.innerHTML = '' + it('should not add the aria-label attribute if the attribute already exists', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tooltipShown = document.querySelector('.tooltip') + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tooltipShown = document.querySelector('.tooltip') - expect(tooltipShown).not.toBeNull() - expect(tooltipEl.getAttribute('aria-label')).toEqual('Different label') - done() + expect(tooltipShown).not.toBeNull() + expect(tooltipEl.getAttribute('aria-label')).toEqual('Different label') + resolve() + }) + + tooltip.show() }) - - tooltip.show() }) - it('should not add the aria-label attribute if the element has text content', done => { - fixtureEl.innerHTML = 'text content' + it('should not add the aria-label attribute if the element has text content', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = 'text content' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tooltipShown = document.querySelector('.tooltip') + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tooltipShown = document.querySelector('.tooltip') - expect(tooltipShown).not.toBeNull() - expect(tooltipEl.getAttribute('aria-label')).toBeNull() - done() + expect(tooltipShown).not.toBeNull() + expect(tooltipEl.getAttribute('aria-label')).toBeNull() + resolve() + }) + + tooltip.show() }) - - tooltip.show() }) }) diff --git a/js/tests/unit/util/backdrop.spec.js b/js/tests/unit/util/backdrop.spec.js index f9903c8327..73384fc90b 100644 --- a/js/tests/unit/util/backdrop.spec.js +++ b/js/tests/unit/util/backdrop.spec.js @@ -23,267 +23,297 @@ describe('Backdrop', () => { }) describe('show', () => { - it('should append the backdrop html once on show and include the "show" class if it is "shown"', done => { - const instance = new Backdrop({ - isVisible: true, - isAnimated: false - }) - const getElements = () => document.querySelectorAll(CLASS_BACKDROP) + it('should append the backdrop html once on show and include the "show" class if it is "shown"', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: true, + isAnimated: false + }) + const getElements = () => document.querySelectorAll(CLASS_BACKDROP) - expect(getElements()).toHaveSize(0) - - instance.show() - instance.show(() => { - expect(getElements()).toHaveSize(1) - for (const el of getElements()) { - expect(el).toHaveClass(CLASS_NAME_SHOW) - } - - done() - }) - }) - - it('should not append the backdrop html if it is not "shown"', done => { - const instance = new Backdrop({ - isVisible: false, - isAnimated: true - }) - const getElements = () => document.querySelectorAll(CLASS_BACKDROP) - - expect(getElements()).toHaveSize(0) - instance.show(() => { expect(getElements()).toHaveSize(0) - done() + + instance.show() + instance.show(() => { + expect(getElements()).toHaveSize(1) + for (const el of getElements()) { + expect(el).toHaveClass(CLASS_NAME_SHOW) + } + + resolve() + }) }) }) - it('should append the backdrop html once and include the "fade" class if it is "shown" and "animated"', done => { - const instance = new Backdrop({ - isVisible: true, - isAnimated: true + it('should not append the backdrop html if it is not "shown"', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: false, + isAnimated: true + }) + const getElements = () => document.querySelectorAll(CLASS_BACKDROP) + + expect(getElements()).toHaveSize(0) + instance.show(() => { + expect(getElements()).toHaveSize(0) + resolve() + }) }) - const getElements = () => document.querySelectorAll(CLASS_BACKDROP) + }) - expect(getElements()).toHaveSize(0) + it('should append the backdrop html once and include the "fade" class if it is "shown" and "animated"', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: true, + isAnimated: true + }) + const getElements = () => document.querySelectorAll(CLASS_BACKDROP) - instance.show(() => { - expect(getElements()).toHaveSize(1) - for (const el of getElements()) { - expect(el).toHaveClass(CLASS_NAME_FADE) - } + expect(getElements()).toHaveSize(0) - done() + instance.show(() => { + expect(getElements()).toHaveSize(1) + for (const el of getElements()) { + expect(el).toHaveClass(CLASS_NAME_FADE) + } + + resolve() + }) }) }) }) describe('hide', () => { - it('should remove the backdrop html', done => { - const instance = new Backdrop({ - isVisible: true, - isAnimated: true - }) + it('should remove the backdrop html', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: true, + isAnimated: true + }) - const getElements = () => document.body.querySelectorAll(CLASS_BACKDROP) + const getElements = () => document.body.querySelectorAll(CLASS_BACKDROP) - expect(getElements()).toHaveSize(0) - instance.show(() => { - expect(getElements()).toHaveSize(1) - instance.hide(() => { - expect(getElements()).toHaveSize(0) - done() + expect(getElements()).toHaveSize(0) + instance.show(() => { + expect(getElements()).toHaveSize(1) + instance.hide(() => { + expect(getElements()).toHaveSize(0) + resolve() + }) }) }) }) - it('should remove the "show" class', done => { - const instance = new Backdrop({ - isVisible: true, - isAnimated: true - }) - const elem = instance._getElement() + it('should remove the "show" class', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: true, + isAnimated: true + }) + const elem = instance._getElement() - instance.show() - instance.hide(() => { - expect(elem).not.toHaveClass(CLASS_NAME_SHOW) - done() - }) - }) - - it('should not try to remove Node on remove method if it is not "shown"', done => { - const instance = new Backdrop({ - isVisible: false, - isAnimated: true - }) - const getElements = () => document.querySelectorAll(CLASS_BACKDROP) - const spy = spyOn(instance, 'dispose').and.callThrough() - - expect(getElements()).toHaveSize(0) - expect(instance._isAppended).toBeFalse() - instance.show(() => { + instance.show() instance.hide(() => { - expect(getElements()).toHaveSize(0) - expect(spy).not.toHaveBeenCalled() - expect(instance._isAppended).toBeFalse() - done() + expect(elem).not.toHaveClass(CLASS_NAME_SHOW) + resolve() }) }) }) - it('should not error if the backdrop no longer has a parent', done => { - fixtureEl.innerHTML = '
' + it('should not try to remove Node on remove method if it is not "shown"', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: false, + isAnimated: true + }) + const getElements = () => document.querySelectorAll(CLASS_BACKDROP) + const spy = spyOn(instance, 'dispose').and.callThrough() - const wrapper = fixtureEl.querySelector('#wrapper') - const instance = new Backdrop({ - isVisible: true, - isAnimated: true, - rootElement: wrapper + expect(getElements()).toHaveSize(0) + expect(instance._isAppended).toBeFalse() + instance.show(() => { + instance.hide(() => { + expect(getElements()).toHaveSize(0) + expect(spy).not.toHaveBeenCalled() + expect(instance._isAppended).toBeFalse() + resolve() + }) + }) }) + }) - const getElements = () => document.querySelectorAll(CLASS_BACKDROP) + it('should not error if the backdrop no longer has a parent', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - instance.show(() => { - wrapper.remove() - instance.hide(() => { - expect(getElements()).toHaveSize(0) - done() + const wrapper = fixtureEl.querySelector('#wrapper') + const instance = new Backdrop({ + isVisible: true, + isAnimated: true, + rootElement: wrapper + }) + + const getElements = () => document.querySelectorAll(CLASS_BACKDROP) + + instance.show(() => { + wrapper.remove() + instance.hide(() => { + expect(getElements()).toHaveSize(0) + resolve() + }) }) }) }) }) describe('click callback', () => { - it('should execute callback on click', done => { - const spy = jasmine.createSpy('spy') + it('should execute callback on click', () => { + return new Promise(resolve => { + const spy = jasmine.createSpy('spy') - const instance = new Backdrop({ - isVisible: true, - isAnimated: false, - clickCallback: () => spy() - }) - const endTest = () => { - setTimeout(() => { - expect(spy).toHaveBeenCalled() - done() - }, 10) - } - - instance.show(() => { - const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true }) - document.querySelector(CLASS_BACKDROP).dispatchEvent(clickEvent) - endTest() - }) - }) - }) - - describe('animation callbacks', () => { - it('should show and hide backdrop after counting transition duration if it is animated', done => { - const instance = new Backdrop({ - isVisible: true, - isAnimated: true - }) - const spy2 = jasmine.createSpy('spy2') - - const execDone = () => { - setTimeout(() => { - expect(spy2).toHaveBeenCalledTimes(2) - done() - }, 10) - } - - instance.show(spy2) - instance.hide(() => { - spy2() - execDone() - }) - expect(spy2).not.toHaveBeenCalled() - }) - - it('should show and hide backdrop without a delay if it is not animated', done => { - const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) - const instance = new Backdrop({ - isVisible: true, - isAnimated: false - }) - const spy2 = jasmine.createSpy('spy2') - - instance.show(spy2) - instance.hide(spy2) - - setTimeout(() => { - expect(spy2).toHaveBeenCalled() - expect(spy).not.toHaveBeenCalled() - done() - }, 10) - }) - - it('should not call delay callbacks if it is not "shown"', done => { - const instance = new Backdrop({ - isVisible: false, - isAnimated: true - }) - const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) - - instance.show() - instance.hide(() => { - expect(spy).not.toHaveBeenCalled() - done() - }) - }) - }) - - describe('Config', () => { - describe('rootElement initialization', () => { - it('should be appended on "document.body" by default', done => { - const instance = new Backdrop({ - isVisible: true - }) - const getElement = () => document.querySelector(CLASS_BACKDROP) - instance.show(() => { - expect(getElement().parentElement).toEqual(document.body) - done() - }) - }) - - it('should find the rootElement if passed as a string', done => { const instance = new Backdrop({ isVisible: true, - rootElement: 'body' + isAnimated: false, + clickCallback: () => spy() }) - const getElement = () => document.querySelector(CLASS_BACKDROP) - instance.show(() => { - expect(getElement().parentElement).toEqual(document.body) - done() - }) - }) + const endTest = () => { + setTimeout(() => { + expect(spy).toHaveBeenCalled() + resolve() + }, 10) + } - it('should be appended on any element given by the proper config', done => { - fixtureEl.innerHTML = '
' - - const wrapper = fixtureEl.querySelector('#wrapper') - const instance = new Backdrop({ - isVisible: true, - rootElement: wrapper - }) - const getElement = () => document.querySelector(CLASS_BACKDROP) instance.show(() => { - expect(getElement().parentElement).toEqual(wrapper) - done() + const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true }) + document.querySelector(CLASS_BACKDROP).dispatchEvent(clickEvent) + endTest() }) }) }) - describe('ClassName', () => { - it('should allow configuring className', done => { - const instance = new Backdrop({ - isVisible: true, - className: 'foo' + describe('animation callbacks', () => { + it('should show and hide backdrop after counting transition duration if it is animated', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: true, + isAnimated: true + }) + const spy2 = jasmine.createSpy('spy2') + + const execDone = () => { + setTimeout(() => { + expect(spy2).toHaveBeenCalledTimes(2) + resolve() + }, 10) + } + + instance.show(spy2) + instance.hide(() => { + spy2() + execDone() + }) + expect(spy2).not.toHaveBeenCalled() }) - const getElement = () => document.querySelector('.foo') - instance.show(() => { - expect(getElement()).toEqual(instance._getElement()) - instance.dispose() - done() + }) + + it('should show and hide backdrop without a delay if it is not animated', () => { + return new Promise(resolve => { + const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) + const instance = new Backdrop({ + isVisible: true, + isAnimated: false + }) + const spy2 = jasmine.createSpy('spy2') + + instance.show(spy2) + instance.hide(spy2) + + setTimeout(() => { + expect(spy2).toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() + resolve() + }, 10) + }) + }) + + it('should not call delay callbacks if it is not "shown"', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: false, + isAnimated: true + }) + const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) + + instance.show() + instance.hide(() => { + expect(spy).not.toHaveBeenCalled() + resolve() + }) + }) + }) + }) + + describe('Config', () => { + describe('rootElement initialization', () => { + it('should be appended on "document.body" by default', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: true + }) + const getElement = () => document.querySelector(CLASS_BACKDROP) + instance.show(() => { + expect(getElement().parentElement).toEqual(document.body) + resolve() + }) + }) + }) + + it('should find the rootElement if passed as a string', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: true, + rootElement: 'body' + }) + const getElement = () => document.querySelector(CLASS_BACKDROP) + instance.show(() => { + expect(getElement().parentElement).toEqual(document.body) + resolve() + }) + }) + }) + + it('should be appended on any element given by the proper config', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + + const wrapper = fixtureEl.querySelector('#wrapper') + const instance = new Backdrop({ + isVisible: true, + rootElement: wrapper + }) + const getElement = () => document.querySelector(CLASS_BACKDROP) + instance.show(() => { + expect(getElement().parentElement).toEqual(wrapper) + resolve() + }) + }) + }) + }) + + describe('ClassName', () => { + it('should allow configuring className', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: true, + className: 'foo' + }) + const getElement = () => document.querySelector('.foo') + instance.show(() => { + expect(getElement()).toEqual(instance._getElement()) + instance.dispose() + resolve() + }) + }) }) }) }) diff --git a/js/tests/unit/util/focustrap.spec.js b/js/tests/unit/util/focustrap.spec.js index 52a7573971..55f6a23a7e 100644 --- a/js/tests/unit/util/focustrap.spec.js +++ b/js/tests/unit/util/focustrap.spec.js @@ -1,7 +1,7 @@ import FocusTrap from '../../../src/util/focustrap' import EventHandler from '../../../src/dom/event-handler' import SelectorEngine from '../../../src/dom/selector-engine' -import { clearFixture, getFixture, createEvent } from '../../helpers/fixture' +import { clearFixture, createEvent, getFixture } from '../../helpers/fixture' describe('FocusTrap', () => { let fixtureEl @@ -41,140 +41,148 @@ describe('FocusTrap', () => { expect(trapElement.focus).not.toHaveBeenCalled() }) - it('should force focus inside focus trap if it can', done => { - fixtureEl.innerHTML = [ - 'outside', - '
', - ' inside', - '
' - ].join('') + it('should force focus inside focus trap if it can', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + 'outside', + '
', + ' inside', + '
' + ].join('') - const trapElement = fixtureEl.querySelector('div') - const focustrap = new FocusTrap({ trapElement }) - focustrap.activate() + const trapElement = fixtureEl.querySelector('div') + const focustrap = new FocusTrap({ trapElement }) + focustrap.activate() - const inside = document.getElementById('inside') + const inside = document.getElementById('inside') - const focusInListener = () => { - expect(inside.focus).toHaveBeenCalled() - document.removeEventListener('focusin', focusInListener) - done() - } + const focusInListener = () => { + expect(inside.focus).toHaveBeenCalled() + document.removeEventListener('focusin', focusInListener) + resolve() + } - spyOn(inside, 'focus') - spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [inside]) + spyOn(inside, 'focus') + spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [inside]) - document.addEventListener('focusin', focusInListener) + document.addEventListener('focusin', focusInListener) - const focusInEvent = createEvent('focusin', { bubbles: true }) - Object.defineProperty(focusInEvent, 'target', { - value: document.getElementById('outside') + const focusInEvent = createEvent('focusin', { bubbles: true }) + Object.defineProperty(focusInEvent, 'target', { + value: document.getElementById('outside') + }) + + document.dispatchEvent(focusInEvent) }) - - document.dispatchEvent(focusInEvent) }) - it('should wrap focus around forward on tab', done => { - fixtureEl.innerHTML = [ - 'outside', - '
', - ' first', - ' inside', - ' last', - '
' - ].join('') + it('should wrap focus around forward on tab', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + 'outside', + '
', + ' first', + ' inside', + ' last', + '
' + ].join('') - const trapElement = fixtureEl.querySelector('div') - const focustrap = new FocusTrap({ trapElement }) - focustrap.activate() + const trapElement = fixtureEl.querySelector('div') + const focustrap = new FocusTrap({ trapElement }) + focustrap.activate() - const first = document.getElementById('first') - const inside = document.getElementById('inside') - const last = document.getElementById('last') - const outside = document.getElementById('outside') + const first = document.getElementById('first') + const inside = document.getElementById('inside') + const last = document.getElementById('last') + const outside = document.getElementById('outside') - spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last]) - spyOn(first, 'focus').and.callThrough() + spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last]) + spyOn(first, 'focus').and.callThrough() - const focusInListener = () => { - expect(first.focus).toHaveBeenCalled() - first.removeEventListener('focusin', focusInListener) - done() - } + const focusInListener = () => { + expect(first.focus).toHaveBeenCalled() + first.removeEventListener('focusin', focusInListener) + resolve() + } - first.addEventListener('focusin', focusInListener) + first.addEventListener('focusin', focusInListener) - const keydown = createEvent('keydown') - keydown.key = 'Tab' + const keydown = createEvent('keydown') + keydown.key = 'Tab' - document.dispatchEvent(keydown) - outside.focus() - }) - - it('should wrap focus around backwards on shift-tab', done => { - fixtureEl.innerHTML = [ - 'outside', - '
', - ' first', - ' inside', - ' last', - '
' - ].join('') - - const trapElement = fixtureEl.querySelector('div') - const focustrap = new FocusTrap({ trapElement }) - focustrap.activate() - - const first = document.getElementById('first') - const inside = document.getElementById('inside') - const last = document.getElementById('last') - const outside = document.getElementById('outside') - - spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last]) - spyOn(last, 'focus').and.callThrough() - - const focusInListener = () => { - expect(last.focus).toHaveBeenCalled() - last.removeEventListener('focusin', focusInListener) - done() - } - - last.addEventListener('focusin', focusInListener) - - const keydown = createEvent('keydown') - keydown.key = 'Tab' - keydown.shiftKey = true - - document.dispatchEvent(keydown) - outside.focus() - }) - - it('should force focus on itself if there is no focusable content', done => { - fixtureEl.innerHTML = [ - 'outside', - '
' - ].join('') - - const trapElement = fixtureEl.querySelector('div') - const focustrap = new FocusTrap({ trapElement }) - focustrap.activate() - - const focusInListener = () => { - expect(focustrap._config.trapElement.focus).toHaveBeenCalled() - document.removeEventListener('focusin', focusInListener) - done() - } - - spyOn(focustrap._config.trapElement, 'focus') - - document.addEventListener('focusin', focusInListener) - - const focusInEvent = createEvent('focusin', { bubbles: true }) - Object.defineProperty(focusInEvent, 'target', { - value: document.getElementById('outside') + document.dispatchEvent(keydown) + outside.focus() }) + }) - document.dispatchEvent(focusInEvent) + it('should wrap focus around backwards on shift-tab', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + 'outside', + '
', + ' first', + ' inside', + ' last', + '
' + ].join('') + + const trapElement = fixtureEl.querySelector('div') + const focustrap = new FocusTrap({ trapElement }) + focustrap.activate() + + const first = document.getElementById('first') + const inside = document.getElementById('inside') + const last = document.getElementById('last') + const outside = document.getElementById('outside') + + spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last]) + spyOn(last, 'focus').and.callThrough() + + const focusInListener = () => { + expect(last.focus).toHaveBeenCalled() + last.removeEventListener('focusin', focusInListener) + resolve() + } + + last.addEventListener('focusin', focusInListener) + + const keydown = createEvent('keydown') + keydown.key = 'Tab' + keydown.shiftKey = true + + document.dispatchEvent(keydown) + outside.focus() + }) + }) + + it('should force focus on itself if there is no focusable content', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + 'outside', + '
' + ].join('') + + const trapElement = fixtureEl.querySelector('div') + const focustrap = new FocusTrap({ trapElement }) + focustrap.activate() + + const focusInListener = () => { + expect(focustrap._config.trapElement.focus).toHaveBeenCalled() + document.removeEventListener('focusin', focusInListener) + resolve() + } + + spyOn(focustrap._config.trapElement, 'focus') + + document.addEventListener('focusin', focusInListener) + + const focusInEvent = createEvent('focusin', { bubbles: true }) + Object.defineProperty(focusInEvent, 'target', { + value: document.getElementById('outside') + }) + + document.dispatchEvent(focusInEvent) + }) }) }) diff --git a/js/tests/unit/util/index.spec.js b/js/tests/unit/util/index.spec.js index 9d8c5ed986..0d60ca9894 100644 --- a/js/tests/unit/util/index.spec.js +++ b/js/tests/unit/util/index.spec.js @@ -1,5 +1,6 @@ import * as Util from '../../../src/util/index' import { clearFixture, getFixture } from '../../helpers/fixture' +import { noop } from '../../../src/util/index' describe('Util', () => { let fixtureEl @@ -154,18 +155,20 @@ describe('Util', () => { }) describe('triggerTransitionEnd', () => { - it('should trigger transitionend event', done => { - fixtureEl.innerHTML = '
' + it('should trigger transitionend event', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const el = fixtureEl.querySelector('div') - const spy = spyOn(el, 'dispatchEvent').and.callThrough() + const el = fixtureEl.querySelector('div') + const spy = spyOn(el, 'dispatchEvent').and.callThrough() - el.addEventListener('transitionend', () => { - expect(spy).toHaveBeenCalled() - done() + el.addEventListener('transitionend', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) + + Util.triggerTransitionEnd(el) }) - - Util.triggerTransitionEnd(el) }) }) @@ -611,9 +614,9 @@ describe('Util', () => { }) it('should define a plugin on the jQuery instance', () => { - const pluginMock = function () {} + const pluginMock = Util.noop pluginMock.NAME = 'test' - pluginMock.jQueryInterface = function () {} + pluginMock.jQueryInterface = Util.noop Util.defineJQueryPlugin(pluginMock) expect(fakejQuery.fn.test).toEqual(pluginMock.jQueryInterface) @@ -658,96 +661,104 @@ describe('Util', () => { expect(callbackSpy).toHaveBeenCalled() }) - it('should execute a function after a computed CSS transition duration and there was no transitionend event dispatched', done => { - const el = document.createElement('div') - const callbackSpy = jasmine.createSpy('callback spy') + it('should execute a function after a computed CSS transition duration and there was no transitionend event dispatched', () => { + return new Promise(resolve => { + const el = document.createElement('div') + const callbackSpy = jasmine.createSpy('callback spy') - spyOn(window, 'getComputedStyle').and.returnValue({ - transitionDuration: '0.05s', - transitionDelay: '0s' + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.05s', + transitionDelay: '0s' + }) + + Util.executeAfterTransition(callbackSpy, el) + + setTimeout(() => { + expect(callbackSpy).toHaveBeenCalled() + resolve() + }, 70) }) - - Util.executeAfterTransition(callbackSpy, el) - - setTimeout(() => { - expect(callbackSpy).toHaveBeenCalled() - done() - }, 70) }) - it('should not execute a function a second time after a computed CSS transition duration and if a transitionend event has already been dispatched', done => { - const el = document.createElement('div') - const callbackSpy = jasmine.createSpy('callback spy') + it('should not execute a function a second time after a computed CSS transition duration and if a transitionend event has already been dispatched', () => { + return new Promise(resolve => { + const el = document.createElement('div') + const callbackSpy = jasmine.createSpy('callback spy') - spyOn(window, 'getComputedStyle').and.returnValue({ - transitionDuration: '0.05s', - transitionDelay: '0s' + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.05s', + transitionDelay: '0s' + }) + + Util.executeAfterTransition(callbackSpy, el) + + setTimeout(() => { + el.dispatchEvent(new TransitionEvent('transitionend')) + }, 50) + + setTimeout(() => { + expect(callbackSpy).toHaveBeenCalledTimes(1) + resolve() + }, 70) }) + }) - Util.executeAfterTransition(callbackSpy, el) + it('should not trigger a transitionend event if another transitionend event had already happened', () => { + return new Promise(resolve => { + const el = document.createElement('div') - setTimeout(() => { + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.05s', + transitionDelay: '0s' + }) + + Util.executeAfterTransition(noop, el) + + // simulate a event dispatched by the browser el.dispatchEvent(new TransitionEvent('transitionend')) - }, 50) - setTimeout(() => { - expect(callbackSpy).toHaveBeenCalledTimes(1) - done() - }, 70) + const dispatchSpy = spyOn(el, 'dispatchEvent').and.callThrough() + + setTimeout(() => { + // setTimeout should not have triggered another transitionend event. + expect(dispatchSpy).not.toHaveBeenCalled() + resolve() + }, 70) + }) }) - it('should not trigger a transitionend event if another transitionend event had already happened', done => { - const el = document.createElement('div') + it('should ignore transitionend events from nested elements', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + '
', + '
' + ].join('') - spyOn(window, 'getComputedStyle').and.returnValue({ - transitionDuration: '0.05s', - transitionDelay: '0s' + const outer = fixtureEl.querySelector('.outer') + const nested = fixtureEl.querySelector('.nested') + const callbackSpy = jasmine.createSpy('callback spy') + + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.05s', + transitionDelay: '0s' + }) + + Util.executeAfterTransition(callbackSpy, outer) + + nested.dispatchEvent(new TransitionEvent('transitionend', { + bubbles: true + })) + + setTimeout(() => { + expect(callbackSpy).not.toHaveBeenCalled() + }, 20) + + setTimeout(() => { + expect(callbackSpy).toHaveBeenCalled() + resolve() + }, 70) }) - - Util.executeAfterTransition(() => {}, el) - - // simulate a event dispatched by the browser - el.dispatchEvent(new TransitionEvent('transitionend')) - - const dispatchSpy = spyOn(el, 'dispatchEvent').and.callThrough() - - setTimeout(() => { - // setTimeout should not have triggered another transitionend event. - expect(dispatchSpy).not.toHaveBeenCalled() - done() - }, 70) - }) - - it('should ignore transitionend events from nested elements', done => { - fixtureEl.innerHTML = [ - '
', - '
', - '
' - ].join('') - - const outer = fixtureEl.querySelector('.outer') - const nested = fixtureEl.querySelector('.nested') - const callbackSpy = jasmine.createSpy('callback spy') - - spyOn(window, 'getComputedStyle').and.returnValue({ - transitionDuration: '0.05s', - transitionDelay: '0s' - }) - - Util.executeAfterTransition(callbackSpy, outer) - - nested.dispatchEvent(new TransitionEvent('transitionend', { - bubbles: true - })) - - setTimeout(() => { - expect(callbackSpy).not.toHaveBeenCalled() - }, 20) - - setTimeout(() => { - expect(callbackSpy).toHaveBeenCalled() - done() - }, 70) }) }) diff --git a/js/tests/unit/util/scrollbar.spec.js b/js/tests/unit/util/scrollbar.spec.js index fc3a6f4e87..6fcf5718bb 100644 --- a/js/tests/unit/util/scrollbar.spec.js +++ b/js/tests/unit/util/scrollbar.spec.js @@ -101,7 +101,7 @@ describe('ScrollBar', () => { }) describe('hide - reset', () => { - it('should adjust the inline padding of fixed elements which are full-width', done => { + it('should adjust the inline padding of fixed elements which are full-width', () => { fixtureEl.innerHTML = [ '
', '
', @@ -134,10 +134,9 @@ describe('ScrollBar', () => { expect(getPaddingAttr(fixedEl2)).toBeNull() expect(currentPadding).toEqual(originalPadding) expect(currentPadding2).toEqual(originalPadding2) - done() }) - it('should remove padding & margin if not existed before adjustment', done => { + it('should remove padding & margin if not existed before adjustment', () => { fixtureEl.innerHTML = [ '
', '
', @@ -155,10 +154,9 @@ describe('ScrollBar', () => { expect(fixedEl.getAttribute('style').includes('padding-right')).toBeFalse() expect(stickyEl.getAttribute('style').includes('margin-right')).toBeFalse() - done() }) - it('should adjust the inline margin and padding of sticky elements', done => { + it('should adjust the inline margin and padding of sticky elements', () => { fixtureEl.innerHTML = [ '
', '
', @@ -184,7 +182,6 @@ describe('ScrollBar', () => { expect(getMarginX(stickyTopEl)).toEqual(originalMargin) expect(getPaddingAttr(stickyTopEl)).toBeNull() expect(getPaddingX(stickyTopEl)).toEqual(originalPadding) - done() }) it('should not adjust the inline margin and padding of sticky and fixed elements when element do not have full width', () => { diff --git a/js/tests/unit/util/swipe.spec.js b/js/tests/unit/util/swipe.spec.js index 93131b8fdd..054aab6958 100644 --- a/js/tests/unit/util/swipe.spec.js +++ b/js/tests/unit/util/swipe.spec.js @@ -78,74 +78,80 @@ describe('Swipe', () => { }) describe('Config', () => { - it('Test leftCallback', done => { - const spyRight = jasmine.createSpy('spy') - clearPointerEvents() - defineDocumentElementOntouchstart() - // eslint-disable-next-line no-new - new Swipe(swipeEl, { - leftCallback: () => { - expect(spyRight).not.toHaveBeenCalled() - restorePointerEvents() - done() - }, - rightCallback: spyRight - }) + it('Test leftCallback', () => { + return new Promise(resolve => { + const spyRight = jasmine.createSpy('spy') + clearPointerEvents() + defineDocumentElementOntouchstart() + // eslint-disable-next-line no-new + new Swipe(swipeEl, { + leftCallback: () => { + expect(spyRight).not.toHaveBeenCalled() + restorePointerEvents() + resolve() + }, + rightCallback: spyRight + }) - mockSwipeGesture(swipeEl, { - pos: [300, 10], - deltaX: -300 + mockSwipeGesture(swipeEl, { + pos: [300, 10], + deltaX: -300 + }) }) }) - it('Test rightCallback', done => { - const spyLeft = jasmine.createSpy('spy') - clearPointerEvents() - defineDocumentElementOntouchstart() - // eslint-disable-next-line no-new - new Swipe(swipeEl, { - rightCallback: () => { - expect(spyLeft).not.toHaveBeenCalled() - restorePointerEvents() - done() - }, - leftCallback: spyLeft - }) + it('Test rightCallback', () => { + return new Promise(resolve => { + const spyLeft = jasmine.createSpy('spy') + clearPointerEvents() + defineDocumentElementOntouchstart() + // eslint-disable-next-line no-new + new Swipe(swipeEl, { + rightCallback: () => { + expect(spyLeft).not.toHaveBeenCalled() + restorePointerEvents() + resolve() + }, + leftCallback: spyLeft + }) - mockSwipeGesture(swipeEl, { - pos: [10, 10], - deltaX: 300 + mockSwipeGesture(swipeEl, { + pos: [10, 10], + deltaX: 300 + }) }) }) - it('Test endCallback', done => { - clearPointerEvents() - defineDocumentElementOntouchstart() - let isFirstTime = true + it('Test endCallback', () => { + return new Promise(resolve => { + clearPointerEvents() + defineDocumentElementOntouchstart() + let isFirstTime = true - const callback = () => { - if (isFirstTime) { - isFirstTime = false - return + const callback = () => { + if (isFirstTime) { + isFirstTime = false + return + } + + expect().nothing() + restorePointerEvents() + resolve() } - expect().nothing() - restorePointerEvents() - done() - } + // eslint-disable-next-line no-new + new Swipe(swipeEl, { + endCallback: callback + }) + mockSwipeGesture(swipeEl, { + pos: [10, 10], + deltaX: 300 + }) - // eslint-disable-next-line no-new - new Swipe(swipeEl, { - endCallback: callback - }) - mockSwipeGesture(swipeEl, { - pos: [10, 10], - deltaX: 300 - }) - - mockSwipeGesture(swipeEl, { - pos: [300, 10], - deltaX: -300 + mockSwipeGesture(swipeEl, { + pos: [300, 10], + deltaX: -300 + }) }) }) }) @@ -170,53 +176,57 @@ describe('Swipe', () => { expect(swipe._handleSwipe).not.toHaveBeenCalled() }) - it('should allow swipeRight and call "rightCallback" with pointer events', done => { - if (!supportPointerEvent) { - expect().nothing() - done() - return - } - - const style = '#fixture .pointer-event { touch-action: none !important; }' - fixtureEl.innerHTML += style - - defineDocumentElementOntouchstart() - // eslint-disable-next-line no-new - new Swipe(swipeEl, { - rightCallback: () => { - deleteDocumentElementOntouchstart() + it('should allow swipeRight and call "rightCallback" with pointer events', () => { + return new Promise(resolve => { + if (!supportPointerEvent) { expect().nothing() - done() + resolve() + return } - }) - mockSwipeGesture(swipeEl, { deltaX: 300 }, 'pointer') + const style = '#fixture .pointer-event { touch-action: none !important; }' + fixtureEl.innerHTML += style + + defineDocumentElementOntouchstart() + // eslint-disable-next-line no-new + new Swipe(swipeEl, { + rightCallback: () => { + deleteDocumentElementOntouchstart() + expect().nothing() + resolve() + } + }) + + mockSwipeGesture(swipeEl, { deltaX: 300 }, 'pointer') + }) }) - it('should allow swipeLeft and call "leftCallback" with pointer events', done => { - if (!supportPointerEvent) { - expect().nothing() - done() - return - } - - const style = '#fixture .pointer-event { touch-action: none !important; }' - fixtureEl.innerHTML += style - - defineDocumentElementOntouchstart() - // eslint-disable-next-line no-new - new Swipe(swipeEl, { - leftCallback: () => { + it('should allow swipeLeft and call "leftCallback" with pointer events', () => { + return new Promise(resolve => { + if (!supportPointerEvent) { expect().nothing() - deleteDocumentElementOntouchstart() - done() + resolve() + return } - }) - mockSwipeGesture(swipeEl, { - pos: [300, 10], - deltaX: -300 - }, 'pointer') + const style = '#fixture .pointer-event { touch-action: none !important; }' + fixtureEl.innerHTML += style + + defineDocumentElementOntouchstart() + // eslint-disable-next-line no-new + new Swipe(swipeEl, { + leftCallback: () => { + expect().nothing() + deleteDocumentElementOntouchstart() + resolve() + } + }) + + mockSwipeGesture(swipeEl, { + pos: [300, 10], + deltaX: -300 + }, 'pointer') + }) }) })