import ScrollSpy from '../../src/scrollspy' import Manipulator from '../../src/dom/manipulator' import { clearFixture, createEvent, getFixture, jQueryMock } from '../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).toHaveClass('active') contentEl.removeEventListener('scroll', listener) expect(scrollSpy._process).toHaveBeenCalled() spy.calls.reset() cb() } contentEl.addEventListener('scroll', listener) contentEl.scrollTop = scrollHeight } beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { 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('DATA_KEY', () => { it('should return plugin data key', () => { expect(ScrollSpy.DATA_KEY).toEqual('bs.scrollspy') }) }) describe('constructor', () => { it('should take care of element either passed as a CSS selector or DOM element', () => { fixtureEl.innerHTML = '
' const sSpyEl = fixtureEl.querySelector('#navigation') const sSpyBySelector = new ScrollSpy('#navigation') const sSpyByElement = new ScrollSpy(sSpyEl) expect(sSpyBySelector._element).toEqual(sSpyEl) expect(sSpyByElement._element).toEqual(sSpyEl) }) it('should not process element without target', () => { fixtureEl.innerHTML = [ '', '
', '
', '
', '
' ].join('') const scrollSpy = new ScrollSpy(fixtureEl.querySelector('#content'), { target: '#navigation' }) expect(scrollSpy._targets).toHaveSize(2) }) it('should only switch "active" class on current target', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', '
', '
', ' ', '
', '
', '
', '
', '
', '

Overview

', '

', '
', '
', '

Detail

', '

', '
', '
', '
' ].join('') 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).toHaveClass('active') expect(scrollSpy._process).toHaveBeenCalled() resolve() }) scrollSpyEl.scrollTop = 350 }) }) it('should only switch "active" class on current target specified w element', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '
', '
', '
', '
', ' ', '
', '
', '
', '
', '
', '

Overview

', '

', '
', '
', '

Detail

', '

', '
', '
', '
' ].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).toHaveClass('active') 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 = [ '', '', '
', '
', '
', '
', '
' ].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() }) contentEl.scrollTop = 550 }) }) it('should add the active class to the correct element', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
div 1
', '
div 2
', '
' ].join('') 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: resolve }) } }) }) }) it('should add the active class to the correct element (nav markup)', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
div 1
', '
div 2
', '
' ].join('') 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: resolve }) } }) }) }) it('should add the active class to the correct element (list-group markup)', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
div 1
', '
div 2
', '
' ].join('') 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: resolve }) } }) }) }) it('should clear selection if above the first section', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '', '
', '
', '
', '
', '
', '
', '
' ].join('') 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')).toHaveSize(1) expect(active.getAttribute('id')).toEqual('two-link') firstTime = false contentEl.scrollTop = 0 } else { expect(active).toBeNull() resolve() } }) contentEl.scrollTop = 201 }) }) it('should not clear selection if above the first section and first section is at the top', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '', '
', '
', '
', '
', '
', '
' ].join('') 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')).toHaveSize(1) expect(active.getAttribute('id')).toEqual('two-link') firstTime = false contentEl.scrollTop = negativeHeight } else { expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) expect(active.getAttribute('id')).toEqual('one-link') resolve() } }) contentEl.scrollTop = startOfSectionTwo }) }) it('should correctly select navigation element on backward scrolling when each target section height is 100%', () => { return new Promise(resolve => { fixtureEl.innerHTML = [ '', '
', '
div 1
', '
div 2
', '
div 3
', '
div 4
', '
div 5
', '
' ].join('') 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: resolve }) } }) } }) } }) } }) }) }) it('should allow passed in option offset method: offset', () => { fixtureEl.innerHTML = [ '', '
', '
div 1
', '
div 2
', '
div 3
', '
' ].join('') 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 = [ '', '
', '
div 1
', '
div 2
', '
div 3
', '
' ].join('') 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', () => { fixtureEl.innerHTML = '
' const divEl = fixtureEl.querySelector('div') spyOn(divEl, 'addEventListener').and.callThrough() spyOn(divEl, 'removeEventListener').and.callThrough() const scrollSpy = new ScrollSpy(divEl) expect(divEl.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), jasmine.any(Boolean)) scrollSpy.dispose() expect(divEl.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), jasmine.any(Boolean)) }) }) describe('jQueryInterface', () => { it('should create a scrollspy', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.scrollspy.call(jQueryMock) expect(ScrollSpy.getInstance(div)).not.toBeNull() }) it('should create a scrollspy with given config', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.scrollspy.call(jQueryMock, { offset: 15 }) spyOn(ScrollSpy.prototype, 'constructor') expect(ScrollSpy.prototype.constructor).not.toHaveBeenCalledWith(div, { offset: 15 }) const scrollspy = ScrollSpy.getInstance(div) expect(scrollspy).not.toBeNull() expect(scrollspy._config.offset).toEqual(15) }) it('should not re create a scrollspy', () => { fixtureEl.innerHTML = '
' 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 = '
' 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 = '
' const div = fixtureEl.querySelector('div') const action = 'undefinedMethod' 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 = '
' const div = fixtureEl.querySelector('div') const scrollSpy = new ScrollSpy(div) 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() }) }) describe('getOrCreateInstance', () => { it('should return scrollspy instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const scrollspy = new ScrollSpy(div) expect(ScrollSpy.getOrCreateInstance(div)).toEqual(scrollspy) expect(ScrollSpy.getInstance(div)).toEqual(ScrollSpy.getOrCreateInstance(div, {})) expect(ScrollSpy.getOrCreateInstance(div)).toBeInstanceOf(ScrollSpy) }) it('should return new instance when there is no scrollspy instance', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') 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 = '
' const div = fixtureEl.querySelector('div') expect(ScrollSpy.getInstance(div)).toBeNull() const scrollspy = ScrollSpy.getOrCreateInstance(div, { offset: 1 }) expect(scrollspy).toBeInstanceOf(ScrollSpy) expect(scrollspy._config.offset).toEqual(1) }) it('should return the instance when exists without given configuration', () => { fixtureEl.innerHTML = '
' const div = fixtureEl.querySelector('div') const scrollspy = new ScrollSpy(div, { offset: 1 }) expect(ScrollSpy.getInstance(div)).toEqual(scrollspy) const scrollspy2 = ScrollSpy.getOrCreateInstance(div, { offset: 2 }) expect(scrollspy).toBeInstanceOf(ScrollSpy) expect(scrollspy2).toEqual(scrollspy) expect(scrollspy2._config.offset).toEqual(1) }) }) describe('event handler', () => { it('should create scrollspy on window load event', () => { fixtureEl.innerHTML = '
' const scrollSpyEl = fixtureEl.querySelector('div') window.dispatchEvent(createEvent('load')) expect(ScrollSpy.getInstance(scrollSpyEl)).not.toBeNull() }) }) })