]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
rewrite scrollspy unit tests
authorJohann-S <johann.servoire@gmail.com>
Sun, 30 Jun 2019 09:37:25 +0000 (11:37 +0200)
committerJohann-S <johann.servoire@gmail.com>
Tue, 23 Jul 2019 12:23:50 +0000 (14:23 +0200)
build/build-plugins.js
js/index.esm.js
js/index.umd.js
js/src/scrollspy/scrollspy.js [moved from js/src/scrollspy.js with 97% similarity]
js/src/scrollspy/scrollspy.spec.js [new file with mode: 0644]
js/tests/unit/scrollspy.js [deleted file]

index d0481572532e6712b8718372c1b8b26ecc20ec45..febe575c94d0013b67faf6638435f1a93d330b09 100644 (file)
@@ -39,7 +39,7 @@ const bsPlugins = {
   Dropdown: path.resolve(__dirname, '../js/src/dropdown/dropdown.js'),
   Modal: path.resolve(__dirname, '../js/src/modal/modal.js'),
   Popover: path.resolve(__dirname, '../js/src/popover/popover.js'),
-  ScrollSpy: path.resolve(__dirname, '../js/src/scrollspy.js'),
+  ScrollSpy: path.resolve(__dirname, '../js/src/scrollspy/scrollspy.js'),
   Tab: path.resolve(__dirname, '../js/src/tab.js'),
   Toast: path.resolve(__dirname, '../js/src/toast/toast.js'),
   Tooltip: path.resolve(__dirname, '../js/src/tooltip/tooltip.js')
index 82fd33fbbd6bf8fc850391fc2fa56e500017e082..e8c9ab6391e8eaa2ffe2fc4d0a51e15be48b0849 100644 (file)
@@ -12,7 +12,7 @@ import Collapse from './src/collapse/collapse'
 import Dropdown from './src/dropdown/dropdown'
 import Modal from './src/modal/modal'
 import Popover from './src/popover/popover'
-import ScrollSpy from './src/scrollspy'
+import ScrollSpy from './src/scrollspy/scrollspy'
 import Tab from './src/tab'
 import Toast from './src/toast/toast'
 import Tooltip from './src/tooltip/tooltip'
index 812a3004416d0950d5c9e55b7cb6b09764456d3f..207b952ca5dd5385df92473e4660b1a78a2305f6 100644 (file)
@@ -12,7 +12,7 @@ import Collapse from './src/collapse/collapse'
 import Dropdown from './src/dropdown/dropdown'
 import Modal from './src/modal/modal'
 import Popover from './src/popover/popover'
-import ScrollSpy from './src/scrollspy'
+import ScrollSpy from './src/scrollspy/scrollspy'
 import Tab from './src/tab'
 import Toast from './src/toast/toast'
 import Tooltip from './src/tooltip/tooltip'
similarity index 97%
rename from js/src/scrollspy.js
rename to js/src/scrollspy/scrollspy.js
index bcaab8b6756e6c5cbf6702fcbb068baae42d1737..68c51f7d7bf2686113a3f8b178bb4b13bc69877d 100644 (file)
@@ -11,11 +11,11 @@ import {
   getUID,
   makeArray,
   typeCheckConfig
-} from './util/index'
-import Data from './dom/data'
-import EventHandler from './dom/event-handler'
-import Manipulator from './dom/manipulator'
-import SelectorEngine from './dom/selector-engine'
+} from '../util/index'
+import Data from '../dom/data'
+import EventHandler from '../dom/event-handler'
+import Manipulator from '../dom/manipulator'
+import SelectorEngine from '../dom/selector-engine'
 
 /**
  * ------------------------------------------------------------------------
@@ -341,7 +341,7 @@ EventHandler.on(window, Event.LOAD_DATA_API, () => {
  * jQuery
  * ------------------------------------------------------------------------
  */
