diff options
| author | GeoSot <[email protected]> | 2022-04-13 20:29:13 +0300 |
|---|---|---|
| committer | GitHub <[email protected]> | 2022-04-13 10:29:13 -0700 |
| commit | ece16012270a9ef7781ce9269cb151c5e5961734 (patch) | |
| tree | 2c0a639899c08835b58ab805d21149ccb45c1ca8 /js/tests/unit/scrollspy.spec.js | |
| parent | cfd2f3f7787ba22feb78d916956f6f73746f3ee3 (diff) | |
| download | bootstrap-ece16012270a9ef7781ce9269cb151c5e5961734.tar.xz bootstrap-ece16012270a9ef7781ce9269cb151c5e5961734.zip | |
Revamp Scrollspy using Intersection observer (#33421)
* Revamp scrollspy to use IntersectionObserver
* Add smooth scroll support
* Update scrollspy.js/md
* move functionality to method
* Update scrollspy.js
* Add SmoothScroll to docs example
* Refactor Using `Maps` and smaller methods
* Update scrollspy.md/js
* Update scrollspy.spec.js
* Support backwards compatibility
* minor optimizations
* Merge activation functionality
* Update scrollspy.md
* Update scrollspy.js
* Rewording some of the documentation changes
* Update scrollspy.js
* Update scrollspy.md
* tweaking calculation functionality & drop text that suggests, to deactivate target when wrapper is not visible
* tweak calculation
* Fix lint
* Support scrollspy in body & tests
* change doc example to a more valid solution
Co-authored-by: XhmikosR <[email protected]>
Co-authored-by: Patrick H. Lauke <[email protected]>
Diffstat (limited to 'js/tests/unit/scrollspy.spec.js')
| -rw-r--r-- | js/tests/unit/scrollspy.spec.js | 617 |
1 files changed, 383 insertions, 234 deletions
diff --git a/js/tests/unit/scrollspy.spec.js b/js/tests/unit/scrollspy.spec.js index 96ef3aedf..778c37a38 100644 --- a/js/tests/unit/scrollspy.spec.js +++ b/js/tests/unit/scrollspy.spec.js @@ -1,28 +1,71 @@ import ScrollSpy from '../../src/scrollspy' -import Manipulator from '../../src/dom/manipulator' + +/** Test helpers */ import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture' +import EventHandler from '../../src/dom/event-handler' describe('ScrollSpy', () => { let fixtureEl - const testElementIsActiveAfterScroll = ({ elementSelector, targetSelector, contentEl, scrollSpy, spy, cb }) => { + const getElementScrollSpy = element => element.scrollTo ? + spyOn(element, 'scrollTo').and.callThrough() : + spyOnProperty(element, 'scrollTop', 'set').and.callThrough() + + const scrollTo = (el, height) => { + el.scrollTop = height + } + + const onScrollStop = (callback, element, timeout = 30) => { + let handle = null + const onScroll = function () { + if (handle) { + window.clearTimeout(handle) + } + + handle = setTimeout(() => { + element.removeEventListener('scroll', onScroll) + callback() + }, timeout + 1) + } + + element.addEventListener('scroll', onScroll) + } + + const getDummyFixture = () => { + return [ + '<nav id="navBar" 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>', + ' </ul>', + '</nav>', + '<div class="content" data-bs-target="#navBar" style="overflow-y: auto">', + ' <div id="div-jsm-1">div 1</div>', + '</div>' + ].join('') + } + + const testElementIsActiveAfterScroll = ({ elementSelector, targetSelector, contentEl, scrollSpy, 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 + const paddingTop = 0 + const parentOffset = getComputedStyle(contentEl).getPropertyValue('position') === 'relative' ? 0 : contentEl.offsetTop + const scrollHeight = (target.offsetTop - parentOffset) + paddingTop + + contentEl.addEventListener('activate.bs.scrollspy', event => { + if (scrollSpy._activeTarget !== element) { + return + } - function listener() { expect(element).toHaveClass('active') - contentEl.removeEventListener('scroll', listener) - expect(scrollSpy._process).toHaveBeenCalled() - spy.calls.reset() + expect(scrollSpy._activeTarget).toEqual(element) + expect(event.relatedTarget).toEqual(element) cb() - } + }) - contentEl.addEventListener('scroll', listener) - contentEl.scrollTop = scrollHeight + setTimeout(() => { // in case we scroll something before the test + scrollTo(contentEl, scrollHeight) + }, 100) } beforeAll(() => { @@ -53,16 +96,60 @@ describe('ScrollSpy', () => { describe('constructor', () => { it('should take care of element either passed as a CSS selector or DOM element', () => { - fixtureEl.innerHTML = '<nav id="navigation"></nav><div class="content"></div>' + fixtureEl.innerHTML = getDummyFixture() - const sSpyEl = fixtureEl.querySelector('#navigation') - const sSpyBySelector = new ScrollSpy('#navigation') + const sSpyEl = fixtureEl.querySelector('.content') + const sSpyBySelector = new ScrollSpy('.content') const sSpyByElement = new ScrollSpy(sSpyEl) expect(sSpyBySelector._element).toEqual(sSpyEl) expect(sSpyByElement._element).toEqual(sSpyEl) }) + it('should null, if element is not scrollable', () => { + fixtureEl.innerHTML = [ + '<nav id="navigation" class="navbar">', + ' <ul class="navbar-nav">' + + ' <li class="nav-item"><a class="nav-link active" id="one-link" href="#">One</a></li>' + + ' </ul>', + '</nav>', + '<div id="content">', + ' <div id="1" style="height: 300px;">test</div>', + '</div>' + ].join('') + + const scrollSpy = new ScrollSpy(fixtureEl.querySelector('#content'), { + target: '#navigation' + }) + + expect(scrollSpy._observer.root).toBeNull() + expect(scrollSpy._rootElement).toBeNull() + }) + + it('should not take count to not visible sections', () => { + fixtureEl.innerHTML = [ + '<nav id="navigation" class="navbar">', + ' <ul class="navbar-nav">', + ' <li class="nav-item"><a class="nav-link active" 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: 300px;">test</div>', + ' <div id="two" hidden style="height: 300px;">test</div>', + ' <div id="three" style="display: none;">test</div>', + '</div>' + ].join('') + + const scrollSpy = new ScrollSpy(fixtureEl.querySelector('#content'), { + target: '#navigation' + }) + + expect(scrollSpy._observableSections.size).toBe(1) + expect(scrollSpy._targetLinks.size).toBe(1) + }) + it('should not process element without target', () => { fixtureEl.innerHTML = [ '<nav id="navigation" class="navbar">', @@ -73,8 +160,8 @@ describe('ScrollSpy', () => { ' </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 id="two" style="height: 300px;">test</div>', + ' <div id="three" style="height: 10px;">test2</div>', '</div>' ].join('') @@ -82,7 +169,7 @@ describe('ScrollSpy', () => { target: '#navigation' }) - expect(scrollSpy._targets).toHaveSize(2) + expect(scrollSpy._targetLinks).toHaveSize(2) }) it('should only switch "active" class on current target', () => { @@ -100,14 +187,8 @@ describe('ScrollSpy', () => { ' </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 style="height: 200px;" id="masthead">Overview</div>', + ' <div style="height: 200px;" id="detail">Detail</div>', ' </div>', '</div>' ].join('') @@ -120,13 +201,52 @@ describe('ScrollSpy', () => { spyOn(scrollSpy, '_process').and.callThrough() - scrollSpyEl.addEventListener('scroll', () => { + onScrollStop(() => { expect(rootEl).toHaveClass('active') expect(scrollSpy._process).toHaveBeenCalled() resolve() + }, scrollSpyEl) + + scrollTo(scrollSpyEl, 350) + }) + }) + + it('should not process data if `activeTarget` is same as given target', () => { + return new Promise((resolve, reject) => { + 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('') + + const contentEl = fixtureEl.querySelector('.content') + const scrollSpy = new ScrollSpy(contentEl, { + offset: 0, + target: '.navbar' }) - scrollSpyEl.scrollTop = 350 + const triggerSpy = spyOn(EventHandler, 'trigger').and.callThrough() + + scrollSpy._activeTarget = fixtureEl.querySelector('#a-1') + testElementIsActiveAfterScroll({ + elementSelector: '#a-1', + targetSelector: '#div-1', + contentEl, + scrollSpy, + cb: reject + }) + + setTimeout(() => { + expect(triggerSpy).not.toHaveBeenCalled() + resolve() + }, 100) }) }) @@ -145,14 +265,8 @@ describe('ScrollSpy', () => { ' </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 style="height: 200px;" id="masthead">Overview</div>', + ' <div style="height: 200px;" id="detail">Detail</div>', ' </div>', '</div>' ].join('') @@ -165,51 +279,14 @@ describe('ScrollSpy', () => { spyOn(scrollSpy, '_process').and.callThrough() - scrollSpyEl.addEventListener('scroll', () => { + onScrollStop(() => { expect(rootEl).toHaveClass('active') + expect(scrollSpy._activeTarget).toEqual(fixtureEl.querySelector('[href="#detail"]')) expect(scrollSpy._process).toHaveBeenCalled() resolve() - }) - - scrollSpyEl.scrollTop = 350 - }) - }) - - it('should correctly select middle navigation option when large offset is used', () => { - return new Promise(resolve => { - fixtureEl.innerHTML = [ - '<div id="header" style="height: 500px;"></div>', - '<nav id="navigation" class="navbar">', - ' <ul class="navbar-nav">', - ' <li class="nav-item"><a class="nav-link active" 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('') - - const contentEl = fixtureEl.querySelector('#content') - const scrollSpy = new ScrollSpy(contentEl, { - target: '#navigation', - offset: Manipulator.position(contentEl).top - }) - - spyOn(scrollSpy, '_process').and.callThrough() - - contentEl.addEventListener('scroll', () => { - expect(fixtureEl.querySelector('#one-link')).not.toHaveClass('active') - expect(fixtureEl.querySelector('#two-link')).toHaveClass('active') - expect(fixtureEl.querySelector('#three-link')).not.toHaveClass('active') - expect(scrollSpy._process).toHaveBeenCalled() - resolve() - }) + }, scrollSpyEl) - contentEl.scrollTop = 550 + scrollTo(scrollSpyEl, 350) }) }) @@ -233,21 +310,18 @@ describe('ScrollSpy', () => { offset: 0, target: '.navbar' }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() testElementIsActiveAfterScroll({ elementSelector: '#a-1', targetSelector: '#div-1', contentEl, scrollSpy, - spy, cb() { testElementIsActiveAfterScroll({ elementSelector: '#a-2', targetSelector: '#div-2', contentEl, scrollSpy, - spy, cb: resolve }) } @@ -255,7 +329,7 @@ describe('ScrollSpy', () => { }) }) - it('should add the active class to the correct element (nav markup)', () => { + it('should add to nav the active class to the correct element (nav markup)', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '<nav class="navbar">', @@ -275,21 +349,18 @@ describe('ScrollSpy', () => { offset: 0, target: '.navbar' }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() testElementIsActiveAfterScroll({ elementSelector: '#a-1', targetSelector: '#div-1', contentEl, scrollSpy, - spy, cb() { testElementIsActiveAfterScroll({ elementSelector: '#a-2', targetSelector: '#div-2', contentEl, scrollSpy, - spy, cb: resolve }) } @@ -297,7 +368,7 @@ describe('ScrollSpy', () => { }) }) - it('should add the active class to the correct element (list-group markup)', () => { + it('should add to list-group, the active class to the correct element (list-group markup)', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '<nav class="navbar">', @@ -317,21 +388,18 @@ describe('ScrollSpy', () => { offset: 0, target: '.navbar' }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() testElementIsActiveAfterScroll({ elementSelector: '#a-1', targetSelector: '#div-1', contentEl, scrollSpy, - spy, cb() { testElementIsActiveAfterScroll({ elementSelector: '#a-2', targetSelector: '#div-2', contentEl, scrollSpy, - spy, cb: resolve }) } @@ -351,10 +419,10 @@ describe('ScrollSpy', () => { ' </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: 200px;"></div>', + ' <div id="one" style="height: 100px;">text</div>', + ' <div id="two" style="height: 100px;">text</div>', + ' <div id="three" style="height: 100px;">text</div>', ' <div id="spacer" style="height: 100px;"></div>', '</div>' ].join('') @@ -362,29 +430,24 @@ describe('ScrollSpy', () => { const contentEl = fixtureEl.querySelector('#content') const scrollSpy = new ScrollSpy(contentEl, { target: '#navigation', - offset: Manipulator.position(contentEl).top + offset: contentEl.offsetTop }) const spy = spyOn(scrollSpy, '_process').and.callThrough() - let firstTime = true - - contentEl.addEventListener('scroll', () => { - const active = fixtureEl.querySelector('.active') - + onScrollStop(() => { + const active = () => fixtureEl.querySelector('.active') expect(spy).toHaveBeenCalled() - spy.calls.reset() - if (firstTime) { - expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) - expect(active.getAttribute('id')).toEqual('two-link') - firstTime = false - contentEl.scrollTop = 0 - } else { - expect(active).toBeNull() + + expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) + expect(active().getAttribute('id')).toEqual('two-link') + onScrollStop(() => { + expect(active()).toBeNull() resolve() - } - }) + }, contentEl) + scrollTo(contentEl, 0) + }, contentEl) - contentEl.scrollTop = 201 + scrollTo(contentEl, 200) }) }) @@ -399,43 +462,40 @@ describe('ScrollSpy', () => { ' <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 id="content" style="height: 150px; overflow-y: auto;">', + ' <div id="one" style="height: 100px;">test</div>', + ' <div id="two" style="height: 100px;">test</div>', + ' <div id="three" style="height: 100px;">test</div>', + ' <div id="spacer" style="height: 100px;">test</div>', '</div>' ].join('') - const negativeHeight = -10 + const negativeHeight = 0 const startOfSectionTwo = 101 const contentEl = fixtureEl.querySelector('#content') + // eslint-disable-next-line no-unused-vars const scrollSpy = new ScrollSpy(contentEl, { target: '#navigation', - offset: contentEl.offsetTop + rootMargin: '0px 0px -50%' }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() - let firstTime = true + onScrollStop(() => { + const activeId = () => fixtureEl.querySelector('.active').getAttribute('id') - contentEl.addEventListener('scroll', () => { - const active = fixtureEl.querySelector('.active') + expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) + expect(activeId()).toEqual('two-link') + scrollTo(contentEl, negativeHeight) - expect(spy).toHaveBeenCalled() - spy.calls.reset() - if (firstTime) { - expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) - expect(active.getAttribute('id')).toEqual('two-link') - firstTime = false - contentEl.scrollTop = negativeHeight - } else { + onScrollStop(() => { expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) - expect(active.getAttribute('id')).toEqual('one-link') + expect(activeId()).toEqual('one-link') resolve() - } - }) + }, contentEl) - contentEl.scrollTop = startOfSectionTwo + scrollTo(contentEl, 0) + }, contentEl) + + scrollTo(contentEl, startOfSectionTwo) }) }) @@ -465,46 +525,41 @@ describe('ScrollSpy', () => { offset: 0, target: '.navbar' }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() + scrollTo(contentEl, 0) testElementIsActiveAfterScroll({ elementSelector: '#li-100-5', targetSelector: '#div-100-5', - scrollSpy, - spy, contentEl, + scrollSpy, cb() { - contentEl.scrollTop = 0 + scrollTo(contentEl, 0) testElementIsActiveAfterScroll({ - elementSelector: '#li-100-4', - targetSelector: '#div-100-4', - scrollSpy, - spy, + elementSelector: '#li-100-2', + targetSelector: '#div-100-2', contentEl, + scrollSpy, cb() { - contentEl.scrollTop = 0 + scrollTo(contentEl, 0) testElementIsActiveAfterScroll({ elementSelector: '#li-100-3', targetSelector: '#div-100-3', - scrollSpy, - spy, contentEl, + scrollSpy, cb() { - contentEl.scrollTop = 0 + scrollTo(contentEl, 0) testElementIsActiveAfterScroll({ elementSelector: '#li-100-2', targetSelector: '#div-100-2', - scrollSpy, - spy, contentEl, + scrollSpy, cb() { - contentEl.scrollTop = 0 + scrollTo(contentEl, 0) testElementIsActiveAfterScroll({ elementSelector: '#li-100-1', targetSelector: '#div-100-1', - scrollSpy, - spy, contentEl, + scrollSpy, cb: resolve }) } @@ -517,116 +572,73 @@ describe('ScrollSpy', () => { }) }) }) + }) - 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('') - - const contentEl = fixtureEl.querySelector('.content') - const targetEl = fixtureEl.querySelector('#div-jsm-2') - const scrollSpy = new ScrollSpy(contentEl, { - target: '.navbar', - offset: 0, - method: 'offset' - }) + describe('refresh', () => { + it('should disconnect existing observer', () => { + fixtureEl.innerHTML = getDummyFixture() - expect(scrollSpy._offsets[1]).toEqual(Manipulator.offset(targetEl).top) - expect(scrollSpy._offsets[1]).not.toEqual(Manipulator.position(targetEl).top) - }) + const el = fixtureEl.querySelector('.content') + const scrollSpy = new ScrollSpy(el) - 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('') + spyOn(scrollSpy._observer, 'disconnect') - const contentEl = fixtureEl.querySelector('.content') - const targetEl = fixtureEl.querySelector('#div-jsm-2') - const scrollSpy = new ScrollSpy(contentEl, { - target: '.navbar', - offset: 0, - method: 'position' - }) + scrollSpy.refresh() - expect(scrollSpy._offsets[1]).not.toEqual(Manipulator.offset(targetEl).top) - expect(scrollSpy._offsets[1]).toEqual(Manipulator.position(targetEl).top) + expect(scrollSpy._observer.disconnect).toHaveBeenCalled() }) }) describe('dispose', () => { it('should dispose a scrollspy', () => { - fixtureEl.innerHTML = '<div style="display: none;"></div>' + fixtureEl.innerHTML = getDummyFixture() - const divEl = fixtureEl.querySelector('div') - spyOn(divEl, 'addEventListener').and.callThrough() - spyOn(divEl, 'removeEventListener').and.callThrough() + const el = fixtureEl.querySelector('.content') + const scrollSpy = new ScrollSpy(el) - const scrollSpy = new ScrollSpy(divEl) - expect(divEl.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), jasmine.any(Boolean)) + expect(ScrollSpy.getInstance(el)).not.toBeNull() scrollSpy.dispose() - expect(divEl.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), jasmine.any(Boolean)) + expect(ScrollSpy.getInstance(el)).toBeNull() }) }) describe('jQueryInterface', () => { it('should create a scrollspy', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface jQueryMock.elements = [div] - jQueryMock.fn.scrollspy.call(jQueryMock) + jQueryMock.fn.scrollspy.call(jQueryMock, { target: '#navBar' }) expect(ScrollSpy.getInstance(div)).not.toBeNull() }) it('should create a scrollspy with given config', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface jQueryMock.elements = [div] - jQueryMock.fn.scrollspy.call(jQueryMock, { offset: 15 }) + jQueryMock.fn.scrollspy.call(jQueryMock, { rootMargin: '100px' }) spyOn(ScrollSpy.prototype, 'constructor') - expect(ScrollSpy.prototype.constructor).not.toHaveBeenCalledWith(div, { offset: 15 }) + expect(ScrollSpy.prototype.constructor).not.toHaveBeenCalledWith(div, { rootMargin: '100px' }) const scrollspy = ScrollSpy.getInstance(div) expect(scrollspy).not.toBeNull() - expect(scrollspy._config.offset).toEqual(15) + expect(scrollspy._config.rootMargin).toEqual('100px') }) it('should not re create a scrollspy', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') const scrollSpy = new ScrollSpy(div) jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface @@ -638,9 +650,9 @@ describe('ScrollSpy', () => { }) it('should call a scrollspy method', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') const scrollSpy = new ScrollSpy(div) spyOn(scrollSpy, 'refresh') @@ -655,9 +667,9 @@ describe('ScrollSpy', () => { }) it('should throw error on undefined method', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') const action = 'undefinedMethod' jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface @@ -667,29 +679,60 @@ describe('ScrollSpy', () => { jQueryMock.fn.scrollspy.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) + + it('should throw error on protected method', () => { + fixtureEl.innerHTML = getDummyFixture() + + const div = fixtureEl.querySelector('.content') + const action = '_getConfig' + + jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface + jQueryMock.elements = [div] + + expect(() => { + jQueryMock.fn.scrollspy.call(jQueryMock, action) + }).toThrowError(TypeError, `No method named "${action}"`) + }) + + it('should throw error if method "constructor" is being called', () => { + fixtureEl.innerHTML = getDummyFixture() + + const div = fixtureEl.querySelector('.content') + const action = 'constructor' + + jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface + jQueryMock.elements = [div] + + expect(() => { + jQueryMock.fn.scrollspy.call(jQueryMock, action) + }).toThrowError(TypeError, `No method named "${action}"`) + }) }) describe('getInstance', () => { it('should return scrollspy instance', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') - const scrollSpy = new ScrollSpy(div) + const div = fixtureEl.querySelector('.content') + const scrollSpy = new ScrollSpy(div, { target: fixtureEl.querySelector('#navBar') }) expect(ScrollSpy.getInstance(div)).toEqual(scrollSpy) expect(ScrollSpy.getInstance(div)).toBeInstanceOf(ScrollSpy) }) it('should return null if there is no instance', () => { - expect(ScrollSpy.getInstance(fixtureEl)).toBeNull() + fixtureEl.innerHTML = getDummyFixture() + + const div = fixtureEl.querySelector('.content') + expect(ScrollSpy.getInstance(div)).toBeNull() }) }) describe('getOrCreateInstance', () => { it('should return scrollspy instance', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') const scrollspy = new ScrollSpy(div) expect(ScrollSpy.getOrCreateInstance(div)).toEqual(scrollspy) @@ -698,18 +741,18 @@ describe('ScrollSpy', () => { }) it('should return new instance when there is no scrollspy instance', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') expect(ScrollSpy.getInstance(div)).toBeNull() expect(ScrollSpy.getOrCreateInstance(div)).toBeInstanceOf(ScrollSpy) }) it('should return new instance when there is no scrollspy instance with given configuration', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') expect(ScrollSpy.getInstance(div)).toBeNull() const scrollspy = ScrollSpy.getOrCreateInstance(div, { @@ -721,9 +764,9 @@ describe('ScrollSpy', () => { }) it('should return the instance when exists without given configuration', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') const scrollspy = new ScrollSpy(div, { offset: 1 }) @@ -741,13 +784,119 @@ describe('ScrollSpy', () => { describe('event handler', () => { it('should create scrollspy on window load event', () => { - fixtureEl.innerHTML = '<div data-bs-spy="scroll"></div>' + fixtureEl.innerHTML = [ + '<div id="nav"></div>' + + '<div id="wrapper" data-bs-spy="scroll" data-bs-target="#nav" style="overflow-y: auto"></div>' + ].join('') - const scrollSpyEl = fixtureEl.querySelector('div') + const scrollSpyEl = fixtureEl.querySelector('#wrapper') window.dispatchEvent(createEvent('load')) expect(ScrollSpy.getInstance(scrollSpyEl)).not.toBeNull() }) }) + + describe('SmoothScroll', () => { + it('should not enable smoothScroll', () => { + fixtureEl.innerHTML = getDummyFixture() + const offSpy = spyOn(EventHandler, 'off').and.callThrough() + const onSpy = spyOn(EventHandler, 'on').and.callThrough() + + const div = fixtureEl.querySelector('.content') + const target = fixtureEl.querySelector('#navBar') + // eslint-disable-next-line no-new + new ScrollSpy(div, { + offset: 1 + }) + + expect(offSpy).not.toHaveBeenCalledWith(target, 'click.bs.scrollspy') + expect(onSpy).not.toHaveBeenCalledWith(target, 'click.bs.scrollspy') + }) + + it('should enable smoothScroll', () => { + fixtureEl.innerHTML = getDummyFixture() + const offSpy = spyOn(EventHandler, 'off').and.callThrough() + const onSpy = spyOn(EventHandler, 'on').and.callThrough() + + const div = fixtureEl.querySelector('.content') + const target = fixtureEl.querySelector('#navBar') + // eslint-disable-next-line no-new + new ScrollSpy(div, { + offset: 1, + smoothScroll: true + }) + + expect(offSpy).toHaveBeenCalledWith(target, 'click.bs.scrollspy') + expect(onSpy).toHaveBeenCalledWith(target, 'click.bs.scrollspy', '[href]', jasmine.any(Function)) + }) + + it('should not smoothScroll to element if it not handles a scrollspy section', () => { + fixtureEl.innerHTML = [ + '<nav id="navBar" class="navbar">', + ' <ul class="nav">', + ' <a id="anchor-1" href="#div-jsm-1">div 1</a></li>', + ' <a id="anchor-2" href="#foo">div 2</a></li>', + ' </ul>', + '</nav>', + '<div class="content" data-bs-target="#navBar" style="overflow-y: auto">', + ' <div id="div-jsm-1">div 1</div>', + '</div>' + ].join('') + + const div = fixtureEl.querySelector('.content') + // eslint-disable-next-line no-new + new ScrollSpy(div, { + offset: 1, + smoothScroll: true + }) + + const clickSpy = getElementScrollSpy(div) + + fixtureEl.querySelector('#anchor-2').click() + expect(clickSpy).not.toHaveBeenCalled() + }) + + it('should call `scrollTop` if element doesn\'t not support `scrollTo`', () => { + fixtureEl.innerHTML = getDummyFixture() + + const div = fixtureEl.querySelector('.content') + const link = fixtureEl.querySelector('[href="#div-jsm-1"]') + delete div.scrollTo + const clickSpy = getElementScrollSpy(div) + // eslint-disable-next-line no-new + new ScrollSpy(div, { + offset: 1, + smoothScroll: true + }) + + link.click() + expect(clickSpy).toHaveBeenCalled() + }) + + it('should smoothScroll to the proper observable element on anchor click', done => { + fixtureEl.innerHTML = getDummyFixture() + + const div = fixtureEl.querySelector('.content') + const link = fixtureEl.querySelector('[href="#div-jsm-1"]') + const observable = fixtureEl.querySelector('#div-jsm-1') + const clickSpy = getElementScrollSpy(div) + // eslint-disable-next-line no-new + new ScrollSpy(div, { + offset: 1, + smoothScroll: true + }) + + setTimeout(() => { + if (div.scrollTo) { + expect(clickSpy).toHaveBeenCalledWith({ top: observable.offsetTop - div.offsetTop }) + } else { + expect(clickSpy).toHaveBeenCalledWith(observable.offsetTop - div.offsetTop) + } + + done() + }, 100) + link.click() + }) + }) }) |
