From 79aed2d132f6d434fd326bf974ca86d11bde0c66 Mon Sep 17 00:00:00 2001 From: Conrad Chan Date: Fri, 4 Jan 2019 14:12:10 -0800 Subject: [PATCH] Chore: adding unit tests for VirtualScroller (#882) --- src/lib/VirtualScroller.js | 4 +- src/lib/__tests__/VirtualScroller-test.html | 1 + src/lib/__tests__/VirtualScroller-test.js | 223 ++++++++++++++++++++ 3 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 src/lib/__tests__/VirtualScroller-test.html create mode 100644 src/lib/__tests__/VirtualScroller-test.js diff --git a/src/lib/VirtualScroller.js b/src/lib/VirtualScroller.js index 27dcbc130a..d99b36415e 100644 --- a/src/lib/VirtualScroller.js +++ b/src/lib/VirtualScroller.js @@ -3,6 +3,7 @@ import isFunction from 'lodash/isFunction'; import throttle from 'lodash/throttle'; const BUFFERED_ITEM_MULTIPLIER = 3; +const SCROLL_THROTTLE_MS = 50; class VirtualScroller { /** @property {HTMLElement} - The anchor element for this Virtual Scroller */ @@ -50,8 +51,7 @@ class VirtualScroller { this.previousScrollTop = 0; this.createListElement = this.createListElement.bind(this); - this.onScrollHandler = this.onScrollHandler.bind(this); - this.throttledOnScrollHandler = throttle(this.onScrollHandler, 50); + this.onScrollHandler = throttle(this.onScrollHandler.bind(this), SCROLL_THROTTLE_MS); this.renderItems = this.renderItems.bind(this); } diff --git a/src/lib/__tests__/VirtualScroller-test.html b/src/lib/__tests__/VirtualScroller-test.html new file mode 100644 index 0000000000..03a74a8013 --- /dev/null +++ b/src/lib/__tests__/VirtualScroller-test.html @@ -0,0 +1 @@ +
diff --git a/src/lib/__tests__/VirtualScroller-test.js b/src/lib/__tests__/VirtualScroller-test.js new file mode 100644 index 0000000000..d5a6d8694e --- /dev/null +++ b/src/lib/__tests__/VirtualScroller-test.js @@ -0,0 +1,223 @@ +/* eslint-disable no-unused-expressions */ +import VirtualScroller from '../VirtualScroller'; + +let virtualScroller; +let stubs = {}; + +const sandbox = sinon.sandbox.create(); + +describe('VirtualScroller', () => { + before(() => fixture.setBase('src/lib')); + + beforeEach(() => { + fixture.load('__tests__/VirtualScroller-test.html'); + virtualScroller = new VirtualScroller(document.getElementById('test-virtual-scroller')); + }); + + afterEach(() => { + fixture.cleanup(); + sandbox.verifyAndRestore(); + + if (virtualScroller && typeof virtualScroller.destroy === 'function') { + virtualScroller.destroy(); + } + + virtualScroller = null; + stubs = {}; + }); + + describe('constructor()', () => { + it('should initialize anchorEl and previousScrollTop', () => { + expect(virtualScroller.anchorEl.id).to.be.equal('test-virtual-scroller'); + expect(virtualScroller.previousScrollTop).to.be.equal(0); + }); + }); + + describe('destroy()', () => { + it('should remove the HTML element references', () => { + const scrollingEl = { remove: () => {} }; + sandbox.stub(scrollingEl, 'remove'); + + virtualScroller.scrollingEl = scrollingEl; + virtualScroller.listEl = {}; + + virtualScroller.destroy(); + + expect(scrollingEl.remove).to.be.called; + expect(virtualScroller.scrollingEl).to.be.null; + expect(virtualScroller.listEl).to.be.null; + }); + }); + + describe('init()', () => { + beforeEach(() => { + stubs.validateRequiredConfig = sandbox.stub(virtualScroller, 'validateRequiredConfig'); + }); + + it('should parse the config object', () => { + stubs.renderItemFn = sandbox.stub(); + stubs.renderItems = sandbox.stub(virtualScroller, 'renderItems'); + stubs.bindDOMListeners = sandbox.stub(virtualScroller, 'bindDOMListeners'); + + virtualScroller.init({ + totalItems: 10, + itemHeight: 100, + containerHeight: 500, + renderItemFn: stubs.renderItemFn + }); + + expect(virtualScroller.totalItems).to.be.equal(10); + expect(virtualScroller.itemHeight).to.be.equal(100); + expect(virtualScroller.containerHeight).to.be.equal(500); + expect(virtualScroller.renderItemFn).to.be.equal(stubs.renderItemFn); + expect(virtualScroller.margin).to.be.equal(0); + expect(virtualScroller.totalViewItems).to.be.equal(5); + expect(virtualScroller.maxBufferHeight).to.be.equal(500); + expect(virtualScroller.maxRenderedItems).to.be.equal(18); + + expect(virtualScroller.scrollingEl.classList.contains('bp-vs')).to.be.true; + expect(virtualScroller.listEl.classList.contains('bp-vs-list')).to.be.true; + + expect(stubs.renderItems).to.be.called; + expect(stubs.bindDOMListeners).to.be.called; + }); + }); + + describe('validateRequiredConfig()', () => { + it('should not throw an error if config is good', () => { + expect(() => + virtualScroller.validateRequiredConfig({ + totalItems: 10, + itemHeight: 100, + renderItemFn: () => {}, + containerHeight: 500 + }) + ).to.not.throw(); + }); + + [ + { name: 'totalItems falsy', config: {} }, + { name: 'totalItems not finite', config: { totalItems: '10' } }, + { name: 'itemHeight falsy', config: { totalItems: 10 } }, + { name: 'itemHeight not finite', config: { totalItems: 10, itemHeight: '100' } }, + { name: 'renderItemFn falsy', config: { totalItems: 10, itemHeight: 100 } }, + { name: 'renderItemFn not a function', config: { totalItems: 10, itemHeight: 100, renderItemFn: 'hi' } }, + { name: 'containerHeight falsy', config: { totalItems: 10, itemHeight: 100, renderItemFn: () => {} } }, + { + name: 'containerHeight not finite', + config: { totalItems: 10, itemHeight: 100, renderItemFn: () => {}, containerHeight: '500' } + } + ].forEach((data) => { + it(`should throw an error if config is bad: ${data.name}`, () => { + expect(() => virtualScroller.validateRequiredConfig(data.config)).to.throw(); + }); + }); + }); + + describe('onScrollHandler()', () => { + beforeEach(() => { + stubs.renderItems = sandbox.stub(virtualScroller, 'renderItems'); + virtualScroller.maxBufferHeight = 100; + }); + + it('should not proceed if the scroll movement < maxBufferHeight', () => { + virtualScroller.previousScrollTop = 0; + virtualScroller.onScrollHandler({ target: { scrollTop: 10 } }); + + expect(stubs.renderItems).to.not.be.called; + }); + + it('should proceed if positive scroll movement > maxBufferHeight', () => { + virtualScroller.previousScrollTop = 0; + virtualScroller.onScrollHandler({ target: { scrollTop: 101 } }); + + expect(stubs.renderItems).to.be.called; + expect(virtualScroller.previousScrollTop).to.be.equal(101); + }); + + it('should proceed if negative scroll movement > maxBufferHeight', () => { + virtualScroller.previousScrollTop = 102; + virtualScroller.onScrollHandler({ target: { scrollTop: 1 } }); + + expect(stubs.renderItems).to.be.called; + expect(virtualScroller.previousScrollTop).to.be.equal(1); + }); + }); + + describe('renderItems()', () => { + let newListEl; + + beforeEach(() => { + virtualScroller.scrollingEl = { replaceChild: () => {} }; + newListEl = { appendChild: () => {} }; + stubs.createListElement = sandbox.stub(virtualScroller, 'createListElement').returns(newListEl); + stubs.renderItem = sandbox.stub(virtualScroller, 'renderItem'); + stubs.replaceChild = sandbox.stub(virtualScroller.scrollingEl, 'replaceChild'); + stubs.appendChild = sandbox.stub(newListEl, 'appendChild'); + }); + + afterEach(() => { + virtualScroller.scrollingEl = null; + }); + + it('should render maxRenderedItems', () => { + virtualScroller.maxRenderedItems = 10; + virtualScroller.totalItems = 100; + virtualScroller.renderItems(); + + expect(newListEl.appendChild.callCount).to.be.equal(10); + expect(virtualScroller.scrollingEl.replaceChild).to.be.called; + }); + + it('should render the remaining items up to totalItems', () => { + virtualScroller.maxRenderedItems = 10; + virtualScroller.totalItems = 100; + virtualScroller.renderItems(95); + + expect(newListEl.appendChild.callCount).to.be.equal(5); + expect(virtualScroller.scrollingEl.replaceChild).to.be.called; + }); + }); + + describe('renderItem()', () => { + it('should render an item absolutely positioned with arbitrary content', () => { + const renderedThumbnail = document.createElement('div'); + renderedThumbnail.className = 'rendered-thumbnail'; + stubs.renderItemFn = sandbox.stub().returns(renderedThumbnail); + + virtualScroller.itemHeight = 100; + virtualScroller.margin = 0; + virtualScroller.renderItemFn = stubs.renderItemFn; + + const item = virtualScroller.renderItem(0); + expect(stubs.renderItemFn).to.be.called; + expect(item.classList.contains('bp-vs-list-item')).to.be.true; + expect(item.firstChild.classList.contains('rendered-thumbnail')).to.be.true; + }); + + it('should still render the item even if renderItemFn throws an error', () => { + const renderedThumbnail = document.createElement('div'); + renderedThumbnail.className = 'rendered-thumbnail'; + stubs.renderItemFn = sandbox.stub().throws(); + + virtualScroller.itemHeight = 100; + virtualScroller.margin = 0; + virtualScroller.renderItemFn = stubs.renderItemFn; + + const item = virtualScroller.renderItem(0); + expect(stubs.renderItemFn).to.be.called; + expect(item.classList.contains('bp-vs-list-item')).to.be.true; + expect(item.firstChild).to.be.null; + }); + }); + + describe('createListElement()', () => { + it('should return the list element', () => { + virtualScroller.totalItems = 10; + virtualScroller.itemHeight = 100; + virtualScroller.margin = 0; + + expect(virtualScroller.createListElement().classList.contains('bp-vs-list')).to.be.true; + }); + }); +});