-
+/* istanbul ignore if */
 if (typeof $ !== 'undefined') {
   const JQUERY_NO_CONFLICT = $.fn[NAME]
   $.fn[NAME] = ScrollSpy._jQueryInterface
diff --git a/js/src/scrollspy/scrollspy.spec.js b/js/src/scrollspy/scrollspy.spec.js
new file mode 100644 (file)
index 0000000..52b20ba
--- /dev/null
@@ -0,0 +1,669 @@
+import ScrollSpy from './scrollspy'
+import Manipulator from '../dom/manipulator'
+import EventHandler from '../dom/event-handler'
+
+/** Test helpers */
+import { getFixture, clearFixture, createEvent, jQueryMock } from '../../tests/helpers/fixture'
+
+describe('ScrollSpy', () => {
+  let fixtureEl
+
+  const testElementIsActiveAfterScroll = ({ elementSelector, targetSelector, contentEl, scrollSpy, spy, cb }) => {
+    const element = fixtureEl.querySelector(elementSelector)
+    const target = fixtureEl.querySelector(targetSelector)
+
+    // add top padding to fix Chrome on Android failures
+    const paddingTop = 5
+    const scrollHeight = Math.ceil(contentEl.scrollTop + Manipulator.position(target).top) + paddingTop
+
+    function listener() {
+      expect(element.classList.contains('active')).toEqual(true)
+      contentEl.removeEventListener('scroll', listener)
+      expect(scrollSpy._process).toHaveBeenCalled()
+      spy.calls.reset()
+      cb()
+    }
+
+    contentEl.addEventListener('scroll', listener)
+    contentEl.scrollTop = scrollHeight
+  }
+
+  beforeAll(() => {
+    fixtureEl = getFixture()
+  })
+
+  afterEach(() => {
+    fixtureEl.style.display = 'none'
+    clearFixture()
+  })
+
+  describe('VERSION', () => {
+    it('should return plugin version', () => {
+      expect(ScrollSpy.VERSION).toEqual(jasmine.any(String))
+    })
+  })
+
+  describe('Default', () => {
+    it('should return plugin default config', () => {
+      expect(ScrollSpy.Default).toEqual(jasmine.any(Object))
+    })
+  })
+
+  describe('constructor', () => {
+    it('should generate an id when there is not one', () => {
+      fixtureEl.innerHTML = [
+        '<nav></nav>',
+        '<div class="content"></div>'
+      ].join('')
+
+      const navEl = fixtureEl.querySelector('nav')
+      const scrollSpy = new ScrollSpy(fixtureEl.querySelector('.content'), {
+        target: navEl
+      })
+
+      expect(scrollSpy).toBeDefined()
+      expect(navEl.getAttribute('id')).not.toEqual(null)
+    })
+
+    it('should not process element without target', () => {
+      fixtureEl.innerHTML = [
+        '<nav id="navigation" class="navbar">',
+        '  <ul class="navbar-nav">',
+        '    <li class="nav-item active"><a class="nav-link" id="one-link" href="#">One</a></li>',
+        '    <li class="nav-item"><a class="nav-link" id="two-link" href="#two">Two</a></li>',
+        '    <li class="nav-item"><a class="nav-link" id="three-link" href="#three">Three</a></li>',
+        '  </ul>',
+        '</nav>',
+        '<div id="content" style="height: 200px; overflow-y: auto;">',
+        ' <div id="two" style="height: 300px;"></div>',
+        ' <div id="three" style="height: 10px;"></div>',
+        '</div>'
+      ].join('')
+
+      fixtureEl.style.display = 'block'
+      const scrollSpy = new ScrollSpy(fixtureEl.querySelector('#content'), {
+        target: '#navigation'
+      })
+
+      expect(scrollSpy._targets.length).toEqual(2)
+    })
+
+    it('should only switch "active" class on current target', done => {
+      fixtureEl.innerHTML = [
+        '<div id="root" class="active" style="display: block">',
+        '  <div class="topbar">',
+        '    <div class="topbar-inner">',
+        '      <div class="container" id="ss-target">',
+        '        <ul class="nav">',
+        '          <li class="nav-item"><a href="#masthead">Overview</a></li>',
+        '          <li class="nav-item"><a href="#detail">Detail</a></li>',
+        '        </ul>',
+        '      </div>',
+        '    </div>',
+        '  </div>',
+        '  <div id="scrollspy-example" style="height: 100px; overflow: auto;">',
+        '    <div style="height: 200px;">',
+        '      <h4 id="masthead">Overview</h4>',
+        '      <p style="height: 200px;"></p>',
+        '    </div>',
+        '    <div style="height: 200px;">',
+        '      <h4 id="detail">Detail</h4>',
+        '      <p style="height: 200px;"></p>',
+        '    </div>',
+        '  </div>',
+        '</div>'
+      ].join('')
+
+      fixtureEl.style.display = 'block'
+      const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example')
+      const rootEl = fixtureEl.querySelector('#root')
+      const scrollSpy = new ScrollSpy(scrollSpyEl, {
+        target: 'ss-target'
+      })
+
+      spyOn(scrollSpy, '_process').and.callThrough()
+
+      scrollSpyEl.addEventListener('scroll', () => {
+        expect(rootEl.classList.contains('active')).toEqual(true)
+        expect(scrollSpy._process).toHaveBeenCalled()
+        done()
+      })
+
+      scrollSpyEl.scrollTop = 350
+    })
+
+    it('should only switch "active" class on current target specified w element', done => {
+      fixtureEl.innerHTML = [
+        '<div id="root" class="active" style="display: block">',
+        '  <div class="topbar">',
+        '    <div class="topbar-inner">',
+        '      <div class="container" id="ss-target">',
+        '        <ul class="nav">',
+        '          <li class="nav-item"><a href="#masthead">Overview</a></li>',
+        '          <li class="nav-item"><a href="#detail">Detail</a></li>',
+        '        </ul>',
+        '      </div>',
+        '    </div>',
+        '  </div>',
+        '  <div id="scrollspy-example" style="height: 100px; overflow: auto;">',
+        '    <div style="height: 200px;">',
+        '      <h4 id="masthead">Overview</h4>',
+        '      <p style="height: 200px;"></p>',
+        '    </div>',
+        '    <div style="height: 200px;">',
+        '      <h4 id="detail">Detail</h4>',
+        '      <p style="height: 200px;"></p>',
+        '    </div>',
+        '  </div>',
+        '</div>'
+      ].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.classList.contains('active')).toEqual(true)
+        expect(scrollSpy._process).toHaveBeenCalled()
+        done()
+      })
+
+      fixtureEl.style.display = 'block'
+      scrollSpyEl.scrollTop = 350
+    })
+
+    it('should correctly select middle navigation option when large offset is used', done => {
+      fixtureEl.innerHTML = [
+        '<div id="header" style="height: 500px;"></div>',
+        '<nav id="navigation" class="navbar">',
+        ' <ul class="navbar-nav">',
+        '   <li class="nav-item active"><a class="nav-link" id="one-link" href="#one">One</a></li>',
+        '   <li class="nav-item"><a class="nav-link" id="two-link" href="#two">Two</a></li>',
+        '   <li class="nav-item"><a class="nav-link" id="three-link" href="#three">Three</a></li>',
+        ' </ul>',
+        '</nav>',
+        '<div id="content" style="height: 200px; overflow-y: auto;">',
+        ' <div id="one" style="height: 500px;"></div>',
+        ' <div id="two" style="height: 300px;"></div>',
+        ' <div id="three" style="height: 10px;"></div>',
+        '</div>'
+      ].join('')
+
+      fixtureEl.style.display = 'block'
+      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').classList.contains('active')).toEqual(false)
+        expect(fixtureEl.querySelector('#two-link').classList.contains('active')).toEqual(true)
+        expect(fixtureEl.querySelector('#three-link').classList.contains('active')).toEqual(false)
+        expect(scrollSpy._process).toHaveBeenCalled()
+        done()
+      })
+
+      contentEl.scrollTop = 550
+    })
+
+    it('should add the active class to the correct element', done => {
+      fixtureEl.innerHTML = [
+        '<nav class="navbar">',
+        '  <ul class="nav">',
+        '    <li class="nav-item"><a class="nav-link" id="a-1" href="#div-1">div 1</a></li>',
+        '    <li class="nav-item"><a class="nav-link" id="a-2" href="#div-2">div 2</a></li>',
+        '  </ul>',
+        '</nav>',
+        '<div class="content" style="overflow: auto; height: 50px">',
+        '  <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>',
+        '  <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>',
+        '</div>'
+      ].join('')
+
+      fixtureEl.style.display = 'block'
+      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()
+          })
+        }
+      })
+    })
+
+    it('should add the active class to the correct element (nav markup)', done => {
+      fixtureEl.innerHTML = [
+        '<nav class="navbar">',
+        '  <nav class="nav">',
+        '    <a class="nav-link" id="a-1" href="#div-1">div 1</a>',
+        '    <a class="nav-link" id="a-2" href="#div-2">div 2</a>',
+        '  </nav>',
+        '</nav>',
+        '<div class="content" style="overflow: auto; height: 50px">',
+        '  <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>',
+        '  <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>',
+        '</div>'
+      ].join('')
+
+      fixtureEl.style.display = 'block'
+      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()
+          })
+        }
+      })
+    })
+
+    it('should add the active class to the correct element (list-group markup)', done => {
+      fixtureEl.innerHTML = [
+        '<nav class="navbar">',
+        '  <div class="list-group">',
+        '    <a class="list-group-item" id="a-1" href="#div-1">div 1</a>',
+        '    <a class="list-group-item" id="a-2" href="#div-2">div 2</a>',
+        '  </div>',
+        '</nav>',
+        '<div class="content" style="overflow: auto; height: 50px">',
+        '  <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>',
+        '  <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>',
+        '</div>'
+      ].join('')
+
+      fixtureEl.style.display = 'block'
+      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()
+          })
+        }
+      })
+    })
+
+    it('should clear selection if above the first section', done => {
+      fixtureEl.innerHTML = [
+        '<div id="header" style="height: 500px;"></div>',
+        '<nav id="navigation" class="navbar">',
+        '  <ul class="navbar-nav">',
+        '    <li class="nav-item"><a id="one-link"   class="nav-link active" href="#one">One</a></li>',
+        '    <li class="nav-item"><a id="two-link"   class="nav-link" href="#two">Two</a></li>',
+        '    <li class="nav-item"><a id="three-link" class="nav-link" href="#three">Three</a></li>',
+        '  </ul>',
+        '</nav>',
+        '<div id="content" style="height: 200px; overflow-y: auto;">',
+        '  <div id="spacer" style="height: 100px;"></div>',
+        '  <div id="one" style="height: 100px;"></div>',
+        '  <div id="two" style="height: 100px;"></div>',
+        '  <div id="three" style="height: 100px;"></div>',
+        '  <div id="spacer" style="height: 100px;"></div>',
+        '</div>'
+      ].join('')
+
+      fixtureEl.style.display = 'block'
+
+      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').length).toEqual(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 = [
+        '<div id="header" style="height: 500px;"></div>',
+        '<nav id="navigation" class="navbar">',
+        '  <ul class="navbar-nav">',
+        '    <li class="nav-item"><a id="one-link" class="nav-link active" href="#one">One</a></li>',
+        '    <li class="nav-item"><a id="two-link" class="nav-link" href="#two">Two</a></li>',
+        '    <li class="nav-item"><a id="three-link" class="nav-link" href="#three">Three</a></li>',
+        '  </ul>',
+        '</nav>',
+        '<div id="content" style="height: 200px; overflow-y: auto;">',
+        '  <div id="one" style="height: 100px;"></div>',
+        '  <div id="two" style="height: 100px;"></div>',
+        '  <div id="three" style="height: 100px;"></div>',
+        '  <div id="spacer" style="height: 100px;"></div>',
+        '</div>'
+      ].join('')
+
+      fixtureEl.style.display = 'block'
+
+      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').length).toEqual(1)
+          expect(active.getAttribute('id')).toEqual('two-link')
+          firstTime = false
+          contentEl.scrollTop = negativeHeight
+        } else {
+          expect(fixtureEl.querySelectorAll('.active').length).toEqual(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 = [
+        '<nav class="navbar">',
+        '  <ul class="nav">',
+        '    <li class="nav-item"><a id="li-100-1" class="nav-link" href="#div-100-1">div 1</a></li>',
+        '    <li class="nav-item"><a id="li-100-2" class="nav-link" href="#div-100-2">div 2</a></li>',
+        '    <li class="nav-item"><a id="li-100-3" class="nav-link" href="#div-100-3">div 3</a></li>',
+        '    <li class="nav-item"><a id="li-100-4" class="nav-link" href="#div-100-4">div 4</a></li>',
+        '    <li class="nav-item"><a id="li-100-5" class="nav-link" href="#div-100-5">div 5</a></li>',
+        '  </ul>',
+        '</nav>',
+        '<div class="content" style="position: relative; overflow: auto; height: 100px">',
+        '  <div id="div-100-1" style="position: relative; height: 100%; padding: 0; margin: 0">div 1</div>',
+        '  <div id="div-100-2" style="position: relative; height: 100%; padding: 0; margin: 0">div 2</div>',
+        '  <div id="div-100-3" style="position: relative; height: 100%; padding: 0; margin: 0">div 3</div>',
+        '  <div id="div-100-4" style="position: relative; height: 100%; padding: 0; margin: 0">div 4</div>',
+        '  <div id="div-100-5" style="position: relative; height: 100%; padding: 0; margin: 0">div 5</div>',
+        '</div>'
+      ].join('')
+
+      fixtureEl.style.display = 'block'
+
+      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: done
+                      })
+                    }
+                  })
+                }
+              })
+            }
+          })
+        }
+      })
+    })
+
+    it('should allow passed in option offset method: offset', () => {
+      fixtureEl.innerHTML = [
+        '<nav class="navbar">',
+        '  <ul class="nav">',
+        '    <li class="nav-item"><a id="li-jsm-1" class="nav-link" href="#div-jsm-1">div 1</a></li>',
+        '    <li class="nav-item"><a id="li-jsm-2" class="nav-link" href="#div-jsm-2">div 2</a></li>',
+        '    <li class="nav-item"><a id="li-jsm-3" class="nav-link" href="#div-jsm-3">div 3</a></li>',
+        '  </ul>',
+        '</nav>',
+        '<div class="content"  style="position: relative; overflow: auto; height: 100px">',
+        '  <div id="div-jsm-1" style="position: relative; height: 200px; padding: 0; margin: 0">div 1</div>',
+        '  <div id="div-jsm-2" style="position: relative; height: 150px; padding: 0; margin: 0">div 2</div>',
+        '  <div id="div-jsm-3" style="position: relative; height: 250px; padding: 0; margin: 0">div 3</div>',
+        '</div>'
+      ].join('')
+
+      fixtureEl.style.display = 'block'
+
+      const contentEl = fixtureEl.querySelector('.content')
+      const targetEl = fixtureEl.querySelector('#div-jsm-2')
+      const scrollSpy = new ScrollSpy(contentEl, {
+        target: '.navbar',
+        offset: 0,
+        method: 'offset'
+      })
+
+      expect(scrollSpy._offsets[1]).toEqual(Manipulator.offset(targetEl).top)
+      expect(scrollSpy._offsets[1]).not.toEqual(Manipulator.position(targetEl).top)
+    })
+
+    it('should allow passed in option offset method: position', () => {
+      fixtureEl.innerHTML = [
+        '<nav class="navbar">',
+        '  <ul class="nav">',
+        '    <li class="nav-item"><a id="li-jsm-1" class="nav-link" href="#div-jsm-1">div 1</a></li>',
+        '    <li class="nav-item"><a id="li-jsm-2" class="nav-link" href="#div-jsm-2">div 2</a></li>',
+        '    <li class="nav-item"><a id="li-jsm-3" class="nav-link" href="#div-jsm-3">div 3</a></li>',
+        '  </ul>',
+        '</nav>',
+        '<div class="content"  style="position: relative; overflow: auto; height: 100px">',
+        '  <div id="div-jsm-1" style="position: relative; height: 200px; padding: 0; margin: 0">div 1</div>',
+        '  <div id="div-jsm-2" style="position: relative; height: 150px; padding: 0; margin: 0">div 2</div>',
+        '  <div id="div-jsm-3" style="position: relative; height: 250px; padding: 0; margin: 0">div 3</div>',
+        '</div>'
+      ].join('')
+
+      fixtureEl.style.display = 'block'
+
+      const contentEl = fixtureEl.querySelector('.content')
+      const targetEl = fixtureEl.querySelector('#div-jsm-2')
+      const scrollSpy = new ScrollSpy(contentEl, {
+        target: '.navbar',
+        offset: 0,
+        method: 'position'
+      })
+
+      expect(scrollSpy._offsets[1]).not.toEqual(Manipulator.offset(targetEl).top)
+      expect(scrollSpy._offsets[1]).toEqual(Manipulator.position(targetEl).top)
+    })
+  })
+
+  describe('dispose', () => {
+    it('should dispose a scrollspy', () => {
+      spyOn(EventHandler, 'off')
+
+      const scrollSpy = new ScrollSpy(fixtureEl)
+
+      scrollSpy.dispose()
+      expect(EventHandler.off).toHaveBeenCalledWith(fixtureEl, '.bs.scrollspy')
+    })
+  })
+
+  describe('_jQueryInterface', () => {
+    it('should create a scrollspy', () => {
+      fixtureEl.innerHTML = '<div></div>'
+
+      const div = fixtureEl.querySelector('div')
+
+      jQueryMock.fn.scrollspy = ScrollSpy._jQueryInterface
+      jQueryMock.elements = [div]
+
+      jQueryMock.fn.scrollspy.call(jQueryMock)
+
+      expect(ScrollSpy._getInstance(div)).toBeDefined()
+    })
+
+    it('should not re create a scrollspy', () => {
+      fixtureEl.innerHTML = '<div></div>'
+
+      const div = fixtureEl.querySelector('div')
+      const scrollSpy = new ScrollSpy(div)
+
+      jQueryMock.fn.scrollspy = ScrollSpy._jQueryInterface
+      jQueryMock.elements = [div]
+
+      jQueryMock.fn.scrollspy.call(jQueryMock)
+
+      expect(ScrollSpy._getInstance(div)).toEqual(scrollSpy)
+    })
+
+    it('should call a scrollspy method', () => {
+      fixtureEl.innerHTML = '<div></div>'
+
+      const div = fixtureEl.querySelector('div')
+      const scrollSpy = new ScrollSpy(div)
+
+      spyOn(scrollSpy, 'refresh')
+
+      jQueryMock.fn.scrollspy = ScrollSpy._jQueryInterface
+      jQueryMock.elements = [div]
+
+      jQueryMock.fn.scrollspy.call(jQueryMock, 'refresh')
+
+      expect(ScrollSpy._getInstance(div)).toEqual(scrollSpy)
+      expect(scrollSpy.refresh).toHaveBeenCalled()
+    })
+
+    it('should throw error on undefined method', () => {
+      fixtureEl.innerHTML = '<div></div>'
+
+      const div = fixtureEl.querySelector('div')
+      const action = 'undefinedMethod'
+
+      jQueryMock.fn.scrollspy = ScrollSpy._jQueryInterface
+      jQueryMock.elements = [div]
+
+      try {
+        jQueryMock.fn.scrollspy.call(jQueryMock, action)
+      } catch (error) {
+        expect(error.message).toEqual(`No method named "${action}"`)
+      }
+    })
+  })
+
+  describe('_getInstance', () => {
+    it('should return null if there is no instance', () => {
+      expect(ScrollSpy._getInstance(fixtureEl)).toEqual(null)
+    })
+  })
+
+  describe('event handler', () => {
+    it('should create scrollspy on window load event', () => {
+      fixtureEl.innerHTML = '<div data-spy="scroll"></div>'
+
+      const scrollSpyEl = fixtureEl.querySelector('div')
+
+      window.dispatchEvent(createEvent('load'))
+
+      expect(ScrollSpy._getInstance(scrollSpyEl)).not.toBeNull()
+    })
+  })
+})
diff --git a/js/tests/unit/scrollspy.js b/js/tests/unit/scrollspy.js
deleted file mode 100644 (file)
index d191150..0000000
+++ /dev/null
@@ -1,743 +0,0 @@
-$(function () {
-  'use strict'
-
-  var ScrollSpy = typeof window.bootstrap === 'undefined' ? window.ScrollSpy : window.bootstrap.ScrollSpy
-
-  QUnit.module('scrollspy plugin')
-
-  QUnit.test('should be defined on jquery object', function (assert) {
-    assert.expect(1)
-    assert.ok($(document.body).scrollspy, 'scrollspy method is defined')
-  })
-
-  QUnit.module('scrollspy', {
-    beforeEach: function () {
-      // Run all tests in noConflict mode -- it's the only way to ensure that the plugin works in noConflict mode
-      $.fn.bootstrapScrollspy = $.fn.scrollspy.noConflict()
-    },
-    afterEach: function () {
-      $.fn.scrollspy = $.fn.bootstrapScrollspy
-      delete $.fn.bootstrapScrollspy
-      $('#qunit-fixture').html('')
-    }
-  })
-
-  QUnit.test('should provide no conflict', function (assert) {
-    assert.expect(1)
-    assert.strictEqual(typeof $.fn.scrollspy, 'undefined', 'scrollspy was set back to undefined (org value)')
-  })
-
-  QUnit.test('should throw explicit error on undefined method', function (assert) {
-    assert.expect(1)
-    var $el = $('<div/>').appendTo('#qunit-fixture')
-    $el.bootstrapScrollspy()
-    try {
-      $el.bootstrapScrollspy('noMethod')
-    } catch (error) {
-      assert.strictEqual(error.message, 'No method named "noMethod"')
-    }
-  })
-
-  QUnit.test('should return jquery collection containing the element', function (assert) {
-    assert.expect(2)
-    var $el = $('<div/>').appendTo('#qunit-fixture')
-    var $scrollspy = $el.bootstrapScrollspy()
-    assert.ok($scrollspy instanceof $, 'returns jquery collection')
-    assert.strictEqual($scrollspy[0], $el[0], 'collection contains element')
-  })
-
-  QUnit.test('should only switch "active" class on current target', function (assert) {
-    assert.expect(1)
-    var done = assert.async()
-
-    var sectionHTML = '<div id="root" class="active">' +
-        '<div class="topbar">' +
-        '<div class="topbar-inner">' +
-        '<div class="container" id="ss-target">' +
-        '<ul class="nav">' +
-        '<li class="nav-item"><a href="#masthead">Overview</a></li>' +
-        '<li class="nav-item"><a href="#detail">Detail</a></li>' +
-        '</ul>' +
-        '</div>' +
-        '</div>' +
-        '</div>' +
-        '<div id="scrollspy-example" style="height: 100px; overflow: auto;">' +
-        '<div style="height: 200px;">' +
-        '<h4 id="masthead">Overview</h4>' +
-        '<p style="height: 200px">' +
-        'Ad leggings keytar, brunch id art party dolor labore.' +
-        '</p>' +
-        '</div>' +
-        '<div style="height: 200px;">' +
-        '<h4 id="detail">Detail</h4>' +
-        '<p style="height: 200px">' +
-        'Veniam marfa mustache skateboard, adipisicing fugiat velit pitchfork beard.' +
-        '</p>' +
-        '</div>' +
-        '</div>' +
-        '</div>'
-    var $section = $(sectionHTML).appendTo('#qunit-fixture')
-
-    var $scrollspy = $section
-      .show()
-      .find('#scrollspy-example')
-      .bootstrapScrollspy({
-        target: 'ss-target'
-      })
-
-    $scrollspy.one('scroll', function () {
-      assert.ok($section.hasClass('active'), '"active" class still on root node')
-      done()
-    })
-
-    $scrollspy.scrollTop(350)
-  })
-
-  QUnit.test('should only switch "active" class on current target specified w element', function (assert) {
-    assert.expect(1)
-    var done = assert.async()
-
-    var sectionHTML = '<div id="root" class="active">' +
-        '<div class="topbar">' +
-        '<div class="topbar-inner">' +
-        '<div class="container" id="ss-target">' +
-        '<ul class="nav">' +
-        '<li class="nav-item"><a href="#masthead">Overview</a></li>' +
-        '<li class="nav-item"><a href="#detail">Detail</a></li>' +
-        '</ul>' +
-        '</div>' +
-        '</div>' +
-        '</div>' +
-        '<div id="scrollspy-example" style="height: 100px; overflow: auto;">' +
-        '<div style="height: 200px;">' +
-        '<h4 id="masthead">Overview</h4>' +
-        '<p style="height: 200px">' +
-        'Ad leggings keytar, brunch id art party dolor labore.' +
-        '</p>' +
-        '</div>' +
-        '<div style="height: 200px;">' +
-        '<h4 id="detail">Detail</h4>' +
-        '<p style="height: 200px">' +
-        'Veniam marfa mustache skateboard, adipisicing fugiat velit pitchfork beard.' +
-        '</p>' +
-        '</div>' +
-        '</div>' +
-        '</div>'
-    var $section = $(sectionHTML).appendTo('#qunit-fixture')
-
-    var $scrollspy = $section
-      .show()
-      .find('#scrollspy-example')
-      .bootstrapScrollspy({
-        target: document.getElementById('ss-target')
-      })
-
-    $scrollspy.one('scroll', function () {
-      assert.ok($section.hasClass('active'), '"active" class still on root node')
-      done()
-    })
-
-    $scrollspy.scrollTop(350)
-  })
-
-  QUnit.test('should correctly select middle navigation option when large offset is used', function (assert) {
-    assert.expect(3)
-    var done = assert.async()
-
-    var sectionHTML = '<div id="header" style="height: 500px;"></div>' +
-        '<nav id="navigation" class="navbar">' +
-        '<ul class="navbar-nav">' +
-        '<li class="nav-item active"><a class="nav-link" id="one-link" href="#one">One</a></li>' +
-        '<li class="nav-item"><a class="nav-link" id="two-link" href="#two">Two</a></li>' +
-        '<li class="nav-item"><a class="nav-link" id="three-link" href="#three">Three</a></li>' +
-        '</ul>' +
-        '</nav>' +
-        '<div id="content" style="height: 200px; overflow-y: auto;">' +
-        '<div id="one" style="height: 500px;"></div>' +
-        '<div id="two" style="height: 300px;"></div>' +
-        '<div id="three" style="height: 10px;"></div>' +
-        '</div>'
-    var $section = $(sectionHTML).appendTo('#qunit-fixture')
-    var $scrollspy = $section
-      .show()
-      .filter('#content')
-
-    $scrollspy.bootstrapScrollspy({
-      target: '#navigation',
-      offset: $scrollspy.position().top
-    })
-
-    $scrollspy.one('scroll', function () {
-      assert.ok(!$section.find('#one-link').hasClass('active'), '"active" class removed from first section')
-      assert.ok($section.find('#two-link').hasClass('active'), '"active" class on middle section')
-      assert.ok(!$section.find('#three-link').hasClass('active'), '"active" class not on last section')
-      done()
-    })
-
-    $scrollspy.scrollTop(550)
-  })
-
-  QUnit.test('should add the active class to the correct element', function (assert) {
-    assert.expect(2)
-    var navbarHtml =
-      '<nav class="navbar">' +
-      '<ul class="nav">' +
-      '<li class="nav-item"><a class="nav-link" id="a-1" href="#div-1">div 1</a></li>' +
-      '<li class="nav-item"><a class="nav-link" id="a-2" href="#div-2">div 2</a></li>' +
-      '</ul>' +
-      '</nav>'
-    var contentHtml =
-        '<div class="content" style="overflow: auto; height: 50px">' +
-      '<div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>' +
-      '<div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>' +
-      '</div>'
-
-    $(navbarHtml).appendTo('#qunit-fixture')
-    var $content = $(contentHtml)
-      .appendTo('#qunit-fixture')
-      .bootstrapScrollspy({
-        offset: 0,
-        target: '.navbar'
-      })
-
-    var done = assert.async()
-    var testElementIsActiveAfterScroll = function (element, target) {
-      var deferred = $.Deferred()
-      // add top padding to fix Chrome on Android failures
-      var paddingTop = 5
-      var scrollHeight = Math.ceil($content.scrollTop() + $(target).position().top) + paddingTop
-      $content.one('scroll', function () {
-        assert.ok($(element).hasClass('active'), 'target:' + target + ', element' + element)
-        deferred.resolve()
-      })
-      $content.scrollTop(scrollHeight)
-      return deferred.promise()
-    }
-
-    $.when(testElementIsActiveAfterScroll('#a-1', '#div-1'))
-      .then(function () {
-        return testElementIsActiveAfterScroll('#a-2', '#div-2')
-      })
-      .then(function () {
-        done()
-      })
-  })
-
-  QUnit.test('should add the active class to the correct element (nav markup)', function (assert) {
-    assert.expect(2)
-    var navbarHtml =
-      '<nav class="navbar">' +
-      '<nav class="nav">' +
-      '<a class="nav-link" id="a-1" href="#div-1">div 1</a>' +
-      '<a class="nav-link" id="a-2" href="#div-2">div 2</a>' +
-      '</nav>' +
-      '</nav>'
-    var contentHtml =
-        '<div class="content" style="overflow: auto; height: 50px">' +
-      '<div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>' +
-      '<div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>' +
-      '</div>'
-
-    $(navbarHtml).appendTo('#qunit-fixture')
-    var $content = $(contentHtml)
-      .appendTo('#qunit-fixture')
-      .bootstrapScrollspy({
-        offset: 0,
-        target: '.navbar'
-      })
-
-    var done = assert.async()
-    var testElementIsActiveAfterScroll = function (element, target) {
-      var deferred = $.Deferred()
-      // add top padding to fix Chrome on Android failures
-      var paddingTop = 5
-      var scrollHeight = Math.ceil($content.scrollTop() + $(target).position().top) + paddingTop
-      $content.one('scroll', function () {
-        assert.ok($(element).hasClass('active'), 'target:' + target + ', element' + element)
-        deferred.resolve()
-      })
-      $content.scrollTop(scrollHeight)
-      return deferred.promise()
-    }
-
-    $.when(testElementIsActiveAfterScroll('#a-1', '#div-1'))
-      .then(function () {
-        return testElementIsActiveAfterScroll('#a-2', '#div-2')
-      })
-      .then(function () {
-        done()
-      })
-  })
-
-  QUnit.test('should add the active class to the correct element (list-group markup)', function (assert) {
-    assert.expect(2)
-    var navbarHtml =
-      '<nav class="navbar">' +
-      '<div class="list-group">' +
-      '<a class="list-group-item" id="a-1" href="#div-1">div 1</a>' +
-      '<a class="list-group-item" id="a-2" href="#div-2">div 2</a>' +
-      '</div>' +
-      '</nav>'
-    var contentHtml =
-        '<div class="content" style="overflow: auto; height: 50px">' +
-      '<div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>' +
-      '<div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>' +
-      '</div>'
-
-    $(navbarHtml).appendTo('#qunit-fixture')
-    var $content = $(contentHtml)
-      .appendTo('#qunit-fixture')
-      .bootstrapScrollspy({
-        offset: 0,
-        target: '.navbar'
-      })
-
-    var done = assert.async()
-    var testElementIsActiveAfterScroll = function (element, target) {
-      var deferred = $.Deferred()
-      // add top padding to fix Chrome on Android failures
-      var paddingTop = 5
-      var scrollHeight = Math.ceil($content.scrollTop() + $(target).position().top) + paddingTop
-      $content.one('scroll', function () {
-        assert.ok($(element).hasClass('active'), 'target:' + target + ', element' + element)
-        deferred.resolve()
-      })
-      $content.scrollTop(scrollHeight)
-      return deferred.promise()
-    }
-
-    $.when(testElementIsActiveAfterScroll('#a-1', '#div-1'))
-      .then(function () {
-        return testElementIsActiveAfterScroll('#a-2', '#div-2')
-      })
-      .then(function () {
-        done()
-      })
-  })
-
-  QUnit.test('should add the active class correctly when there are nested elements at 0 scroll offset', function (assert) {
-    assert.expect(6)
-    var times = 0
-    var done = assert.async()
-    var navbarHtml = '<nav id="navigation" class="navbar">' +
-      '<ul class="nav">' +
-      '<li class="nav-item"><a id="a-1" class="nav-link" href="#div-1">div 1</a>' +
-      '<ul class="nav">' +
-      '<li class="nav-item"><a id="a-2" class="nav-link" href="#div-2">div 2</a></li>' +
-      '</ul>' +
-      '</li>' +
-      '</ul>' +
-      '</nav>'
-
-    var contentHtml = '<div class="content" style="position: absolute; top: 0px; overflow: auto; height: 50px">' +
-      '<div id="div-1" style="padding: 0; margin: 0">' +
-      '<div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>' +
-      '</div>' +
-      '</div>'
-
-    $(navbarHtml).appendTo('#qunit-fixture')
-
-    var $content = $(contentHtml)
-      .appendTo('#qunit-fixture')
-      .bootstrapScrollspy({
-        offset: 0,
-        target: '#navigation'
-      })
-
-    function testActiveElements() {
-      if (++times > 3) {
-        return done()
-      }
-
-      $content.one('scroll', function () {
-        assert.ok($('#a-1').hasClass('active'), 'nav item for outer element has "active" class')
-        assert.ok($('#a-2').hasClass('active'), 'nav item for inner element has "active" class')
-        testActiveElements()
-      })
-
-      $content.scrollTop($content.scrollTop() + 10)
-    }
-
-    testActiveElements()
-  })
-
-  QUnit.test('should add the active class correctly when there are nested elements (nav markup)', function (assert) {
-    assert.expect(6)
-    var times = 0
-    var done = assert.async()
-    var navbarHtml = '<nav id="navigation" class="navbar">' +
-      '<nav class="nav">' +
-      '<a id="a-1" class="nav-link" href="#div-1">div 1</a>' +
-      '<nav class="nav">' +
-      '<a id="a-2" class="nav-link" href="#div-2">div 2</a>' +
-      '</nav>' +
-      '</nav>' +
-      '</nav>'
-
-    var contentHtml = '<div class="content" style="position: absolute; top: 0px; overflow: auto; height: 50px">' +
-      '<div id="div-1" style="padding: 0; margin: 0">' +
-      '<div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>' +
-      '</div>' +
-      '</div>'
-
-    $(navbarHtml).appendTo('#qunit-fixture')
-
-    var $content = $(contentHtml)
-      .appendTo('#qunit-fixture')
-      .bootstrapScrollspy({
-        offset: 0,
-        target: '#navigation'
-      })
-
-    function testActiveElements() {
-      if (++times > 3) {
-        return done()
-      }
-
-      $content.one('scroll', function () {
-        assert.ok($('#a-1').hasClass('active'), 'nav item for outer element has "active" class')
-        assert.ok($('#a-2').hasClass('active'), 'nav item for inner element has "active" class')
-        testActiveElements()
-      })
-
-      $content.scrollTop($content.scrollTop() + 10)
-    }
-
-    testActiveElements()
-  })
-
-  QUnit.test('should add the active class correctly when there are nested elements (nav nav-item markup)', function (assert) {
-    assert.expect(6)
-    var times = 0
-    var done = assert.async()
-    var navbarHtml = '<nav id="navigation" class="navbar">' +
-      '<ul class="nav">' +
-      '<li class="nav-item"><a id="a-1" class="nav-link" href="#div-1">div 1</a></li>' +
-      '<ul class="nav">' +
-      '<li class="nav-item"><a id="a-2" class="nav-link" href="#div-2">div 2</a></li>' +
-      '</ul>' +
-      '</ul>' +
-      '</nav>'
-
-    var contentHtml = '<div class="content" style="position: absolute; top: 0px; overflow: auto; height: 50px">' +
-      '<div id="div-1" style="padding: 0; margin: 0">' +
-      '<div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>' +
-      '</div>' +
-      '</div>'
-
-    $(navbarHtml).appendTo('#qunit-fixture')
-
-    var $content = $(contentHtml)
-      .appendTo('#qunit-fixture')
-      .bootstrapScrollspy({
-        offset: 0,
-        target: '#navigation'
-      })
-
-    function testActiveElements() {
-      if (++times > 3) {
-        return done()
-      }
-
-      $content.one('scroll', function () {
-        assert.ok($('#a-1').hasClass('active'), 'nav item for outer element has "active" class')
-        assert.ok($('#a-2').hasClass('active'), 'nav item for inner element has "active" class')
-        testActiveElements()
-      })
-
-      $content.scrollTop($content.scrollTop() + 10)
-    }
-
-    testActiveElements()
-  })
-
-  QUnit.test('should add the active class correctly when there are nested elements (list-group markup)', function (assert) {
-    assert.expect(6)
-    var times = 0
-    var done = assert.async()
-    var navbarHtml = '<nav id="navigation" class="navbar">' +
-      '<div class="list-group">' +
-      '<a id="a-1" class="list-group-item" href="#div-1">div 1</a>' +
-      '<div class="list-group">' +
-      '<a id="a-2" class="list-group-item" href="#div-2">div 2</a>' +
-      '</div>' +
-      '</div>' +
-      '</nav>'
-
-    var contentHtml = '<div class="content" style="position: absolute; top: 0px; overflow: auto; height: 50px">' +
-      '<div id="div-1" style="padding: 0; margin: 0">' +
-      '<div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>' +
-      '</div>' +
-      '</div>'
-
-    $(navbarHtml).appendTo('#qunit-fixture')
-
-    var $content = $(contentHtml)
-      .appendTo('#qunit-fixture')
-      .bootstrapScrollspy({
-        offset: 0,
-        target: '#navigation'
-      })
-
-    function testActiveElements() {
-      if (++times > 3) {
-        return done()
-      }
-
-      $content.one('scroll', function () {
-        assert.ok($('#a-1').hasClass('active'), 'nav item for outer element has "active" class')
-        assert.ok($('#a-2').hasClass('active'), 'nav item for inner element has "active" class')
-        testActiveElements()
-      })
-
-      $content.scrollTop($content.scrollTop() + 10)
-    }
-
-    testActiveElements()
-  })
-
-  QUnit.test('should clear selection if above the first section', function (assert) {
-    assert.expect(3)
-    var done = assert.async()
-
-    var sectionHTML = '<div id="header" style="height: 500px;"></div>' +
-        '<nav id="navigation" class="navbar">' +
-        '<ul class="navbar-nav">' +
-        '<li class="nav-item"><a id="one-link"   class="nav-link active" href="#one">One</a></li>' +
-        '<li class="nav-item"><a id="two-link"   class="nav-link" href="#two">Two</a></li>' +
-        '<li class="nav-item"><a id="three-link" class="nav-link" href="#three">Three</a></li>' +
-        '</ul>' +
-        '</nav>'
-    $(sectionHTML).appendTo('#qunit-fixture')
-
-    var scrollspyHTML = '<div id="content" style="height: 200px; overflow-y: auto;">' +
-        '<div id="spacer" style="height: 100px;"/>' +
-        '<div id="one" style="height: 100px;"/>' +
-        '<div id="two" style="height: 100px;"/>' +
-        '<div id="three" style="height: 100px;"/>' +
-        '<div id="spacer" style="height: 100px;"/>' +
-        '</div>'
-    var $scrollspy = $(scrollspyHTML).appendTo('#qunit-fixture')
-
-    $scrollspy
-      .bootstrapScrollspy({
-        target: '#navigation',
-        offset: $scrollspy.position().top
-      })
-      .one('scroll', function () {
-        assert.strictEqual($('.active').length, 1, '"active" class on only one element present')
-        assert.strictEqual($('.active').is('#two-link'), true, '"active" class on second section')
-        $scrollspy
-          .one('scroll', function () {
-            assert.strictEqual($('.active').length, 0, 'selection cleared')
-            done()
-          })
-          .scrollTop(0)
-      })
-      .scrollTop(201)
-  })
-
-  QUnit.test('should NOT clear selection if above the first section and first section is at the top', function (assert) {
-    assert.expect(4)
-    var done = assert.async()
-
-    var sectionHTML = '<div id="header" style="height: 500px;"></div>' +
-        '<nav id="navigation" class="navbar">' +
-        '<ul class="navbar-nav">' +
-        '<li class="nav-item"><a id="one-link"   class="nav-link active" href="#one">One</a></li>' +
-        '<li class="nav-item"><a id="two-link"   class="nav-link" href="#two">Two</a></li>' +
-        '<li class="nav-item"><a id="three-link" class="nav-link" href="#three">Three</a></li>' +
-        '</ul>' +
-        '</nav>'
-    $(sectionHTML).appendTo('#qunit-fixture')
-
-    var negativeHeight = -10
-    var startOfSectionTwo = 101
-
-    var scrollspyHTML = '<div id="content" style="height: 200px; overflow-y: auto;">' +
-        '<div id="one" style="height: 100px;"/>' +
-        '<div id="two" style="height: 100px;"/>' +
-        '<div id="three" style="height: 100px;"/>' +
-        '<div id="spacer" style="height: 100px;"/>' +
-        '</div>'
-    var $scrollspy = $(scrollspyHTML).appendTo('#qunit-fixture')
-
-    $scrollspy
-      .bootstrapScrollspy({
-        target: '#navigation',
-        offset: $scrollspy[0].offsetTop
-      })
-      .one('scroll', function () {
-        assert.strictEqual($('.active').length, 1, '"active" class on only one element present')
-        assert.strictEqual($('.active').is('#two-link'), true, '"active" class on second section')
-        $scrollspy
-          .one('scroll', function () {
-            assert.strictEqual($('.active').length, 1, '"active" class on only one element present')
-            assert.strictEqual($('.active').is('#one-link'), true, '"active" class on first section')
-            done()
-          })
-          .scrollTop(negativeHeight)
-      })
-      .scrollTop(startOfSectionTwo)
-  })
-
-  QUnit.test('should correctly select navigation element on backward scrolling when each target section height is 100%', function (assert) {
-    assert.expect(5)
-    var navbarHtml =
-      '<nav class="navbar">' +
-      '<ul class="nav">' +
-      '<li class="nav-item"><a id="li-100-1" class="nav-link" href="#div-100-1">div 1</a></li>' +
-      '<li class="nav-item"><a id="li-100-2" class="nav-link" href="#div-100-2">div 2</a></li>' +
-      '<li class="nav-item"><a id="li-100-3" class="nav-link" href="#div-100-3">div 3</a></li>' +
-      '<li class="nav-item"><a id="li-100-4" class="nav-link" href="#div-100-4">div 4</a></li>' +
-      '<li class="nav-item"><a id="li-100-5" class="nav-link" href="#div-100-5">div 5</a></li>' +
-      '</ul>' +
-      '</nav>'
-    var contentHtml =
-        '<div class="content" style="position: relative; overflow: auto; height: 100px">' +
-      '<div id="div-100-1" style="position: relative; height: 100%; padding: 0; margin: 0">div 1</div>' +
-      '<div id="div-100-2" style="position: relative; height: 100%; padding: 0; margin: 0">div 2</div>' +
-      '<div id="div-100-3" style="position: relative; height: 100%; padding: 0; margin: 0">div 3</div>' +
-      '<div id="div-100-4" style="position: relative; height: 100%; padding: 0; margin: 0">div 4</div>' +
-      '<div id="div-100-5" style="position: relative; height: 100%; padding: 0; margin: 0">div 5</div>' +
-      '</div>'
-
-    $(navbarHtml).appendTo('#qunit-fixture')
-    var $content = $(contentHtml)
-      .appendTo('#qunit-fixture')
-      .bootstrapScrollspy({
-        offset: 0,
-        target: '.navbar'
-      })
-
-    var testElementIsActiveAfterScroll = function (element, target) {
-      var deferred = $.Deferred()
-      // add top padding to fix Chrome on Android failures
-      var paddingTop = 5
-      var scrollHeight = Math.ceil($content.scrollTop() + $(target).position().top) + paddingTop
-      $content.one('scroll', function () {
-        assert.ok($(element).hasClass('active'), 'target:' + target + ', element: ' + element)
-        deferred.resolve()
-      })
-      $content.scrollTop(scrollHeight)
-      return deferred.promise()
-    }
-
-    var done = assert.async()
-    $.when(testElementIsActiveAfterScroll('#li-100-5', '#div-100-5'))
-      .then(function () {
-        return testElementIsActiveAfterScroll('#li-100-4', '#div-100-4')
-      })
-      .then(function () {
-        return testElementIsActiveAfterScroll('#li-100-3', '#div-100-3')
-      })
-      .then(function () {
-        return testElementIsActiveAfterScroll('#li-100-2', '#div-100-2')
-      })
-      .then(function () {
-        return testElementIsActiveAfterScroll('#li-100-1', '#div-100-1')
-      })
-      .then(function () {
-        done()
-      })
-  })
-
-  QUnit.test('should allow passed in option offset method: offset', function (assert) {
-    assert.expect(4)
-
-    var testOffsetMethod = function (type) {
-      var $navbar = $(
-        '<nav class="navbar"' + (type === 'data' ? ' id="navbar-offset-method-menu"' : '') + '>' +
-        '<ul class="nav">' +
-        '<li class="nav-item"><a id="li-' + type + 'm-1" class="nav-link" href="#div-' + type + 'm-1">div 1</a></li>' +
-        '<li class="nav-item"><a id="li-' + type + 'm-2" class="nav-link" href="#div-' + type + 'm-2">div 2</a></li>' +
-        '<li class="nav-item"><a id="li-' + type + 'm-3" class="nav-link" href="#div-' + type + 'm-3">div 3</a></li>' +
-        '</ul>' +
-        '</nav>'
-      )
-      var $content = $(
-        '<div class="content"' + (type === 'data' ? ' data-spy="scroll" data-target="#navbar-offset-method-menu" data-offset="0" data-method="offset"' : '') + ' style="position: relative; overflow: auto; height: 100px">' +
-        '<div id="div-' + type + 'm-1" style="position: relative; height: 200px; padding: 0; margin: 0">div 1</div>' +
-        '<div id="div-' + type + 'm-2" style="position: relative; height: 150px; padding: 0; margin: 0">div 2</div>' +
-        '<div id="div-' + type + 'm-3" style="position: relative; height: 250px; padding: 0; margin: 0">div 3</div>' +
-        '</div>'
-      )
-
-      $navbar.appendTo('#qunit-fixture')
-      $content.appendTo('#qunit-fixture')
-
-      if (type === 'js') {
-        $content.bootstrapScrollspy({
-          target: '.navbar',
-          offset: 0,
-          method: 'offset'
-        })
-      } else if (type === 'data') {
-        window.dispatchEvent(new Event('load'))
-      }
-
-      var $target = $('#div-' + type + 'm-2')
-      var scrollspy = ScrollSpy._getInstance($content[0])
-
-      assert.ok(scrollspy._offsets[1] === $target.offset().top, 'offset method with ' + type + ' option')
-      assert.ok(scrollspy._offsets[1] !== $target.position().top, 'position method with ' + type + ' option')
-      $navbar.remove()
-      $content.remove()
-    }
-
-    testOffsetMethod('js')
-    testOffsetMethod('data')
-  })
-
-  QUnit.test('should allow passed in option offset method: position', function (assert) {
-    assert.expect(4)
-
-    var testOffsetMethod = function (type) {
-      var $navbar = $(
-        '<nav class="navbar"' + (type === 'data' ? ' id="navbar-offset-method-menu"' : '') + '>' +
-        '<ul class="nav">' +
-        '<li class="nav-item"><a class="nav-link" id="li-' + type + 'm-1" href="#div-' + type + 'm-1">div 1</a></li>' +
-        '<li class="nav-item"><a class="nav-link" id="li-' + type + 'm-2" href="#div-' + type + 'm-2">div 2</a></li>' +
-        '<li class="nav-item"><a class="nav-link" id="li-' + type + 'm-3" href="#div-' + type + 'm-3">div 3</a></li>' +
-        '</ul>' +
-        '</nav>'
-      )
-      var $content = $(
-        '<div class="content"' + (type === 'data' ? ' data-spy="scroll" data-target="#navbar-offset-method-menu" data-offset="0" data-method="position"' : '') + ' style="position: relative; overflow: auto; height: 100px">' +
-        '<div id="div-' + type + 'm-1" style="position: relative; height: 200px; padding: 0; margin: 0">div 1</div>' +
-        '<div id="div-' + type + 'm-2" style="position: relative; height: 150px; padding: 0; margin: 0">div 2</div>' +
-        '<div id="div-' + type + 'm-3" style="position: relative; height: 250px; padding: 0; margin: 0">div 3</div>' +
-        '</div>'
-      )
-
-      $navbar.appendTo('#qunit-fixture')
-      $content.appendTo('#qunit-fixture')
-
-      if (type === 'js') {
-        $content.bootstrapScrollspy({
-          target: '.navbar',
-          offset: 0,
-          method: 'position'
-        })
-      } else if (type === 'data') {
-        window.dispatchEvent(new Event('load'))
-      }
-
-      var $target = $('#div-' + type + 'm-2')
-      var scrollspy = ScrollSpy._getInstance($content[0])
-
-      assert.ok(scrollspy._offsets[1] !== $target.offset().top, 'offset method with ' + type + ' option')
-      assert.ok(scrollspy._offsets[1] === $target.position().top, 'position method with ' + type + ' option')
-      $navbar.remove()
-      $content.remove()
-    }
-
-    testOffsetMethod('js')
-    testOffsetMethod('data')
-  })
-
-  QUnit.test('should return the version', function (assert) {
-    assert.expect(1)
-    assert.strictEqual(typeof ScrollSpy.VERSION, 'string')
-  })
-})