Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion js/src/base-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,16 @@ class BaseComponent extends Config {

// Private
_queueCallback(callback, element, isAnimated = true) {
executeAfterTransition(callback, element, isAnimated)
executeAfterTransition(() => {
// `dispose()` nulls every property of the instance, but it cannot cancel a
// callback already queued on a transition. Skip it rather than let it run
// against a disposed instance and throw
if (!this._element) {
return
}

callback()
}, element, isAnimated)
}

_getConfig(config) {
Expand Down
59 changes: 59 additions & 0 deletions js/tests/unit/tooltip.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,65 @@ describe('Tooltip', () => {
tooltip2.dispose()
expect(tooltipWithoutTitleEl.getAttribute('title')).toBeNull()
})

it('should not run a callback queued by hide() when disposed mid-transition', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>'

const tooltipEl = fixtureEl.querySelector('a')
const tooltip = new Tooltip(tooltipEl)
const hiddenSpy = jasmine.createSpy('hidden')
const errorSpy = jasmine.createSpy('error')

tooltipEl.addEventListener('hidden.bs.tooltip', hiddenSpy)

tooltipEl.addEventListener('shown.bs.tooltip', () => {
window.addEventListener('error', errorSpy)

// `hide()` queues its completion callback on the fade transition,
// `dispose()` then nulls every property before that callback runs
tooltip.hide()
tooltip.dispose()

setTimeout(() => {
window.removeEventListener('error', errorSpy)

expect(errorSpy).not.toHaveBeenCalled()
expect(hiddenSpy).not.toHaveBeenCalled()
resolve()
}, 50)
})

tooltip.show()
})
})

it('should not run a callback queued by show() when disposed mid-transition', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>'

const tooltipEl = fixtureEl.querySelector('a')
const tooltip = new Tooltip(tooltipEl)
const shownSpy = jasmine.createSpy('shown')
const errorSpy = jasmine.createSpy('error')

tooltipEl.addEventListener('shown.bs.tooltip', shownSpy)
window.addEventListener('error', errorSpy)

tooltip.show()
tooltip.dispose()

setTimeout(() => {
window.removeEventListener('error', errorSpy)

expect(errorSpy).not.toHaveBeenCalled()
expect(shownSpy).not.toHaveBeenCalled()
// the queued callback must not write back onto the disposed instance
expect(tooltip._isHovered).toBeNull()
resolve()
}, 50)
})
})
})

describe('show', () => {
Expand Down