| import Data from '../../src/dom/data.js' |
| import EventHandler from '../../src/dom/event-handler.js' |
| import Dialog from '../../src/dialog.js' |
| import { |
| clearBodyAndDocument, clearFixture, createEvent, getFixture |
| } from '../helpers/fixture.js' |
| |
| describe('Dialog', () => { |
| let fixtureEl |
| |
| beforeAll(() => { |
| fixtureEl = getFixture() |
| }) |
| |
| afterEach(() => { |
| clearFixture() |
| clearBodyAndDocument() |
| document.documentElement.classList.remove('dialog-open') |
| |
| for (const dialog of document.querySelectorAll('dialog[open]')) { |
| dialog.close() |
| } |
| }) |
| |
| beforeEach(() => { |
| clearBodyAndDocument() |
| }) |
| |
| describe('VERSION', () => { |
| it('should return plugin version', () => { |
| expect(Dialog.VERSION).toEqual(jasmine.any(String)) |
| }) |
| }) |
| |
| describe('Default', () => { |
| it('should return plugin default config', () => { |
| expect(Dialog.Default).toEqual(jasmine.any(Object)) |
| }) |
| }) |
| |
| describe('DATA_KEY', () => { |
| it('should return plugin data key', () => { |
| expect(Dialog.DATA_KEY).toEqual('bs.dialog') |
| }) |
| }) |
| |
| describe('constructor', () => { |
| it('should take care of element either passed as a CSS selector or DOM element', () => { |
| fixtureEl.innerHTML = '<dialog class="dialog" id="testDialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialogBySelector = new Dialog('#testDialog') |
| expect(dialogBySelector._element).toEqual(dialogEl) |
| |
| const dialogByElement = new Dialog(dialogEl) |
| expect(dialogByElement._element).toEqual(dialogEl) |
| }) |
| }) |
| |
| describe('toggle', () => { |
| it('should toggle the dialog open state', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| expect(dialogEl.open).toBeTrue() |
| dialog.toggle() |
| }) |
| |
| dialogEl.addEventListener('hidden.bs.dialog', () => { |
| expect(dialogEl.open).toBeFalse() |
| resolve() |
| }) |
| |
| dialog.toggle() |
| }) |
| }) |
| }) |
| |
| describe('show', () => { |
| it('should show a dialog', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| dialogEl.addEventListener('show.bs.dialog', event => { |
| expect(event).toBeDefined() |
| }) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| expect(dialogEl.open).toBeTrue() |
| expect(document.documentElement.classList.contains('dialog-open')).toBeTrue() |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should pass relatedTarget to show event', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = [ |
| '<button id="trigger"></button>', |
| '<dialog class="dialog"></dialog>' |
| ].join('') |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const trigger = fixtureEl.querySelector('#trigger') |
| const dialog = new Dialog(dialogEl) |
| |
| dialogEl.addEventListener('show.bs.dialog', event => { |
| expect(event.relatedTarget).toEqual(trigger) |
| }) |
| |
| dialogEl.addEventListener('shown.bs.dialog', event => { |
| expect(event.relatedTarget).toEqual(trigger) |
| resolve() |
| }) |
| |
| dialog.show(trigger) |
| }) |
| }) |
| |
| it('should do nothing if a dialog is already open', () => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| // Manually open the dialog |
| dialogEl.showModal() |
| |
| const spy = spyOn(EventHandler, 'trigger') |
| dialog.show() |
| |
| expect(spy).not.toHaveBeenCalled() |
| }) |
| |
| it('should do nothing if a dialog is transitioning', () => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| const spy = spyOn(EventHandler, 'trigger') |
| dialog._isTransitioning = true |
| |
| dialog.show() |
| |
| expect(spy).not.toHaveBeenCalled() |
| }) |
| |
| it('should not fire shown event when show is prevented', () => { |
| return new Promise((resolve, reject) => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| dialogEl.addEventListener('show.bs.dialog', event => { |
| event.preventDefault() |
| |
| const expectedDone = () => { |
| expect().nothing() |
| resolve() |
| } |
| |
| setTimeout(expectedDone, 10) |
| }) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| reject(new Error('shown event triggered')) |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should set is transitioning if fade class is present', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog fade"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| dialogEl.addEventListener('show.bs.dialog', () => { |
| setTimeout(() => { |
| expect(dialog._isTransitioning).toBeTrue() |
| }) |
| }) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| expect(dialog._isTransitioning).toBeFalse() |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should close dialog when a click occurred on data-bs-dismiss="dialog" inside dialog', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = [ |
| '<dialog class="dialog">', |
| ' <div class="dialog-header">', |
| ' <button type="button" data-bs-dismiss="dialog"></button>', |
| ' </div>', |
| '</dialog>' |
| ].join('') |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const btnClose = fixtureEl.querySelector('[data-bs-dismiss="dialog"]') |
| const dialog = new Dialog(dialogEl) |
| |
| const spy = spyOn(dialog, 'hide').and.callThrough() |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| btnClose.click() |
| }) |
| |
| dialogEl.addEventListener('hidden.bs.dialog', () => { |
| expect(spy).toHaveBeenCalled() |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| }) |
| |
| describe('hide', () => { |
| it('should hide a dialog', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| dialog.hide() |
| }) |
| |
| dialogEl.addEventListener('hide.bs.dialog', event => { |
| expect(event).toBeDefined() |
| }) |
| |
| dialogEl.addEventListener('hidden.bs.dialog', () => { |
| expect(dialogEl.open).toBeFalse() |
| expect(document.documentElement.classList.contains('dialog-open')).toBeFalse() |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should do nothing if the dialog is not shown', () => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| dialog.hide() |
| |
| expect().nothing() |
| }) |
| |
| it('should do nothing if the dialog is transitioning', () => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| dialogEl.showModal() |
| dialog._isTransitioning = true |
| dialog.hide() |
| |
| expect().nothing() |
| }) |
| |
| it('should not hide a dialog if hide is prevented', () => { |
| return new Promise((resolve, reject) => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| dialog.hide() |
| }) |
| |
| const hideCallback = () => { |
| setTimeout(() => { |
| expect(dialogEl.open).toBeTrue() |
| resolve() |
| }, 10) |
| } |
| |
| dialogEl.addEventListener('hide.bs.dialog', event => { |
| event.preventDefault() |
| hideCallback() |
| }) |
| |
| dialogEl.addEventListener('hidden.bs.dialog', () => { |
| reject(new Error('should not trigger hidden')) |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should close dialog when backdrop is clicked', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| const spy = spyOn(dialog, 'hide').and.callThrough() |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| // Click directly on the dialog element (backdrop area) |
| const clickEvent = createEvent('click') |
| Object.defineProperty(clickEvent, 'target', { value: dialogEl }) |
| dialogEl.dispatchEvent(clickEvent) |
| }) |
| |
| dialogEl.addEventListener('hidden.bs.dialog', () => { |
| expect(spy).toHaveBeenCalled() |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should not close dialog when clicking inside dialog content', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = [ |
| '<dialog class="dialog">', |
| ' <div class="dialog-body">Content</div>', |
| '</dialog>' |
| ].join('') |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialogBody = fixtureEl.querySelector('.dialog-body') |
| const dialog = new Dialog(dialogEl) |
| |
| const spy = spyOn(dialog, 'hide') |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| // Click on inner content - should not close |
| const clickEvent = createEvent('click', { bubbles: true }) |
| dialogBody.dispatchEvent(clickEvent) |
| |
| setTimeout(() => { |
| expect(spy).not.toHaveBeenCalled() |
| resolve() |
| }, 10) |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| }) |
| |
| describe('backdrop static', () => { |
| it('should not close dialog when backdrop is static and backdrop is clicked', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl, { |
| backdrop: 'static' |
| }) |
| |
| const spy = spyOn(dialog, 'hide') |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| const clickEvent = createEvent('click') |
| Object.defineProperty(clickEvent, 'target', { value: dialogEl }) |
| dialogEl.dispatchEvent(clickEvent) |
| |
| setTimeout(() => { |
| expect(spy).not.toHaveBeenCalled() |
| expect(dialogEl.open).toBeTrue() |
| resolve() |
| }, 10) |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should add dialog-static class when backdrop is static and clicked', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl, { |
| backdrop: 'static' |
| }) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| const clickEvent = createEvent('click') |
| Object.defineProperty(clickEvent, 'target', { value: dialogEl }) |
| dialogEl.dispatchEvent(clickEvent) |
| |
| expect(dialogEl.classList.contains('dialog-static')).toBeTrue() |
| |
| setTimeout(() => { |
| expect(dialogEl.classList.contains('dialog-static')).toBeFalse() |
| resolve() |
| }, 300) |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should fire hidePrevented.bs.dialog event when static backdrop is clicked', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl, { |
| backdrop: 'static' |
| }) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| const clickEvent = createEvent('click') |
| Object.defineProperty(clickEvent, 'target', { value: dialogEl }) |
| dialogEl.dispatchEvent(clickEvent) |
| }) |
| |
| dialogEl.addEventListener('hidePrevented.bs.dialog', () => { |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| }) |
| |
| describe('non-modal dialogs', () => { |
| it('should open a non-modal dialog with show() when modal = false', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl, { |
| modal: false |
| }) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| expect(dialogEl.open).toBeTrue() |
| expect(dialogEl.classList.contains('dialog-nonmodal')).toBeTrue() |
| // Non-modal dialogs should not add dialog-open to the root element |
| expect(document.documentElement.classList.contains('dialog-open')).toBeFalse() |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should remove dialog-nonmodal class on hide', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl, { |
| modal: false |
| }) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| expect(dialogEl.classList.contains('dialog-nonmodal')).toBeTrue() |
| dialog.hide() |
| }) |
| |
| dialogEl.addEventListener('hidden.bs.dialog', () => { |
| expect(dialogEl.classList.contains('dialog-nonmodal')).toBeFalse() |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should not respond to backdrop clicks for non-modal dialogs', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl, { |
| modal: false |
| }) |
| |
| const spy = spyOn(dialog, 'hide') |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| const clickEvent = createEvent('click') |
| Object.defineProperty(clickEvent, 'target', { value: dialogEl }) |
| dialogEl.dispatchEvent(clickEvent) |
| |
| setTimeout(() => { |
| expect(spy).not.toHaveBeenCalled() |
| resolve() |
| }, 10) |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should close non-modal dialog with escape key when keyboard = true', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl, { |
| modal: false, |
| keyboard: true |
| }) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| const keydownEvent = createEvent('keydown') |
| keydownEvent.key = 'Escape' |
| dialogEl.dispatchEvent(keydownEvent) |
| }) |
| |
| dialogEl.addEventListener('hidden.bs.dialog', () => { |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should not close non-modal dialog with escape key when keyboard = false', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl, { |
| modal: false, |
| keyboard: false |
| }) |
| |
| const spy = spyOn(dialog, 'hide') |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| const keydownEvent = createEvent('keydown') |
| keydownEvent.key = 'Escape' |
| dialogEl.dispatchEvent(keydownEvent) |
| |
| setTimeout(() => { |
| expect(spy).not.toHaveBeenCalled() |
| resolve() |
| }, 10) |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should use data-bs-modal="false" to create non-modal dialog', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = [ |
| '<button data-bs-toggle="dialog" data-bs-target="#exampleDialog" data-bs-modal="false"></button>', |
| '<dialog id="exampleDialog" class="dialog"></dialog>' |
| ].join('') |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const trigger = fixtureEl.querySelector('[data-bs-toggle="dialog"]') |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| const dialog = Dialog.getInstance(dialogEl) |
| expect(dialog._config.modal).toBeFalse() |
| expect(dialogEl.classList.contains('dialog-nonmodal')).toBeTrue() |
| resolve() |
| }) |
| |
| trigger.click() |
| }) |
| }) |
| }) |
| |
| describe('handleUpdate', () => { |
| it('should exist for API consistency', () => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| expect(typeof dialog.handleUpdate).toEqual('function') |
| // Should not throw |
| dialog.handleUpdate() |
| }) |
| }) |
| |
| describe('keyboard', () => { |
| it('should close dialog when escape key is pressed', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| const cancelEvent = createEvent('cancel') |
| dialogEl.dispatchEvent(cancelEvent) |
| }) |
| |
| dialogEl.addEventListener('hidden.bs.dialog', () => { |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should fire cancel.bs.dialog event when escape is pressed', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| const cancelEvent = createEvent('cancel') |
| dialogEl.dispatchEvent(cancelEvent) |
| }) |
| |
| dialogEl.addEventListener('cancel.bs.dialog', () => { |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should not close dialog when escape key is pressed with keyboard = false', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl, { |
| keyboard: false |
| }) |
| |
| const spy = spyOn(dialog, 'hide') |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| const cancelEvent = createEvent('cancel') |
| dialogEl.dispatchEvent(cancelEvent) |
| |
| setTimeout(() => { |
| expect(spy).not.toHaveBeenCalled() |
| expect(dialogEl.open).toBeTrue() |
| resolve() |
| }, 10) |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should show static backdrop animation when escape pressed and keyboard = false', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl, { |
| keyboard: false |
| }) |
| |
| const spy = spyOn(dialog, '_triggerBackdropTransition').and.callThrough() |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| const cancelEvent = createEvent('cancel') |
| dialogEl.dispatchEvent(cancelEvent) |
| |
| setTimeout(() => { |
| expect(spy).toHaveBeenCalled() |
| resolve() |
| }, 10) |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| }) |
| |
| describe('dispose', () => { |
| it('should dispose a dialog', () => { |
| fixtureEl.innerHTML = '<dialog class="dialog" id="exampleDialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| expect(Dialog.getInstance(dialogEl)).toEqual(dialog) |
| |
| const spyOff = spyOn(EventHandler, 'off') |
| |
| dialog.dispose() |
| |
| expect(Dialog.getInstance(dialogEl)).toBeNull() |
| expect(spyOff).toHaveBeenCalled() |
| }) |
| |
| it('should close the dialog and restore body scroll when disposed while open', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog" id="exampleDialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| expect(dialogEl.open).toBeTrue() |
| expect(document.documentElement.classList.contains('dialog-open')).toBeTrue() |
| |
| dialog.dispose() |
| |
| expect(dialogEl.open).toBeFalse() |
| expect(document.documentElement.classList.contains('dialog-open')).toBeFalse() |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should remove the native cancel listener on dispose', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog" id="exampleDialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| const spy = jasmine.createSpy('cancel') |
| |
| dialogEl.addEventListener('cancel.bs.dialog', spy) |
| |
| dialog.dispose() |
| |
| // After dispose, a native cancel event must not reach the component |
| dialogEl.dispatchEvent(createEvent('cancel')) |
| |
| setTimeout(() => { |
| expect(spy).not.toHaveBeenCalled() |
| resolve() |
| }, 10) |
| }) |
| }) |
| |
| it('should keep a consumer cancel listener after dispose', () => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| const consumerSpy = jasmine.createSpy('consumerCancel') |
| |
| // A consumer registers its own native cancel listener via EventHandler |
| EventHandler.on(dialogEl, 'cancel', consumerSpy) |
| |
| dialog.dispose() |
| |
| // dispose removes only the component's handler; the consumer's survives |
| dialogEl.dispatchEvent(createEvent('cancel')) |
| |
| expect(consumerSpy).toHaveBeenCalled() |
| }) |
| }) |
| |
| describe('data-api', () => { |
| it('should toggle dialog', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = [ |
| '<button type="button" data-bs-toggle="dialog" data-bs-target="#exampleDialog"></button>', |
| '<dialog id="exampleDialog" class="dialog"></dialog>' |
| ].join('') |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const trigger = fixtureEl.querySelector('[data-bs-toggle="dialog"]') |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| expect(dialogEl.open).toBeTrue() |
| setTimeout(() => trigger.click(), 10) |
| }) |
| |
| dialogEl.addEventListener('hidden.bs.dialog', () => { |
| expect(dialogEl.open).toBeFalse() |
| resolve() |
| }) |
| |
| trigger.click() |
| }) |
| }) |
| |
| it('should not open the dialog when the show event is prevented', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = [ |
| '<button type="button" data-bs-toggle="dialog" data-bs-target="#exampleDialog"></button>', |
| '<dialog id="exampleDialog" class="dialog"></dialog>' |
| ].join('') |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const trigger = fixtureEl.querySelector('[data-bs-toggle="dialog"]') |
| |
| dialogEl.addEventListener('show.bs.dialog', event => event.preventDefault()) |
| |
| trigger.click() |
| |
| setTimeout(() => { |
| expect(dialogEl.open).toBeFalse() |
| resolve() |
| }, 10) |
| }) |
| }) |
| |
| it('should not recreate a new dialog', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = [ |
| '<button type="button" data-bs-toggle="dialog" data-bs-target="#exampleDialog"></button>', |
| '<dialog id="exampleDialog" class="dialog"></dialog>' |
| ].join('') |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| const trigger = fixtureEl.querySelector('[data-bs-toggle="dialog"]') |
| |
| const spy = spyOn(dialog, 'toggle').and.callThrough() |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| expect(spy).toHaveBeenCalled() |
| resolve() |
| }) |
| |
| trigger.click() |
| }) |
| }) |
| |
| it('should prevent default when the trigger is <a> or <area>', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = [ |
| '<a data-bs-toggle="dialog" href="#" data-bs-target="#exampleDialog"></a>', |
| '<dialog id="exampleDialog" class="dialog"></dialog>' |
| ].join('') |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const trigger = fixtureEl.querySelector('[data-bs-toggle="dialog"]') |
| |
| const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough() |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| expect(dialogEl.open).toBeTrue() |
| expect(spy).toHaveBeenCalled() |
| resolve() |
| }) |
| |
| trigger.click() |
| }) |
| }) |
| |
| it('should focus the trigger on hide', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = [ |
| '<button data-bs-toggle="dialog" data-bs-target="#exampleDialog"></button>', |
| '<dialog id="exampleDialog" class="dialog"></dialog>' |
| ].join('') |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const trigger = fixtureEl.querySelector('[data-bs-toggle="dialog"]') |
| |
| const spy = spyOn(trigger, 'focus') |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| const dialog = Dialog.getInstance(dialogEl) |
| dialog.hide() |
| }) |
| |
| const hideListener = () => { |
| setTimeout(() => { |
| expect(spy).toHaveBeenCalledWith({ preventScroll: true }) |
| resolve() |
| }, 20) |
| } |
| |
| dialogEl.addEventListener('hidden.bs.dialog', () => { |
| hideListener() |
| }) |
| |
| trigger.click() |
| }) |
| }) |
| |
| it('should use data attributes for config', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = [ |
| '<button data-bs-toggle="dialog" data-bs-target="#exampleDialog" data-bs-backdrop="static"></button>', |
| '<dialog id="exampleDialog" class="dialog"></dialog>' |
| ].join('') |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const trigger = fixtureEl.querySelector('[data-bs-toggle="dialog"]') |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| const dialog = Dialog.getInstance(dialogEl) |
| expect(dialog._config.backdrop).toEqual('static') |
| resolve() |
| }) |
| |
| trigger.click() |
| }) |
| }) |
| }) |
| |
| describe('dialog swapping', () => { |
| it('should swap dialogs when trigger is inside an open dialog', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = [ |
| '<button data-bs-toggle="dialog" data-bs-target="#dialog1">Open first</button>', |
| '<dialog id="dialog1" class="dialog">', |
| ' <button data-bs-toggle="dialog" data-bs-target="#dialog2">Go to second</button>', |
| '</dialog>', |
| '<dialog id="dialog2" class="dialog"></dialog>' |
| ].join('') |
| |
| const dialog1El = fixtureEl.querySelector('#dialog1') |
| const dialog2El = fixtureEl.querySelector('#dialog2') |
| const firstTrigger = fixtureEl.querySelector('[data-bs-target="#dialog1"]') |
| const swapTrigger = dialog1El.querySelector('[data-bs-target="#dialog2"]') |
| |
| dialog1El.addEventListener('shown.bs.dialog', () => { |
| // Now click the swap trigger inside dialog1 |
| swapTrigger.click() |
| }) |
| |
| dialog2El.addEventListener('shown.bs.dialog', () => { |
| expect(dialog2El.open).toBeTrue() |
| }) |
| |
| dialog1El.addEventListener('hidden.bs.dialog', () => { |
| expect(dialog1El.open).toBeFalse() |
| expect(dialog2El.open).toBeTrue() |
| resolve() |
| }) |
| |
| firstTrigger.click() |
| }) |
| }) |
| }) |
| |
| describe('getInstance', () => { |
| it('should return dialog instance', () => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| expect(Dialog.getInstance(dialogEl)).toEqual(dialog) |
| expect(Dialog.getInstance(dialogEl)).toBeInstanceOf(Dialog) |
| }) |
| |
| it('should return null when there is no dialog instance', () => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('dialog') |
| |
| expect(Dialog.getInstance(dialogEl)).toBeNull() |
| }) |
| }) |
| |
| describe('getOrCreateInstance', () => { |
| it('should return dialog instance', () => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| expect(Dialog.getOrCreateInstance(dialogEl)).toEqual(dialog) |
| expect(Dialog.getInstance(dialogEl)).toEqual(Dialog.getOrCreateInstance(dialogEl, {})) |
| expect(Dialog.getOrCreateInstance(dialogEl)).toBeInstanceOf(Dialog) |
| }) |
| |
| it('should return new instance when there is no dialog instance', () => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('dialog') |
| |
| expect(Dialog.getInstance(dialogEl)).toBeNull() |
| expect(Dialog.getOrCreateInstance(dialogEl)).toBeInstanceOf(Dialog) |
| }) |
| |
| it('should return new instance when there is no dialog instance with given configuration', () => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('dialog') |
| |
| expect(Dialog.getInstance(dialogEl)).toBeNull() |
| const dialog = Dialog.getOrCreateInstance(dialogEl, { |
| backdrop: 'static' |
| }) |
| expect(dialog).toBeInstanceOf(Dialog) |
| expect(dialog._config.backdrop).toEqual('static') |
| }) |
| |
| it('should return the instance when exists without given configuration', () => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('dialog') |
| const dialog = new Dialog(dialogEl, { |
| backdrop: 'static' |
| }) |
| expect(Dialog.getInstance(dialogEl)).toEqual(dialog) |
| |
| const dialog2 = Dialog.getOrCreateInstance(dialogEl, { |
| backdrop: true |
| }) |
| expect(dialog).toBeInstanceOf(Dialog) |
| expect(dialog2).toEqual(dialog) |
| |
| expect(dialog2._config.backdrop).toEqual('static') |
| }) |
| }) |
| |
| describe('child component cleanup', () => { |
| it('should hide tooltip instances inside dialog when dialog closes', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = [ |
| '<dialog class="dialog">', |
| ' <button data-bs-toggle="tooltip" title="tip">Hover</button>', |
| '</dialog>' |
| ].join('') |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const tooltipTrigger = fixtureEl.querySelector('[data-bs-toggle="tooltip"]') |
| const dialog = new Dialog(dialogEl) |
| |
| const fakeTooltip = { hide: jasmine.createSpy('tooltipHide') } |
| Data.set(tooltipTrigger, 'bs.tooltip', fakeTooltip) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| dialog.hide() |
| }) |
| |
| dialogEl.addEventListener('hidden.bs.dialog', () => { |
| expect(fakeTooltip.hide).toHaveBeenCalled() |
| Data.remove(tooltipTrigger, 'bs.tooltip') |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should hide popover instances inside dialog when dialog closes', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = [ |
| '<dialog class="dialog">', |
| ' <button data-bs-toggle="popover" title="pop">Click</button>', |
| '</dialog>' |
| ].join('') |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const popoverTrigger = fixtureEl.querySelector('[data-bs-toggle="popover"]') |
| const dialog = new Dialog(dialogEl) |
| |
| const fakePopover = { hide: jasmine.createSpy('popoverHide') } |
| Data.set(popoverTrigger, 'bs.popover', fakePopover) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| dialog.hide() |
| }) |
| |
| dialogEl.addEventListener('hidden.bs.dialog', () => { |
| expect(fakePopover.hide).toHaveBeenCalled() |
| Data.remove(popoverTrigger, 'bs.popover') |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should hide toast instances inside dialog when dialog closes', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = [ |
| '<dialog class="dialog">', |
| ' <div class="toast show">Toast content</div>', |
| '</dialog>' |
| ].join('') |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const toastEl = fixtureEl.querySelector('.toast') |
| const dialog = new Dialog(dialogEl) |
| |
| const fakeToast = { hide: jasmine.createSpy('toastHide') } |
| Data.set(toastEl, 'bs.toast', fakeToast) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| dialog.hide() |
| }) |
| |
| dialogEl.addEventListener('hidden.bs.dialog', () => { |
| expect(fakeToast.hide).toHaveBeenCalled() |
| Data.remove(toastEl, 'bs.toast') |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| }) |
| |
| describe('stacked modals', () => { |
| it('should keep dialog-open on the root element when closing one of two open modal dialogs', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = [ |
| '<dialog id="dialog1" class="dialog"></dialog>', |
| '<dialog id="dialog2" class="dialog"></dialog>' |
| ].join('') |
| |
| const dialog1El = fixtureEl.querySelector('#dialog1') |
| const dialog2El = fixtureEl.querySelector('#dialog2') |
| const dialog1 = new Dialog(dialog1El) |
| const dialog2 = new Dialog(dialog2El) |
| |
| dialog1El.addEventListener('shown.bs.dialog', () => { |
| dialog2.show() |
| }) |
| |
| dialog2El.addEventListener('shown.bs.dialog', () => { |
| expect(document.documentElement.classList.contains('dialog-open')).toBeTrue() |
| dialog1.hide() |
| }) |
| |
| dialog1El.addEventListener('hidden.bs.dialog', () => { |
| expect(dialog2El.open).toBeTrue() |
| expect(document.documentElement.classList.contains('dialog-open')).toBeTrue() |
| dialog2.hide() |
| }) |
| |
| dialog2El.addEventListener('hidden.bs.dialog', () => { |
| expect(document.documentElement.classList.contains('dialog-open')).toBeFalse() |
| resolve() |
| }) |
| |
| dialog1.show() |
| }) |
| }) |
| }) |
| |
| describe('dialog-instant', () => { |
| it('should show and fire shown event when dialog-instant class is present', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog dialog-instant"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| expect(dialog._isTransitioning).toBeFalse() |
| expect(dialogEl.open).toBeTrue() |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should not report as animated when dialog-instant is present', () => { |
| fixtureEl.innerHTML = '<dialog class="dialog dialog-instant"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| expect(dialog._isAnimated()).toBeFalse() |
| }) |
| |
| it('should report as animated when dialog-instant is not present', () => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| expect(dialog._isAnimated()).toBeTrue() |
| }) |
| }) |
| |
| describe('hiding class', () => { |
| it('should add hiding class during hide and remove after hidden', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| dialog.hide() |
| // hiding class should be present during the transition |
| expect(dialogEl.classList.contains('hiding')).toBeTrue() |
| }) |
| |
| dialogEl.addEventListener('hidden.bs.dialog', () => { |
| expect(dialogEl.classList.contains('hiding')).toBeFalse() |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| }) |
| |
| describe('hidePrevented', () => { |
| it('should not add dialog-static class when hidePrevented is default prevented', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl, { |
| backdrop: 'static' |
| }) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| dialogEl.addEventListener('hidePrevented.bs.dialog', event => { |
| event.preventDefault() |
| |
| setTimeout(() => { |
| expect(dialogEl.classList.contains('dialog-static')).toBeFalse() |
| resolve() |
| }, 10) |
| }) |
| |
| const clickEvent = createEvent('click') |
| Object.defineProperty(clickEvent, 'target', { value: dialogEl }) |
| dialogEl.dispatchEvent(clickEvent) |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| }) |
| |
| describe('non-modal keyboard', () => { |
| it('should fire cancel.bs.dialog event on Escape for non-modal dialog', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl, { |
| modal: false, |
| keyboard: true |
| }) |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| const keydownEvent = createEvent('keydown') |
| keydownEvent.key = 'Escape' |
| dialogEl.dispatchEvent(keydownEvent) |
| }) |
| |
| dialogEl.addEventListener('cancel.bs.dialog', () => { |
| resolve() |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| |
| it('should not close and not trigger backdrop transition for non-modal with keyboard = false on Escape', () => { |
| return new Promise(resolve => { |
| fixtureEl.innerHTML = '<dialog class="dialog"></dialog>' |
| |
| const dialogEl = fixtureEl.querySelector('.dialog') |
| const dialog = new Dialog(dialogEl, { |
| modal: false, |
| keyboard: false |
| }) |
| |
| const hideSpy = spyOn(dialog, 'hide') |
| const backdropSpy = spyOn(dialog, '_triggerBackdropTransition') |
| |
| dialogEl.addEventListener('shown.bs.dialog', () => { |
| const keydownEvent = createEvent('keydown') |
| keydownEvent.key = 'Escape' |
| dialogEl.dispatchEvent(keydownEvent) |
| |
| setTimeout(() => { |
| expect(hideSpy).not.toHaveBeenCalled() |
| expect(backdropSpy).not.toHaveBeenCalled() |
| resolve() |
| }, 10) |
| }) |
| |
| dialog.show() |
| }) |
| }) |
| }) |
| }) |