diff --git a/docs/getting-started/support.md b/docs/getting-started/support.md
index 92112abd3b..348b1893f6 100644
--- a/docs/getting-started/support.md
+++ b/docs/getting-started/support.md
@@ -180,6 +180,9 @@ The `.dropdown-backdrop` element isn't used on iOS in the nav because of the com
Page zooming inevitably presents rendering artifacts in some components, both in Bootstrap and the rest of the web. Depending on the issue, we may be able to fix it (search first and then open an issue if need be). However, we tend to ignore these as they often have no direct solution other than hacky workarounds.
+### Sticky `:hover`/`:focus` on mobile
+Even though real hovering isn't possible on most touchscreens, most mobile browsers emulate hovering support and make `:hover` "sticky". In other words, `:hover` styles start applying after tapping an element and only stop applying after the user taps some other element. This can cause Bootstrap's `:hover` states to become unwantedly "stuck" on such browsers. Some mobile browsers also make `:focus` similarly sticky. There is currently no simple workaround for these issues other than removing such styles entirely.
+
### Printing
Even in some modern browsers, printing can be quirky.
diff --git a/js/tests/unit/affix.js b/js/tests/unit/affix.js
index 3152d8d2e4..040fe98030 100644
--- a/js/tests/unit/affix.js
+++ b/js/tests/unit/affix.js
@@ -35,8 +35,8 @@ $(function () {
ok(!$affix.hasClass('affix'), 'affix class was not added')
})
- test('should trigger affixed event after affix', function () {
- stop()
+ test('should trigger affixed event after affix', function (assert) {
+ var done = assert.async()
var templateHTML = '
'
+ '
'
@@ -57,7 +57,7 @@ $(function () {
}).on('affixed.bs.affix', function () {
ok(true, 'affixed event fired')
$('#affixTarget, #affixAfter').remove()
- start()
+ done()
})
setTimeout(function () {
@@ -69,8 +69,8 @@ $(function () {
}, 0)
})
- test('should affix-top when scrolling up to offset when parent has padding', function () {
- stop()
+ test('should affix-top when scrolling up to offset when parent has padding', function (assert) {
+ var done = assert.async()
var templateHTML = '
'
+ '
'
@@ -87,7 +87,7 @@ $(function () {
.on('affixed-top.bs.affix', function () {
ok($('#affixTopTarget').hasClass('affix-top'), 'affix-top class applied')
$('#padding-offset').remove()
- start()
+ done()
})
setTimeout(function () {
diff --git a/js/tests/unit/alert.js b/js/tests/unit/alert.js
index bc4eed6765..140a148385 100644
--- a/js/tests/unit/alert.js
+++ b/js/tests/unit/alert.js
@@ -55,13 +55,13 @@ $(function () {
equal($('#qunit-fixture').find('.alert').length, 0, 'element removed from dom')
})
- test('should not fire closed when close is prevented', function () {
- stop()
+ test('should not fire closed when close is prevented', function (assert) {
+ var done = assert.async()
$('')
.on('close.bs.alert', function (e) {
e.preventDefault()
ok(true, 'close event fired')
- start()
+ done()
})
.on('closed.bs.alert', function () {
ok(false, 'closed event fired')
diff --git a/js/tests/unit/button.js b/js/tests/unit/button.js
index 320996483f..cb51d40b95 100644
--- a/js/tests/unit/button.js
+++ b/js/tests/unit/button.js
@@ -29,57 +29,57 @@ $(function () {
strictEqual($button[0], $el[0], 'collection contains element')
})
- test('should return set state to loading', function () {
+ test('should return set state to loading', function (assert) {
var $btn = $('')
equal($btn.html(), 'mdo', 'btn text equals mdo')
$btn.bootstrapButton('loading')
- stop()
+ var done = assert.async()
setTimeout(function () {
equal($btn.html(), 'fat', 'btn text equals fat')
ok($btn[0].hasAttribute('disabled'), 'btn is disabled')
ok($btn.hasClass('disabled'), 'btn has disabled class')
- start()
+ done()
}, 0)
})
- test('should return reset state', function () {
+ test('should return reset state', function (assert) {
var $btn = $('')
equal($btn.html(), 'mdo', 'btn text equals mdo')
$btn.bootstrapButton('loading')
- stop()
+ var doneOne = assert.async()
setTimeout(function () {
equal($btn.html(), 'fat', 'btn text equals fat')
ok($btn[0].hasAttribute('disabled'), 'btn is disabled')
ok($btn.hasClass('disabled'), 'btn has disabled class')
- start()
- stop()
+ doneOne()
+ var doneTwo = assert.async()
$btn.bootstrapButton('reset')
setTimeout(function () {
equal($btn.html(), 'mdo', 'btn text equals mdo')
ok(!$btn[0].hasAttribute('disabled'), 'btn is not disabled')
ok(!$btn.hasClass('disabled'), 'btn does not have disabled class')
- start()
+ doneTwo()
}, 0)
}, 0)
})
- test('should work with an empty string as reset state', function () {
+ test('should work with an empty string as reset state', function (assert) {
var $btn = $('')
equal($btn.html(), '', 'btn text equals ""')
$btn.bootstrapButton('loading')
- stop()
+ var doneOne = assert.async()
setTimeout(function () {
equal($btn.html(), 'fat', 'btn text equals fat')
ok($btn[0].hasAttribute('disabled'), 'btn is disabled')
ok($btn.hasClass('disabled'), 'btn has disabled class')
- start()
- stop()
+ doneOne()
+ var doneTwo = assert.async()
$btn.bootstrapButton('reset')
setTimeout(function () {
equal($btn.html(), '', 'btn text equals ""')
ok(!$btn[0].hasAttribute('disabled'), 'btn is not disabled')
ok(!$btn.hasClass('disabled'), 'btn does not have disabled class')
- start()
+ doneTwo()
}, 0)
}, 0)
})
diff --git a/js/tests/unit/carousel.js b/js/tests/unit/carousel.js
index 1598787f1d..6da932d2c2 100644
--- a/js/tests/unit/carousel.js
+++ b/js/tests/unit/carousel.js
@@ -29,13 +29,13 @@ $(function () {
strictEqual($carousel[0], $el[0], 'collection contains element')
})
- test('should not fire slid when slide is prevented', function () {
- stop()
+ test('should not fire slid when slide is prevented', function (assert) {
+ var done = assert.async()
$('')
.on('slide.bs.carousel', function (e) {
e.preventDefault()
ok(true, 'slide event fired')
- start()
+ done()
})
.on('slid.bs.carousel', function () {
ok(false, 'slid event fired')
@@ -43,7 +43,7 @@ $(function () {
.bootstrapCarousel('next')
})
- test('should reset when slide is prevented', function () {
+ test('should reset when slide is prevented', function (assert) {
var carouselHTML = '
'
+ ''
+ ''
@@ -66,7 +66,7 @@ $(function () {
+ '
'
var $carousel = $(carouselHTML)
- stop()
+ var done = assert.async()
$carousel
.one('slide.bs.carousel', function (e) {
e.preventDefault()
@@ -82,13 +82,13 @@ $(function () {
ok(!$carousel.find('.carousel-indicators li:eq(0)').is('.active'), 'first indicator still active')
ok($carousel.find('.carousel-item:eq(1)').is('.active'), 'second item active')
ok($carousel.find('.carousel-indicators li:eq(1)').is('.active'), 'second indicator active')
- start()
+ done()
}, 0)
})
.bootstrapCarousel('next')
})
- test('should fire slide event with direction', function () {
+ test('should fire slide event with direction', function (assert) {
var carouselHTML = '
'
+ '
'
+ '
'
@@ -124,7 +124,7 @@ $(function () {
+ '
'
var $carousel = $(carouselHTML)
- stop()
+ var done = assert.async()
$carousel
.one('slide.bs.carousel', function (e) {
@@ -135,14 +135,14 @@ $(function () {
.one('slide.bs.carousel', function (e) {
ok(e.direction, 'direction present on prev')
strictEqual(e.direction, 'right', 'direction is right on prev')
- start()
+ done()
})
.bootstrapCarousel('prev')
})
.bootstrapCarousel('next')
})
- test('should fire slid event with direction', function () {
+ test('should fire slid event with direction', function (assert) {
var carouselHTML = '
'
+ '
'
+ '
'
@@ -178,7 +178,7 @@ $(function () {
+ '
'
var $carousel = $(carouselHTML)
- stop()
+ var done = assert.async()
$carousel
.one('slid.bs.carousel', function (e) {
@@ -189,14 +189,14 @@ $(function () {
.one('slid.bs.carousel', function (e) {
ok(e.direction, 'direction present on prev')
strictEqual(e.direction, 'right', 'direction is right on prev')
- start()
+ done()
})
.bootstrapCarousel('prev')
})
.bootstrapCarousel('next')
})
- test('should fire slide event with relatedTarget', function () {
+ test('should fire slide event with relatedTarget', function (assert) {
var template = '
'
- stop()
+ var done = assert.async()
$(template)
.on('slide.bs.carousel', function (e) {
ok(e.relatedTarget, 'relatedTarget present')
ok($(e.relatedTarget).hasClass('carousel-item'), 'relatedTarget has class "carousel-item"')
- start()
+ done()
})
.bootstrapCarousel('next')
})
- test('should fire slid event with relatedTarget', function () {
+ test('should fire slid event with relatedTarget', function (assert) {
var template = '
'
- stop()
+ var done = assert.async()
$(template)
.on('slid.bs.carousel', function (e) {
ok(e.relatedTarget, 'relatedTarget present')
ok($(e.relatedTarget).hasClass('carousel-item'), 'relatedTarget has class "carousel-item"')
- start()
+ done()
})
.bootstrapCarousel('next')
})
@@ -542,7 +542,7 @@ $(function () {
})
})
- test('should wrap around from end to start when wrap option is true', function () {
+ test('should wrap around from end to start when wrap option is true', function (assert) {
var carouselHTML = '
'
+ ''
+ ''
@@ -566,7 +566,7 @@ $(function () {
var $carousel = $(carouselHTML)
var getActiveId = function () { return $carousel.find('.carousel-item.active').attr('id') }
- stop()
+ var done = assert.async()
$carousel
.one('slid.bs.carousel', function () {
@@ -577,7 +577,7 @@ $(function () {
$carousel
.one('slid.bs.carousel', function () {
strictEqual(getActiveId(), 'one', 'carousel wrapped around and slid from 3rd to 1st slide')
- start()
+ done()
})
.bootstrapCarousel('next')
})
@@ -586,7 +586,7 @@ $(function () {
.bootstrapCarousel('next')
})
- test('should wrap around from start to end when wrap option is true', function () {
+ test('should wrap around from start to end when wrap option is true', function (assert) {
var carouselHTML = '
'
var $carousel = $(carouselHTML)
- stop()
+ var done = assert.async()
$carousel
.on('slid.bs.carousel', function () {
strictEqual($carousel.find('.carousel-item.active').attr('id'), 'three', 'carousel wrapped around and slid from 1st to 3rd slide')
- start()
+ done()
})
.bootstrapCarousel('prev')
})
- test('should stay at the end when the next method is called and wrap is false', function () {
+ test('should stay at the end when the next method is called and wrap is false', function (assert) {
var carouselHTML = '
'
+ ''
+ ''
@@ -643,7 +643,7 @@ $(function () {
var $carousel = $(carouselHTML)
var getActiveId = function () { return $carousel.find('.carousel-item.active').attr('id') }
- stop()
+ var done = assert.async()
$carousel
.one('slid.bs.carousel', function () {
@@ -657,7 +657,7 @@ $(function () {
})
.bootstrapCarousel('next')
strictEqual(getActiveId(), 'three', 'carousel did not wrap around and stayed on 3rd slide')
- start()
+ done()
})
.bootstrapCarousel('next')
})
diff --git a/js/tests/unit/collapse.js b/js/tests/unit/collapse.js
index 30d00e4135..ad59d87dbc 100644
--- a/js/tests/unit/collapse.js
+++ b/js/tests/unit/collapse.js
@@ -43,14 +43,14 @@ $(function () {
ok(/height/i.test($el.attr('style')), 'has height set')
})
- test('should not fire shown when show is prevented', function () {
- stop()
+ test('should not fire shown when show is prevented', function (assert) {
+ var done = assert.async()
$('')
.on('show.bs.collapse', function (e) {
e.preventDefault()
ok(true, 'show event fired')
- start()
+ done()
})
.on('shown.bs.collapse', function () {
ok(false, 'shown event fired')
@@ -58,8 +58,8 @@ $(function () {
.bootstrapCollapse('show')
})
- test('should reset style to auto after finishing opening collapse', function () {
- stop()
+ test('should reset style to auto after finishing opening collapse', function (assert) {
+ var done = assert.async()
$('')
.on('show.bs.collapse', function () {
@@ -67,13 +67,13 @@ $(function () {
})
.on('shown.bs.collapse', function () {
strictEqual(this.style.height, '', 'height is auto')
- start()
+ done()
})
.bootstrapCollapse('show')
})
- test('should remove "collapsed" class from target when collapse is shown', function () {
- stop()
+ test('should remove "collapsed" class from target when collapse is shown', function (assert) {
+ var done = assert.async()
var $target = $('').appendTo('#qunit-fixture')
@@ -81,14 +81,14 @@ $(function () {
.appendTo('#qunit-fixture')
.on('shown.bs.collapse', function () {
ok(!$target.hasClass('collapsed'))
- start()
+ done()
})
$target.click()
})
- test('should add "collapsed" class to target when collapse is hidden', function () {
- stop()
+ test('should add "collapsed" class to target when collapse is hidden', function (assert) {
+ var done = assert.async()
var $target = $('').appendTo('#qunit-fixture')
@@ -96,14 +96,14 @@ $(function () {
.appendTo('#qunit-fixture')
.on('hidden.bs.collapse', function () {
ok($target.hasClass('collapsed'))
- start()
+ done()
})
$target.click()
})
- test('should not close a collapse when initialized with "show" if already shown', function () {
- stop()
+ test('should not close a collapse when initialized with "show" if already shown', function (assert) {
+ var done = assert.async()
expect(0)
@@ -115,11 +115,11 @@ $(function () {
$test.bootstrapCollapse('show')
- setTimeout(start, 0)
+ setTimeout(done, 0)
})
- test('should open a collapse when initialized with "show" if not already shown', function () {
- stop()
+ test('should open a collapse when initialized with "show" if not already shown', function (assert) {
+ var done = assert.async()
expect(1)
@@ -131,11 +131,11 @@ $(function () {
$test.bootstrapCollapse('show')
- setTimeout(start, 0)
+ setTimeout(done, 0)
})
- test('should remove "collapsed" class from active accordion target', function () {
- stop()
+ test('should remove "collapsed" class from active accordion target', function (assert) {
+ var done = assert.async()
var accordionHTML = '
'
+ ''
@@ -161,14 +161,14 @@ $(function () {
ok($target2.hasClass('collapsed'), 'inactive target 2 does have class "collapsed"')
ok(!$target3.hasClass('collapsed'), 'active target 3 does not have class "collapsed"')
- start()
+ done()
})
$target3.click()
})
- test('should allow dots in data-parent', function () {
- stop()
+ test('should allow dots in data-parent', function (assert) {
+ var done = assert.async()
var accordionHTML = '
'
+ ''
@@ -194,14 +194,14 @@ $(function () {
ok($target2.hasClass('collapsed'), 'inactive target 2 does have class "collapsed"')
ok(!$target3.hasClass('collapsed'), 'active target 3 does not have class "collapsed"')
- start()
+ done()
})
$target3.click()
})
- test('should set aria-expanded="true" on target when collapse is shown', function () {
- stop()
+ test('should set aria-expanded="true" on target when collapse is shown', function (assert) {
+ var done = assert.async()
var $target = $('').appendTo('#qunit-fixture')
@@ -209,14 +209,14 @@ $(function () {
.appendTo('#qunit-fixture')
.on('shown.bs.collapse', function () {
equal($target.attr('aria-expanded'), 'true', 'aria-expanded on target is "true"')
- start()
+ done()
})
$target.click()
})
- test('should set aria-expanded="false" on target when collapse is hidden', function () {
- stop()
+ test('should set aria-expanded="false" on target when collapse is hidden', function (assert) {
+ var done = assert.async()
var $target = $('').appendTo('#qunit-fixture')
@@ -224,14 +224,14 @@ $(function () {
.appendTo('#qunit-fixture')
.on('hidden.bs.collapse', function () {
equal($target.attr('aria-expanded'), 'false', 'aria-expanded on target is "false"')
- start()
+ done()
})
$target.click()
})
- test('should change aria-expanded from active accordion target to "false" and set the newly active one to "true"', function () {
- stop()
+ test('should change aria-expanded from active accordion target to "false" and set the newly active one to "true"', function (assert) {
+ var done = assert.async()
var accordionHTML = '
'
+ ''
@@ -257,14 +257,14 @@ $(function () {
equal($target2.attr('aria-expanded'), 'false', 'inactive target 2 has aria-expanded="false"')
equal($target3.attr('aria-expanded'), 'true', 'active target 3 has aria-expanded="false"')
- start()
+ done()
})
$target3.click()
})
- test('should not fire show event if show is prevented because other element is still transitioning', function () {
- stop()
+ test('should not fire show event if show is prevented because other element is still transitioning', function (assert) {
+ var done = assert.async()
var accordionHTML = '
'
+ ''
@@ -294,12 +294,12 @@ $(function () {
setTimeout(function () {
ok(!showFired, 'show event didn\'t fire')
- start()
+ done()
}, 1)
})
- test('should add "collapsed" class to target when collapse is hidden via manual invocation', function () {
- stop()
+ test('should add "collapsed" class to target when collapse is hidden via manual invocation', function (assert) {
+ var done = assert.async()
var $target = $('').appendTo('#qunit-fixture')
@@ -307,13 +307,13 @@ $(function () {
.appendTo('#qunit-fixture')
.on('hidden.bs.collapse', function () {
ok($target.hasClass('collapsed'))
- start()
+ done()
})
.bootstrapCollapse('hide')
})
- test('should remove "collapsed" class from target when collapse is shown via manual invocation', function () {
- stop()
+ test('should remove "collapsed" class from target when collapse is shown via manual invocation', function (assert) {
+ var done = assert.async()
var $target = $('').appendTo('#qunit-fixture')
@@ -321,7 +321,7 @@ $(function () {
.appendTo('#qunit-fixture')
.on('shown.bs.collapse', function () {
ok(!$target.hasClass('collapsed'))
- start()
+ done()
})
.bootstrapCollapse('show')
})
diff --git a/js/tests/unit/dropdown.js b/js/tests/unit/dropdown.js
index 3cdf637ee0..9a7fbcff25 100644
--- a/js/tests/unit/dropdown.js
+++ b/js/tests/unit/dropdown.js
@@ -157,7 +157,7 @@ $(function () {
strictEqual($('#qunit-fixture .open').length, 0, '"open" class removed')
})
- test('should fire show and hide event', function () {
+ test('should fire show and hide event', function (assert) {
var dropdownHTML = '
'
+ '
'
+ 'Dropdown'
@@ -174,7 +174,7 @@ $(function () {
.find('[data-toggle="dropdown"]')
.bootstrapDropdown()
- stop()
+ var done = assert.async()
$dropdown
.parent('.dropdown')
@@ -183,7 +183,7 @@ $(function () {
})
.on('hide.bs.dropdown', function () {
ok(true, 'hide was fired')
- start()
+ done()
})
$dropdown.click()
@@ -191,7 +191,7 @@ $(function () {
})
- test('should fire shown and hidden event', function () {
+ test('should fire shown and hidden event', function (assert) {
var dropdownHTML = '
'
+ '
'
+ 'Dropdown'
@@ -208,7 +208,7 @@ $(function () {
.find('[data-toggle="dropdown"]')
.bootstrapDropdown()
- stop()
+ var done = assert.async()
$dropdown
.parent('.dropdown')
@@ -217,15 +217,15 @@ $(function () {
})
.on('hidden.bs.dropdown', function () {
ok(true, 'hidden was fired')
- start()
+ done()
})
$dropdown.click()
$(document.body).click()
})
- test('should ignore keyboard events within s and