scrollspy.spec.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  1. import ScrollSpy from '../../src/scrollspy'
  2. import Manipulator from '../../src/dom/manipulator'
  3. /** Test helpers */
  4. import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture'
  5. describe('ScrollSpy', () => {
  6. let fixtureEl
  7. const testElementIsActiveAfterScroll = ({ elementSelector, targetSelector, contentEl, scrollSpy, spy, cb }) => {
  8. const element = fixtureEl.querySelector(elementSelector)
  9. const target = fixtureEl.querySelector(targetSelector)
  10. // add top padding to fix Chrome on Android failures
  11. const paddingTop = 5
  12. const scrollHeight = Math.ceil(contentEl.scrollTop + Manipulator.position(target).top) + paddingTop
  13. function listener() {
  14. expect(element.classList.contains('active')).toEqual(true)
  15. contentEl.removeEventListener('scroll', listener)
  16. expect(scrollSpy._process).toHaveBeenCalled()
  17. spy.calls.reset()
  18. cb()
  19. }
  20. contentEl.addEventListener('scroll', listener)
  21. contentEl.scrollTop = scrollHeight
  22. }
  23. beforeAll(() => {
  24. fixtureEl = getFixture()
  25. })
  26. afterEach(() => {
  27. clearFixture()
  28. })
  29. describe('VERSION', () => {
  30. it('should return plugin version', () => {
  31. expect(ScrollSpy.VERSION).toEqual(jasmine.any(String))
  32. })
  33. })
  34. describe('Default', () => {
  35. it('should return plugin default config', () => {
  36. expect(ScrollSpy.Default).toEqual(jasmine.any(Object))
  37. })
  38. })
  39. describe('DATA_KEY', () => {
  40. it('should return plugin data key', () => {
  41. expect(ScrollSpy.DATA_KEY).toEqual('bs.scrollspy')
  42. })
  43. })
  44. describe('constructor', () => {
  45. it('should take care of element either passed as a CSS selector or DOM element', () => {
  46. fixtureEl.innerHTML = '<nav id="navigation"></nav><div class="content"></div>'
  47. const sSpyEl = fixtureEl.querySelector('#navigation')
  48. const sSpyBySelector = new ScrollSpy('#navigation')
  49. const sSpyByElement = new ScrollSpy(sSpyEl)
  50. expect(sSpyBySelector._element).toEqual(sSpyEl)
  51. expect(sSpyByElement._element).toEqual(sSpyEl)
  52. })
  53. it('should generate an id when there is not one', () => {
  54. fixtureEl.innerHTML = [
  55. '<nav></nav>',
  56. '<div class="content"></div>'
  57. ].join('')
  58. const navEl = fixtureEl.querySelector('nav')
  59. const scrollSpy = new ScrollSpy(fixtureEl.querySelector('.content'), {
  60. target: navEl
  61. })
  62. expect(scrollSpy).toBeDefined()
  63. expect(navEl.getAttribute('id')).not.toEqual(null)
  64. })
  65. it('should not process element without target', () => {
  66. fixtureEl.innerHTML = [
  67. '<nav id="navigation" class="navbar">',
  68. ' <ul class="navbar-nav">',
  69. ' <li class="nav-item"><a class="nav-link active" id="one-link" href="#">One</a></li>',
  70. ' <li class="nav-item"><a class="nav-link" id="two-link" href="#two">Two</a></li>',
  71. ' <li class="nav-item"><a class="nav-link" id="three-link" href="#three">Three</a></li>',
  72. ' </ul>',
  73. '</nav>',
  74. '<div id="content" style="height: 200px; overflow-y: auto;">',
  75. ' <div id="two" style="height: 300px;"></div>',
  76. ' <div id="three" style="height: 10px;"></div>',
  77. '</div>'
  78. ].join('')
  79. const scrollSpy = new ScrollSpy(fixtureEl.querySelector('#content'), {
  80. target: '#navigation'
  81. })
  82. expect(scrollSpy._targets.length).toEqual(2)
  83. })
  84. it('should only switch "active" class on current target', done => {
  85. fixtureEl.innerHTML = [
  86. '<div id="root" class="active" style="display: block">',
  87. ' <div class="topbar">',
  88. ' <div class="topbar-inner">',
  89. ' <div class="container" id="ss-target">',
  90. ' <ul class="nav">',
  91. ' <li class="nav-item"><a href="#masthead">Overview</a></li>',
  92. ' <li class="nav-item"><a href="#detail">Detail</a></li>',
  93. ' </ul>',
  94. ' </div>',
  95. ' </div>',
  96. ' </div>',
  97. ' <div id="scrollspy-example" style="height: 100px; overflow: auto;">',
  98. ' <div style="height: 200px;">',
  99. ' <h4 id="masthead">Overview</h4>',
  100. ' <p style="height: 200px;"></p>',
  101. ' </div>',
  102. ' <div style="height: 200px;">',
  103. ' <h4 id="detail">Detail</h4>',
  104. ' <p style="height: 200px;"></p>',
  105. ' </div>',
  106. ' </div>',
  107. '</div>'
  108. ].join('')
  109. const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example')
  110. const rootEl = fixtureEl.querySelector('#root')
  111. const scrollSpy = new ScrollSpy(scrollSpyEl, {
  112. target: 'ss-target'
  113. })
  114. spyOn(scrollSpy, '_process').and.callThrough()
  115. scrollSpyEl.addEventListener('scroll', () => {
  116. expect(rootEl.classList.contains('active')).toEqual(true)
  117. expect(scrollSpy._process).toHaveBeenCalled()
  118. done()
  119. })
  120. scrollSpyEl.scrollTop = 350
  121. })
  122. it('should only switch "active" class on current target specified w element', done => {
  123. fixtureEl.innerHTML = [
  124. '<div id="root" class="active" style="display: block">',
  125. ' <div class="topbar">',
  126. ' <div class="topbar-inner">',
  127. ' <div class="container" id="ss-target">',
  128. ' <ul class="nav">',
  129. ' <li class="nav-item"><a href="#masthead">Overview</a></li>',
  130. ' <li class="nav-item"><a href="#detail">Detail</a></li>',
  131. ' </ul>',
  132. ' </div>',
  133. ' </div>',
  134. ' </div>',
  135. ' <div id="scrollspy-example" style="height: 100px; overflow: auto;">',
  136. ' <div style="height: 200px;">',
  137. ' <h4 id="masthead">Overview</h4>',
  138. ' <p style="height: 200px;"></p>',
  139. ' </div>',
  140. ' <div style="height: 200px;">',
  141. ' <h4 id="detail">Detail</h4>',
  142. ' <p style="height: 200px;"></p>',
  143. ' </div>',
  144. ' </div>',
  145. '</div>'
  146. ].join('')
  147. const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example')
  148. const rootEl = fixtureEl.querySelector('#root')
  149. const scrollSpy = new ScrollSpy(scrollSpyEl, {
  150. target: fixtureEl.querySelector('#ss-target')
  151. })
  152. spyOn(scrollSpy, '_process').and.callThrough()
  153. scrollSpyEl.addEventListener('scroll', () => {
  154. expect(rootEl.classList.contains('active')).toEqual(true)
  155. expect(scrollSpy._process).toHaveBeenCalled()
  156. done()
  157. })
  158. scrollSpyEl.scrollTop = 350
  159. })
  160. it('should correctly select middle navigation option when large offset is used', done => {
  161. fixtureEl.innerHTML = [
  162. '<div id="header" style="height: 500px;"></div>',
  163. '<nav id="navigation" class="navbar">',
  164. ' <ul class="navbar-nav">',
  165. ' <li class="nav-item"><a class="nav-link active" id="one-link" href="#one">One</a></li>',
  166. ' <li class="nav-item"><a class="nav-link" id="two-link" href="#two">Two</a></li>',
  167. ' <li class="nav-item"><a class="nav-link" id="three-link" href="#three">Three</a></li>',
  168. ' </ul>',
  169. '</nav>',
  170. '<div id="content" style="height: 200px; overflow-y: auto;">',
  171. ' <div id="one" style="height: 500px;"></div>',
  172. ' <div id="two" style="height: 300px;"></div>',
  173. ' <div id="three" style="height: 10px;"></div>',
  174. '</div>'
  175. ].join('')
  176. const contentEl = fixtureEl.querySelector('#content')
  177. const scrollSpy = new ScrollSpy(contentEl, {
  178. target: '#navigation',
  179. offset: Manipulator.position(contentEl).top
  180. })
  181. spyOn(scrollSpy, '_process').and.callThrough()
  182. contentEl.addEventListener('scroll', () => {
  183. expect(fixtureEl.querySelector('#one-link').classList.contains('active')).toEqual(false)
  184. expect(fixtureEl.querySelector('#two-link').classList.contains('active')).toEqual(true)
  185. expect(fixtureEl.querySelector('#three-link').classList.contains('active')).toEqual(false)
  186. expect(scrollSpy._process).toHaveBeenCalled()
  187. done()
  188. })
  189. contentEl.scrollTop = 550
  190. })
  191. it('should add the active class to the correct element', done => {
  192. fixtureEl.innerHTML = [
  193. '<nav class="navbar">',
  194. ' <ul class="nav">',
  195. ' <li class="nav-item"><a class="nav-link" id="a-1" href="#div-1">div 1</a></li>',
  196. ' <li class="nav-item"><a class="nav-link" id="a-2" href="#div-2">div 2</a></li>',
  197. ' </ul>',
  198. '</nav>',
  199. '<div class="content" style="overflow: auto; height: 50px">',
  200. ' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>',
  201. ' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>',
  202. '</div>'
  203. ].join('')
  204. const contentEl = fixtureEl.querySelector('.content')
  205. const scrollSpy = new ScrollSpy(contentEl, {
  206. offset: 0,
  207. target: '.navbar'
  208. })
  209. const spy = spyOn(scrollSpy, '_process').and.callThrough()
  210. testElementIsActiveAfterScroll({
  211. elementSelector: '#a-1',
  212. targetSelector: '#div-1',
  213. contentEl,
  214. scrollSpy,
  215. spy,
  216. cb: () => {
  217. testElementIsActiveAfterScroll({
  218. elementSelector: '#a-2',
  219. targetSelector: '#div-2',
  220. contentEl,
  221. scrollSpy,
  222. spy,
  223. cb: () => done()
  224. })
  225. }
  226. })
  227. })
  228. it('should add the active class to the correct element (nav markup)', done => {
  229. fixtureEl.innerHTML = [
  230. '<nav class="navbar">',
  231. ' <nav class="nav">',
  232. ' <a class="nav-link" id="a-1" href="#div-1">div 1</a>',
  233. ' <a class="nav-link" id="a-2" href="#div-2">div 2</a>',
  234. ' </nav>',
  235. '</nav>',
  236. '<div class="content" style="overflow: auto; height: 50px">',
  237. ' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>',
  238. ' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>',
  239. '</div>'
  240. ].join('')
  241. const contentEl = fixtureEl.querySelector('.content')
  242. const scrollSpy = new ScrollSpy(contentEl, {
  243. offset: 0,
  244. target: '.navbar'
  245. })
  246. const spy = spyOn(scrollSpy, '_process').and.callThrough()
  247. testElementIsActiveAfterScroll({
  248. elementSelector: '#a-1',
  249. targetSelector: '#div-1',
  250. contentEl,
  251. scrollSpy,
  252. spy,
  253. cb: () => {
  254. testElementIsActiveAfterScroll({
  255. elementSelector: '#a-2',
  256. targetSelector: '#div-2',
  257. contentEl,
  258. scrollSpy,
  259. spy,
  260. cb: () => done()
  261. })
  262. }
  263. })
  264. })
  265. it('should add the active class to the correct element (list-group markup)', done => {
  266. fixtureEl.innerHTML = [
  267. '<nav class="navbar">',
  268. ' <div class="list-group">',
  269. ' <a class="list-group-item" id="a-1" href="#div-1">div 1</a>',
  270. ' <a class="list-group-item" id="a-2" href="#div-2">div 2</a>',
  271. ' </div>',
  272. '</nav>',
  273. '<div class="content" style="overflow: auto; height: 50px">',
  274. ' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>',
  275. ' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>',
  276. '</div>'
  277. ].join('')
  278. const contentEl = fixtureEl.querySelector('.content')
  279. const scrollSpy = new ScrollSpy(contentEl, {
  280. offset: 0,
  281. target: '.navbar'
  282. })
  283. const spy = spyOn(scrollSpy, '_process').and.callThrough()
  284. testElementIsActiveAfterScroll({
  285. elementSelector: '#a-1',
  286. targetSelector: '#div-1',
  287. contentEl,
  288. scrollSpy,
  289. spy,
  290. cb: () => {
  291. testElementIsActiveAfterScroll({
  292. elementSelector: '#a-2',
  293. targetSelector: '#div-2',
  294. contentEl,
  295. scrollSpy,
  296. spy,
  297. cb: () => done()
  298. })
  299. }
  300. })
  301. })
  302. it('should clear selection if above the first section', done => {
  303. fixtureEl.innerHTML = [
  304. '<div id="header" style="height: 500px;"></div>',
  305. '<nav id="navigation" class="navbar">',
  306. ' <ul class="navbar-nav">',
  307. ' <li class="nav-item"><a id="one-link" class="nav-link active" href="#one">One</a></li>',
  308. ' <li class="nav-item"><a id="two-link" class="nav-link" href="#two">Two</a></li>',
  309. ' <li class="nav-item"><a id="three-link" class="nav-link" href="#three">Three</a></li>',
  310. ' </ul>',
  311. '</nav>',
  312. '<div id="content" style="height: 200px; overflow-y: auto;">',
  313. ' <div id="spacer" style="height: 100px;"></div>',
  314. ' <div id="one" style="height: 100px;"></div>',
  315. ' <div id="two" style="height: 100px;"></div>',
  316. ' <div id="three" style="height: 100px;"></div>',
  317. ' <div id="spacer" style="height: 100px;"></div>',
  318. '</div>'
  319. ].join('')
  320. const contentEl = fixtureEl.querySelector('#content')
  321. const scrollSpy = new ScrollSpy(contentEl, {
  322. target: '#navigation',
  323. offset: Manipulator.position(contentEl).top
  324. })
  325. const spy = spyOn(scrollSpy, '_process').and.callThrough()
  326. let firstTime = true
  327. contentEl.addEventListener('scroll', () => {
  328. const active = fixtureEl.querySelector('.active')
  329. expect(spy).toHaveBeenCalled()
  330. spy.calls.reset()
  331. if (firstTime) {
  332. expect(fixtureEl.querySelectorAll('.active').length).toEqual(1)
  333. expect(active.getAttribute('id')).toEqual('two-link')
  334. firstTime = false
  335. contentEl.scrollTop = 0
  336. } else {
  337. expect(active).toBeNull()
  338. done()
  339. }
  340. })
  341. contentEl.scrollTop = 201
  342. })
  343. it('should not clear selection if above the first section and first section is at the top', done => {
  344. fixtureEl.innerHTML = [
  345. '<div id="header" style="height: 500px;"></div>',
  346. '<nav id="navigation" class="navbar">',
  347. ' <ul class="navbar-nav">',
  348. ' <li class="nav-item"><a id="one-link" class="nav-link active" href="#one">One</a></li>',
  349. ' <li class="nav-item"><a id="two-link" class="nav-link" href="#two">Two</a></li>',
  350. ' <li class="nav-item"><a id="three-link" class="nav-link" href="#three">Three</a></li>',
  351. ' </ul>',
  352. '</nav>',
  353. '<div id="content" style="height: 200px; overflow-y: auto;">',
  354. ' <div id="one" style="height: 100px;"></div>',
  355. ' <div id="two" style="height: 100px;"></div>',
  356. ' <div id="three" style="height: 100px;"></div>',
  357. ' <div id="spacer" style="height: 100px;"></div>',
  358. '</div>'
  359. ].join('')
  360. const negativeHeight = -10
  361. const startOfSectionTwo = 101
  362. const contentEl = fixtureEl.querySelector('#content')
  363. const scrollSpy = new ScrollSpy(contentEl, {
  364. target: '#navigation',
  365. offset: contentEl.offsetTop
  366. })
  367. const spy = spyOn(scrollSpy, '_process').and.callThrough()
  368. let firstTime = true
  369. contentEl.addEventListener('scroll', () => {
  370. const active = fixtureEl.querySelector('.active')
  371. expect(spy).toHaveBeenCalled()
  372. spy.calls.reset()
  373. if (firstTime) {
  374. expect(fixtureEl.querySelectorAll('.active').length).toEqual(1)
  375. expect(active.getAttribute('id')).toEqual('two-link')
  376. firstTime = false
  377. contentEl.scrollTop = negativeHeight
  378. } else {
  379. expect(fixtureEl.querySelectorAll('.active').length).toEqual(1)
  380. expect(active.getAttribute('id')).toEqual('one-link')
  381. done()
  382. }
  383. })
  384. contentEl.scrollTop = startOfSectionTwo
  385. })
  386. it('should correctly select navigation element on backward scrolling when each target section height is 100%', done => {
  387. fixtureEl.innerHTML = [
  388. '<nav class="navbar">',
  389. ' <ul class="nav">',
  390. ' <li class="nav-item"><a id="li-100-1" class="nav-link" href="#div-100-1">div 1</a></li>',
  391. ' <li class="nav-item"><a id="li-100-2" class="nav-link" href="#div-100-2">div 2</a></li>',
  392. ' <li class="nav-item"><a id="li-100-3" class="nav-link" href="#div-100-3">div 3</a></li>',
  393. ' <li class="nav-item"><a id="li-100-4" class="nav-link" href="#div-100-4">div 4</a></li>',
  394. ' <li class="nav-item"><a id="li-100-5" class="nav-link" href="#div-100-5">div 5</a></li>',
  395. ' </ul>',
  396. '</nav>',
  397. '<div class="content" style="position: relative; overflow: auto; height: 100px">',
  398. ' <div id="div-100-1" style="position: relative; height: 100%; padding: 0; margin: 0">div 1</div>',
  399. ' <div id="div-100-2" style="position: relative; height: 100%; padding: 0; margin: 0">div 2</div>',
  400. ' <div id="div-100-3" style="position: relative; height: 100%; padding: 0; margin: 0">div 3</div>',
  401. ' <div id="div-100-4" style="position: relative; height: 100%; padding: 0; margin: 0">div 4</div>',
  402. ' <div id="div-100-5" style="position: relative; height: 100%; padding: 0; margin: 0">div 5</div>',
  403. '</div>'
  404. ].join('')
  405. const contentEl = fixtureEl.querySelector('.content')
  406. const scrollSpy = new ScrollSpy(contentEl, {
  407. offset: 0,
  408. target: '.navbar'
  409. })
  410. const spy = spyOn(scrollSpy, '_process').and.callThrough()
  411. testElementIsActiveAfterScroll({
  412. elementSelector: '#li-100-5',
  413. targetSelector: '#div-100-5',
  414. scrollSpy,
  415. spy,
  416. contentEl,
  417. cb() {
  418. contentEl.scrollTop = 0
  419. testElementIsActiveAfterScroll({
  420. elementSelector: '#li-100-4',
  421. targetSelector: '#div-100-4',
  422. scrollSpy,
  423. spy,
  424. contentEl,
  425. cb() {
  426. contentEl.scrollTop = 0
  427. testElementIsActiveAfterScroll({
  428. elementSelector: '#li-100-3',
  429. targetSelector: '#div-100-3',
  430. scrollSpy,
  431. spy,
  432. contentEl,
  433. cb() {
  434. contentEl.scrollTop = 0
  435. testElementIsActiveAfterScroll({
  436. elementSelector: '#li-100-2',
  437. targetSelector: '#div-100-2',
  438. scrollSpy,
  439. spy,
  440. contentEl,
  441. cb() {
  442. contentEl.scrollTop = 0
  443. testElementIsActiveAfterScroll({
  444. elementSelector: '#li-100-1',
  445. targetSelector: '#div-100-1',
  446. scrollSpy,
  447. spy,
  448. contentEl,
  449. cb: done
  450. })
  451. }
  452. })
  453. }
  454. })
  455. }
  456. })
  457. }
  458. })
  459. })
  460. it('should allow passed in option offset method: offset', () => {
  461. fixtureEl.innerHTML = [
  462. '<nav class="navbar">',
  463. ' <ul class="nav">',
  464. ' <li class="nav-item"><a id="li-jsm-1" class="nav-link" href="#div-jsm-1">div 1</a></li>',
  465. ' <li class="nav-item"><a id="li-jsm-2" class="nav-link" href="#div-jsm-2">div 2</a></li>',
  466. ' <li class="nav-item"><a id="li-jsm-3" class="nav-link" href="#div-jsm-3">div 3</a></li>',
  467. ' </ul>',
  468. '</nav>',
  469. '<div class="content" style="position: relative; overflow: auto; height: 100px">',
  470. ' <div id="div-jsm-1" style="position: relative; height: 200px; padding: 0; margin: 0">div 1</div>',
  471. ' <div id="div-jsm-2" style="position: relative; height: 150px; padding: 0; margin: 0">div 2</div>',
  472. ' <div id="div-jsm-3" style="position: relative; height: 250px; padding: 0; margin: 0">div 3</div>',
  473. '</div>'
  474. ].join('')
  475. const contentEl = fixtureEl.querySelector('.content')
  476. const targetEl = fixtureEl.querySelector('#div-jsm-2')
  477. const scrollSpy = new ScrollSpy(contentEl, {
  478. target: '.navbar',
  479. offset: 0,
  480. method: 'offset'
  481. })
  482. expect(scrollSpy._offsets[1]).toEqual(Manipulator.offset(targetEl).top)
  483. expect(scrollSpy._offsets[1]).not.toEqual(Manipulator.position(targetEl).top)
  484. })
  485. it('should allow passed in option offset method: position', () => {
  486. fixtureEl.innerHTML = [
  487. '<nav class="navbar">',
  488. ' <ul class="nav">',
  489. ' <li class="nav-item"><a id="li-jsm-1" class="nav-link" href="#div-jsm-1">div 1</a></li>',
  490. ' <li class="nav-item"><a id="li-jsm-2" class="nav-link" href="#div-jsm-2">div 2</a></li>',
  491. ' <li class="nav-item"><a id="li-jsm-3" class="nav-link" href="#div-jsm-3">div 3</a></li>',
  492. ' </ul>',
  493. '</nav>',
  494. '<div class="content" style="position: relative; overflow: auto; height: 100px">',
  495. ' <div id="div-jsm-1" style="position: relative; height: 200px; padding: 0; margin: 0">div 1</div>',
  496. ' <div id="div-jsm-2" style="position: relative; height: 150px; padding: 0; margin: 0">div 2</div>',
  497. ' <div id="div-jsm-3" style="position: relative; height: 250px; padding: 0; margin: 0">div 3</div>',
  498. '</div>'
  499. ].join('')
  500. const contentEl = fixtureEl.querySelector('.content')
  501. const targetEl = fixtureEl.querySelector('#div-jsm-2')
  502. const scrollSpy = new ScrollSpy(contentEl, {
  503. target: '.navbar',
  504. offset: 0,
  505. method: 'position'
  506. })
  507. expect(scrollSpy._offsets[1]).not.toEqual(Manipulator.offset(targetEl).top)
  508. expect(scrollSpy._offsets[1]).toEqual(Manipulator.position(targetEl).top)
  509. })
  510. })
  511. describe('dispose', () => {
  512. it('should dispose a scrollspy', () => {
  513. fixtureEl.innerHTML = '<div style="display: none;"></div>'
  514. const divEl = fixtureEl.querySelector('div')
  515. spyOn(divEl, 'addEventListener').and.callThrough()
  516. spyOn(divEl, 'removeEventListener').and.callThrough()
  517. const scrollSpy = new ScrollSpy(divEl)
  518. expect(divEl.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), jasmine.any(Boolean))
  519. scrollSpy.dispose()
  520. expect(divEl.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), jasmine.any(Boolean))
  521. })
  522. })
  523. describe('jQueryInterface', () => {
  524. it('should create a scrollspy', () => {
  525. fixtureEl.innerHTML = '<div></div>'
  526. const div = fixtureEl.querySelector('div')
  527. jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface
  528. jQueryMock.elements = [div]
  529. jQueryMock.fn.scrollspy.call(jQueryMock)
  530. expect(ScrollSpy.getInstance(div)).not.toBeNull()
  531. })
  532. it('should create a scrollspy with given config', () => {
  533. fixtureEl.innerHTML = '<div></div>'
  534. const div = fixtureEl.querySelector('div')
  535. jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface
  536. jQueryMock.elements = [div]
  537. jQueryMock.fn.scrollspy.call(jQueryMock, { offset: 15 })
  538. spyOn(ScrollSpy.prototype, 'constructor')
  539. expect(ScrollSpy.prototype.constructor).not.toHaveBeenCalledWith(div, { offset: 15 })
  540. const scrollspy = ScrollSpy.getInstance(div)
  541. expect(scrollspy).not.toBeNull()
  542. expect(scrollspy._config.offset).toBe(15)
  543. })
  544. it('should not re create a scrollspy', () => {
  545. fixtureEl.innerHTML = '<div></div>'
  546. const div = fixtureEl.querySelector('div')
  547. const scrollSpy = new ScrollSpy(div)
  548. jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface
  549. jQueryMock.elements = [div]
  550. jQueryMock.fn.scrollspy.call(jQueryMock)
  551. expect(ScrollSpy.getInstance(div)).toEqual(scrollSpy)
  552. })
  553. it('should call a scrollspy method', () => {
  554. fixtureEl.innerHTML = '<div></div>'
  555. const div = fixtureEl.querySelector('div')
  556. const scrollSpy = new ScrollSpy(div)
  557. spyOn(scrollSpy, 'refresh')
  558. jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface
  559. jQueryMock.elements = [div]
  560. jQueryMock.fn.scrollspy.call(jQueryMock, 'refresh')
  561. expect(ScrollSpy.getInstance(div)).toEqual(scrollSpy)
  562. expect(scrollSpy.refresh).toHaveBeenCalled()
  563. })
  564. it('should throw error on undefined method', () => {
  565. fixtureEl.innerHTML = '<div></div>'
  566. const div = fixtureEl.querySelector('div')
  567. const action = 'undefinedMethod'
  568. jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface
  569. jQueryMock.elements = [div]
  570. expect(() => {
  571. jQueryMock.fn.scrollspy.call(jQueryMock, action)
  572. }).toThrowError(TypeError, `No method named "${action}"`)
  573. })
  574. })
  575. describe('getInstance', () => {
  576. it('should return scrollspy instance', () => {
  577. fixtureEl.innerHTML = '<div></div>'
  578. const div = fixtureEl.querySelector('div')
  579. const scrollSpy = new ScrollSpy(div)
  580. expect(ScrollSpy.getInstance(div)).toEqual(scrollSpy)
  581. expect(ScrollSpy.getInstance(div)).toBeInstanceOf(ScrollSpy)
  582. })
  583. it('should return null if there is no instance', () => {
  584. expect(ScrollSpy.getInstance(fixtureEl)).toEqual(null)
  585. })
  586. })
  587. describe('getOrCreateInstance', () => {
  588. it('should return scrollspy instance', () => {
  589. fixtureEl.innerHTML = '<div></div>'
  590. const div = fixtureEl.querySelector('div')
  591. const scrollspy = new ScrollSpy(div)
  592. expect(ScrollSpy.getOrCreateInstance(div)).toEqual(scrollspy)
  593. expect(ScrollSpy.getInstance(div)).toEqual(ScrollSpy.getOrCreateInstance(div, {}))
  594. expect(ScrollSpy.getOrCreateInstance(div)).toBeInstanceOf(ScrollSpy)
  595. })
  596. it('should return new instance when there is no scrollspy instance', () => {
  597. fixtureEl.innerHTML = '<div></div>'
  598. const div = fixtureEl.querySelector('div')
  599. expect(ScrollSpy.getInstance(div)).toEqual(null)
  600. expect(ScrollSpy.getOrCreateInstance(div)).toBeInstanceOf(ScrollSpy)
  601. })
  602. it('should return new instance when there is no scrollspy instance with given configuration', () => {
  603. fixtureEl.innerHTML = '<div></div>'
  604. const div = fixtureEl.querySelector('div')
  605. expect(ScrollSpy.getInstance(div)).toEqual(null)
  606. const scrollspy = ScrollSpy.getOrCreateInstance(div, {
  607. offset: 1
  608. })
  609. expect(scrollspy).toBeInstanceOf(ScrollSpy)
  610. expect(scrollspy._config.offset).toEqual(1)
  611. })
  612. it('should return the instance when exists without given configuration', () => {
  613. fixtureEl.innerHTML = '<div></div>'
  614. const div = fixtureEl.querySelector('div')
  615. const scrollspy = new ScrollSpy(div, {
  616. offset: 1
  617. })
  618. expect(ScrollSpy.getInstance(div)).toEqual(scrollspy)
  619. const scrollspy2 = ScrollSpy.getOrCreateInstance(div, {
  620. offset: 2
  621. })
  622. expect(scrollspy).toBeInstanceOf(ScrollSpy)
  623. expect(scrollspy2).toEqual(scrollspy)
  624. expect(scrollspy2._config.offset).toEqual(1)
  625. })
  626. })
  627. describe('event handler', () => {
  628. it('should create scrollspy on window load event', () => {
  629. fixtureEl.innerHTML = '<div data-bs-spy="scroll"></div>'
  630. const scrollSpyEl = fixtureEl.querySelector('div')
  631. window.dispatchEvent(createEvent('load'))
  632. expect(ScrollSpy.getInstance(scrollSpyEl)).not.toBeNull()
  633. })
  634. })
  635. })