diff --git a/karma.conf.js b/karma.conf.js index 8018451..6a211be 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -78,11 +78,9 @@ module.exports = function(config) { mochaReporter: { ignoreSkipped: true }, - // webpackMiddleware: { - // // webpack-dev-middleware configuration - // // i. e. - // stats: 'errors-only' - // } + webpackMiddleware: { + logLevel: 'silent' + }, }); }; diff --git a/src/array-virtual-repeat-strategy.ts b/src/array-virtual-repeat-strategy.ts index 069ec03..368b711 100644 --- a/src/array-virtual-repeat-strategy.ts +++ b/src/array-virtual-repeat-strategy.ts @@ -2,7 +2,15 @@ import { ICollectionObserverSplice, mergeSplice } from 'aurelia-binding'; import { ViewSlot } from 'aurelia-templating'; import { ArrayRepeatStrategy, createFullOverrideContext } from 'aurelia-templating-resources'; import { IView, IVirtualRepeatStrategy } from './interfaces'; -import { getElementDistanceToBottomViewPort, Math$abs, Math$floor, Math$max, Math$min, rebindAndMoveView, updateVirtualOverrideContexts, updateVirtualRepeatContexts } from './utilities'; +import { + getElementDistanceToBottomViewPort, + Math$abs, + Math$floor, + Math$max, + Math$min, + rebindAndMoveView, + updateAllViews +} from './utilities'; import { VirtualRepeat } from './virtual-repeat'; import { getDistanceToParent } from './utilities-dom'; @@ -74,6 +82,7 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy implements I } const local = repeat.local; const lastIndex = currItemCount - 1; + // console.log({firstIndex, lastIndex, currItemCount, realViewsCount}); if (firstIndex + realViewsCount > lastIndex) { // first = currItemCount - realViewsCount instead of: first = currItemCount - 1 - realViewsCount; // this is because during view update @@ -110,7 +119,7 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy implements I for (let i = realViewsCount; i < minLength; i++) { const overrideContext = createFullOverrideContext(repeat, items[i], i, currItemCount); repeat.addView(overrideContext.bindingContext, overrideContext); - } + } } /**@internal */ @@ -253,7 +262,7 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy implements I firstIndexAfterMutation = Math$max(0, firstIndexAfterMutation - removeDelta); } } - console.log({firstIndexAfterMutation}); + // console.log({firstIndexAfterMutation}); newViewCount = 0; // if array size is less than or equal to number of elements in View // the nadjust first index to 0 @@ -299,7 +308,7 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy implements I } } const newBotBufferItemCount = Math$max(0, newArraySize - newTopBufferItemCount - newViewCount); - console.log({ currViewCount, newViewCount, viewsRequiredCount, viewCountDelta, newBotBufferItemCount}) + // console.log({ currViewCount, newViewCount, viewsRequiredCount, viewCountDelta, newBotBufferItemCount}) // first update will be to mimic the behavior of a normal repeat mutation // where real views are inserted, removed @@ -308,20 +317,20 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy implements I repeat._isScrolling = false; repeat._scrollingDown = repeat._scrollingUp = false; repeat._first = firstIndexAfterMutation; - repeat._previousFirst = firstIndex + repeat._previousFirst = firstIndex; repeat._lastRebind = firstIndexAfterMutation + newViewCount; repeat._topBufferHeight = newTopBufferItemCount * itemHeight; repeat._bottomBufferHeight = newBotBufferItemCount * itemHeight; repeat._updateBufferElements(/*skip update?*/true); } // console.log({ firstIndexAfterMutation, newTopBufferItemCount, newBotBufferItemCount}); - console.log({ - first: repeat._first, - prevFirst: repeat._previousFirst, - last: repeat._lastRebind, - top: repeat._topBufferHeight, - bot: repeat._bottomBufferHeight - }); + // console.log({ + // first: repeat._first, + // prevFirst: repeat._previousFirst, + // last: repeat._lastRebind, + // top: repeat._topBufferHeight, + // bot: repeat._bottomBufferHeight + // }); const scrollerInfo = repeat.getScrollerInfo(); const topBufferDistance = getDistanceToParent(repeat.topBufferEl, scrollerInfo.scroller); @@ -331,12 +340,14 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy implements I ? 0 : (scrollerInfo.scrollTop - topBufferDistance) ); - console.log({ - top: scrollerInfo.scrollTop, - height: scrollerInfo.scrollHeight - }, repeat.topBufferEl.style.height, repeat.bottomBufferEl.style.height, repeat._bottomBufferHeight); - - let first_index_after_scroll_adjustment = realScrolltop === 0 ? 0 : Math$floor(realScrolltop / itemHeight); + // console.log({ + // top: scrollerInfo.scrollTop, + // height: scrollerInfo.scrollHeight + // }, repeat.topBufferEl.style.height, repeat.bottomBufferEl.style.height, repeat._bottomBufferHeight); + + let first_index_after_scroll_adjustment = realScrolltop === 0 + ? 0 + : Math$floor(realScrolltop / itemHeight); // if first index after scroll adjustment doesn't fit with number of possible view // it means the scroller has been too far down to the bottom and nolonger suitable to start from this index // rollback until all views fit into new collection, or until has enough collection item to render @@ -371,24 +382,23 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy implements I // botBufferHeight: bot_buffer_item_count_after_scroll_adjustment * itemHeight, // isAtBottom: bot_buffer_item_count_after_scroll_adjustment * itemHeight === 0 // }); - console.log({ - first: repeat._first, - prevFirst: repeat._previousFirst, - last: repeat._lastRebind, - top: repeat._topBufferHeight, - bot: repeat._bottomBufferHeight - }); + // console.log({ + // first: repeat._first, + // prevFirst: repeat._previousFirst, + // last: repeat._lastRebind, + // top: repeat._topBufferHeight, + // bot: repeat._bottomBufferHeight + // }); repeat._handlingMutations = false; - // prevent scroller update + // prevent scroller update repeat.revertScrollCheckGuard(); repeat._updateBufferElements(); - updateVirtualRepeatContexts(repeat, 0); + updateAllViews(repeat, 0); // requestAnimationFrame(() => repeat._handleScroll()); // repeat._handleScroll(); // console.log('-end: %crunSplices', 'color: orangered'); // repeat._onScroll(); - // for (i = 0; spliceCount > i; ++i) { // const splice = splices[i]; // const removedSize = splice.removed.length; @@ -511,6 +521,7 @@ export class ArrayVirtualRepeatStrategy extends ArrayRepeatStrategy implements I let addIndex = splice.index; let end = splice.index + splice.addedCount; for (; end > addIndex; ++addIndex) { + // tslint:disable-next-line const hasDistanceToBottomViewPort = getElementDistanceToBottomViewPort(repeat.templateStrategy.getLastElement(repeat.topBufferEl, repeat.bottomBufferEl)) > 0; if (repeat.viewCount() === 0 || (!this._isIndexBeforeViewSlot(repeat, viewSlot, addIndex) diff --git a/src/null-virtual-repeat-strategy.ts b/src/null-virtual-repeat-strategy.ts index 97a021b..fd0b7d6 100644 --- a/src/null-virtual-repeat-strategy.ts +++ b/src/null-virtual-repeat-strategy.ts @@ -2,9 +2,8 @@ import { NullRepeatStrategy, RepeatStrategy } from 'aurelia-templating-resources import { VirtualRepeat } from './virtual-repeat'; import { IVirtualRepeatStrategy } from './interfaces'; - export class NullVirtualRepeatStrategy extends NullRepeatStrategy implements IVirtualRepeatStrategy { - + createFirstItem() {/**/} instanceMutated() {/**/} diff --git a/src/utilities-dom.ts b/src/utilities-dom.ts index 2df890c..53c00be 100644 --- a/src/utilities-dom.ts +++ b/src/utilities-dom.ts @@ -14,7 +14,7 @@ export const getScrollContainer = (element: Node): HTMLElement => { current = current.parentNode as HTMLElement; } return document.documentElement; -} +}; /** * Determine real distance of an element to top of current document @@ -34,7 +34,7 @@ export const getElementDistanceToTopOfDocument = (element: Element): number => { export const hasOverflowScroll = (element: HTMLElement): boolean => { let style = element.style; return style.overflowY === 'scroll' || style.overflow === 'scroll' || style.overflowY === 'auto' || style.overflow === 'auto'; -} +}; /** * A naive utility to calculate distance of a child element to one of its ancestor, typically used for scroller/buffer combo @@ -65,4 +65,4 @@ export const getDistanceToParent = (child: HTMLElement, parent: HTMLElement) => return childOffsetTop + getDistanceToParent(offsetParent, parent); } } -} +}; diff --git a/src/utilities.ts b/src/utilities.ts index 8307220..c5bc242 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -1,13 +1,12 @@ -import { updateOverrideContext, createFullOverrideContext } from 'aurelia-templating-resources'; +import { updateOverrideContext } from 'aurelia-templating-resources'; import { View } from 'aurelia-templating'; import { VirtualRepeat } from './virtual-repeat'; -import { OverrideContext } from 'aurelia-binding'; import { IView } from './interfaces'; /** * Get total value of a list of css style property on an element */ -export function getStyleValues(element: Element, ...styles: string[]): number { +export const getStyleValues = (element: Element, ...styles: string[]): number => { let currentStyle = window.getComputedStyle(element); let value: number = 0; let styleValue: number = 0; @@ -16,60 +15,62 @@ export function getStyleValues(element: Element, ...styles: string[]): number { value += $isNaN(styleValue) ? 0 : styleValue; } return value; -} +}; -export function calcOuterHeight(element: Element): number { +export const calcOuterHeight = (element: Element): number => { let height = element.getBoundingClientRect().height; height += getStyleValues(element, 'marginTop', 'marginBottom'); return height; -} +}; -export function insertBeforeNode(view: View, bottomBuffer: Element): void { - let parentElement = bottomBuffer.parentElement || bottomBuffer.parentNode; - parentElement.insertBefore(view.lastChild, bottomBuffer); -} +export const insertBeforeNode = (view: View, bottomBuffer: Element): void => { + // todo: account for anchor comment + bottomBuffer.parentNode.insertBefore(view.lastChild, bottomBuffer); +}; /** * Update the override context. * @param startIndex index in collection where to start updating. */ -export function updateVirtualOverrideContexts(repeat: VirtualRepeat, startIndex: number): void { - let views = repeat.viewSlot.children; - let viewLength = views.length; - let collectionLength = repeat.items.length; +export const updateVirtualOverrideContexts = (repeat: VirtualRepeat, startIndex: number): void => { + const views = repeat.viewSlot.children; + const viewLength = views.length; + const collectionLength = repeat.items.length; if (startIndex > 0) { startIndex = startIndex - 1; } - let delta = repeat._topBufferHeight / repeat.itemHeight; + const delta = repeat._topBufferHeight / repeat.itemHeight; - console.log('updating virtual repeat views', startIndex, viewLength); - for (; startIndex < viewLength; ++startIndex) { + for (; viewLength > startIndex; ++startIndex) { updateOverrideContext(views[startIndex].overrideContext, startIndex + delta, collectionLength); } -} +}; -export const updateVirtualRepeatContexts = (repeat: VirtualRepeat, startIndex: number): void => { +export const updateAllViews = (repeat: VirtualRepeat, startIndex: number): void => { const views = repeat.viewSlot.children; const viewLength = views.length; const collection = repeat.items; - const delta = repeat._topBufferHeight / repeat.itemHeight; + const delta = Math$floor(repeat._topBufferHeight / repeat.itemHeight); let collectionIndex = 0; + let view; for (; viewLength > startIndex; ++startIndex) { collectionIndex = startIndex + delta; - rebindView(repeat, repeat.view(startIndex), collectionIndex, collection); + view = repeat.view(startIndex); + rebindView(repeat, view, collectionIndex, collection); + repeat.updateBindings(view); } -} +}; export const rebindView = (repeat: VirtualRepeat, view: IView, collectionIndex: number, collection: any[]): void => { view.bindingContext[repeat.local] = collection[collectionIndex]; updateOverrideContext(view.overrideContext, collectionIndex, collection.length); -} +}; -export function rebindAndMoveView(repeat: VirtualRepeat, view: View, index: number, moveToBottom: boolean): void { +export const rebindAndMoveView = (repeat: VirtualRepeat, view: View, index: number, moveToBottom: boolean): void => { const items = repeat.items; const viewSlot = repeat.viewSlot; @@ -82,16 +83,12 @@ export function rebindAndMoveView(repeat: VirtualRepeat, view: View, index: numb viewSlot.children.unshift(viewSlot.children.splice(-1, 1)[0]); repeat.templateStrategy.moveViewFirst(view, repeat.topBufferEl); } -} +}; -export function getElementDistanceToBottomViewPort(element: Element): number { +export const getElementDistanceToBottomViewPort = (element: Element): number => { return document.documentElement.clientHeight - element.getBoundingClientRect().bottom; -} - -export function getElementDistanceToTopViewPort(element: Element): number { - return element.getBoundingClientRect().top; -} +}; export const Math$abs = Math.abs; export const Math$max = Math.max; diff --git a/src/virtual-repeat.ts b/src/virtual-repeat.ts index 6364bf0..5b8e850 100644 --- a/src/virtual-repeat.ts +++ b/src/virtual-repeat.ts @@ -75,20 +75,24 @@ export class VirtualRepeat extends AbstractRepeater { } /** - * @internal * First view index, for proper follow up calculations + * @internal */ _first: number = 0; /** - * @internal * Preview first view index, for proper determination of delta + * @internal */ _previousFirst = 0; - /**@internal*/ + /** + * Number of views required to fillup the viewport, and also enough to provide smooth scrolling + * Without showing blank space + * @internal + */ _viewsLength = 0; - + /** * @internal * Last rebound view index, user to determine first index of next task when scrolling/ changing viewport scroll position @@ -96,15 +100,15 @@ export class VirtualRepeat extends AbstractRepeater { _lastRebind = 0; /** - * @internal * Height of top buffer to properly push the visible rendered list items into right position * Usually determined by `_first` visible index * `itemHeight` + * @internal */ _topBufferHeight = 0; /** - * @internal * Height of bottom buffer to properly push the visible rendered list items into right position + * @internal */ _bottomBufferHeight = 0; @@ -114,9 +118,10 @@ export class VirtualRepeat extends AbstractRepeater { /**@internal*/ _isAttached = false; /**@internal*/ _ticking = false; /** - * @internal Indicates whether virtual repeat attribute is inside a fixed height container with overflow + * Indicates whether virtual repeat attribute is inside a fixed height container with overflow * * This helps identifies place to add scroll event listener + * @internal */ _fixedHeightContainer = false; @@ -311,12 +316,13 @@ export class VirtualRepeat extends AbstractRepeater { this.templateStrategyLocator = templateStrategyLocator; this.sourceExpression = getItemsSourceExpression(this.instruction, 'virtual-repeat.for'); this.isOneTime = isOneTime(this.sourceExpression); - this.itemHeight = 0; - this._prevItemsCount = 0; + this.itemHeight + = this._prevItemsCount + = this.distanceToTop + = 0; this.revertScrollCheckGuard = () => { this._ticking = false; - this._skipNextScrollHandle = false; - } + }; } /**@override */ @@ -336,7 +342,7 @@ export class VirtualRepeat extends AbstractRepeater { }; const scrollContainer = this.scrollContainer = templateStrategy.getScrollContainer(element); const [topBufferEl, bottomBufferEl] = templateStrategy.createBuffers(element); - + this.topBufferEl = topBufferEl; this.bottomBufferEl = bottomBufferEl; this.itemsChanged(); @@ -557,20 +563,22 @@ export class VirtualRepeat extends AbstractRepeater { = this._lastRebind = this._topBufferHeight = this._bottomBufferHeight + = this._prevItemsCount = this.elementsInView = 0; - this._scrollingDown - = this._scrollingUp + this._isScrolling + = this._scrollingDown + = this._scrollingUp = this._switchedDirection = this._ticking = this._hasCalculatedSizes = this.isLastIndex = false; this._isAtTop = true; - this._updateBufferElements(); + this._updateBufferElements(true); } /**@internal*/ _onScroll(): void { - // console.log('+begin: %conScroll()', 'color: darkgreen'); + // console.log('+begin: %conScroll()', 'color: darkgreen', this._ticking, this._handlingMutations); if (!this._ticking && !this._handlingMutations) { // console.log('+ raf(scrollHandle)'); requestAnimationFrame(() => { @@ -615,7 +623,7 @@ export class VirtualRepeat extends AbstractRepeater { /** * Real scroll top calculated based on current scroll top of scroller and top buffer {height + distance to top} * as there could be elements before top buffer and it affects real scroll top of the selected repeat - * + * * Calculation are done differently based on the scroller: * - not document: the scroll top of this repeat is calculated based on current scroller.scrollTop and the distance to top of `top buffer` * - document: the scroll top is the substraction of `pageYOffset` and distance to top of current buffer element (logic needs revised) @@ -623,13 +631,19 @@ export class VirtualRepeat extends AbstractRepeater { const scrollTop = isFixedHeightContainer ? scroller.scrollTop : (pageYOffset - this.distanceToTop); - const realScrollTop = Math$max(0, isFixedHeightContainer ? scrollTop - Math$abs(topBufferDistance): scrollTop) + const realScrollTop = Math$max( + 0, + isFixedHeightContainer + ? scrollTop - Math$abs(topBufferDistance) + : scrollTop + ); // console.log({realScrollTop}); const elementsInView = this.elementsInView; // Calculate the index of first view // Using Math floor to ensure it has correct space for both small and large calculation let firstIndex = Math$max(0, itemHeight > 0 ? Math$floor(realScrollTop / itemHeight) : 0); + const currLastReboundIndex = this._lastRebind; // if first index starts somewhere after the last view // then readjust based on the delta if (firstIndex > items.length - elementsInView) { @@ -647,11 +661,11 @@ export class VirtualRepeat extends AbstractRepeater { // console.log('down?', this._scrollingDown, 'up?', this._scrollingUp); // TODO if and else paths do almost same thing, refactor? if (this._scrollingDown) { - let viewsToMoveCount = firstIndex - this._lastRebind; + let viewsToMoveCount = firstIndex - currLastReboundIndex; if (this._switchedDirection) { viewsToMoveCount = this._isAtTop ? firstIndex - : (firstIndex - this._lastRebind); + : (firstIndex - currLastReboundIndex); } this._isAtTop = false; this._lastRebind = firstIndex; @@ -672,14 +686,14 @@ export class VirtualRepeat extends AbstractRepeater { this._updateBufferElements(true); } } else if (this._scrollingUp) { - let viewsToMoveCount = this._lastRebind - firstIndex; + let viewsToMoveCount = currLastReboundIndex - firstIndex; // Use for catching initial scroll state where a small page size might cause _getMore not to fire. const initialScrollState = this.isLastIndex === undefined; if (this._switchedDirection) { if (this.isLastIndex) { viewsToMoveCount = this.items.length - firstIndex - elementsInView; } else { - viewsToMoveCount = this._lastRebind - firstIndex; + viewsToMoveCount = currLastReboundIndex - firstIndex; } } this.isLastIndex = false; @@ -768,8 +782,10 @@ export class VirtualRepeat extends AbstractRepeater { return null; }; + // use micro task so it will be executed before next frame + // can help avoid doing double work for handling scroll + // in case of mutation this.taskQueue.queueMicroTask(executeGetMore); - // requestAnimationFrame(executeGetMore); } } } @@ -778,41 +794,46 @@ export class VirtualRepeat extends AbstractRepeater { * On scroll event: * - Set flags based on internal values of first view index, previous view index * - Determines scrolling state, scroll direction, switching scroll direction - * - * * @internal */ _checkScrolling(): void { const { _first, _scrollingUp, _scrollingDown, _previousFirst } = this; - if (_first !== _previousFirst) { - if (_first > _previousFirst - // && (this._bottomBufferHeight > 0 || !this.isLastIndex) - ) { - if (!_scrollingDown) { - this._scrollingDown = true; - this._scrollingUp = false; - this._switchedDirection = true; - } else { - this._switchedDirection = false; - } - this._isScrolling = true; - return; - } - if (_first < _previousFirst - // && (this._topBufferHeight >= 0 || !this._isAtTop) - ) { - if (!_scrollingUp) { - this._scrollingDown = false; - this._scrollingUp = true; - this._switchedDirection = true; - } else { - this._switchedDirection = false; - } - this._isScrolling = true; - return; + let isScrolling = false; + let isScrollingDown = false; + let isScrollingUp = false; + let isSwitchedDirection = false; + + if (_first > _previousFirst + // && (this._bottomBufferHeight > 0 || !this.isLastIndex) + ) { + if (!_scrollingDown) { + isScrollingDown = true; + isScrollingUp = false; + isSwitchedDirection = true; + } else { + isSwitchedDirection = false; } + isScrolling = true; } + // todo: remove if + // keep for checking old behavior + else if (_first < _previousFirst + // && (this._topBufferHeight >= 0 || !this._isAtTop) + ) { + if (!_scrollingUp) { + isScrollingDown = false; + isScrollingUp = true; + isSwitchedDirection = true; + } else { + isSwitchedDirection = false; + } + isScrolling = true; + } + this._isScrolling = isScrolling; + this._scrollingDown = isScrollingDown; + this._scrollingUp = isScrollingUp; + this._switchedDirection = isSwitchedDirection; // else { // console.log('%cChecking scrolling', 'color: green; padding: 2px; background: yellow; font-weight: bold;'); // const currScrollerInfo = this.getScrollerInfo(); @@ -835,7 +856,6 @@ export class VirtualRepeat extends AbstractRepeater { // this._prevScrollerInfo = currScrollerInfo; // return; // } - this._isScrolling = false; } /**@internal */ @@ -987,7 +1007,7 @@ export class VirtualRepeat extends AbstractRepeater { // But what about when we've only scrolled slightly down the list? We need to readjust the top buffer height then this._bottomBufferHeight = Math$max(0, newBottomBufferHeight - adjustedTopBufferHeight); } - this._updateBufferElements(true); + this._updateBufferElements(); } /**@internal*/ @@ -1081,7 +1101,7 @@ export class VirtualRepeat extends AbstractRepeater { return this.viewSlot.removeAt(index, returnToCache, skipAnimation) as IView | Promise; } - updateBindings(view: View) { + updateBindings(view: IView) { let j = view.bindings.length; while (j--) { updateOneTimeBinding(view.bindings[j]); diff --git a/test/interfaces.ts b/test/interfaces.ts index 52d2e89..9671fd1 100644 --- a/test/interfaces.ts +++ b/test/interfaces.ts @@ -1,3 +1,6 @@ +import { IScrollNextScrollContext } from '../src/interfaces'; + export declare class ITestAppInterface { items: T[]; + getNextPage?: (scrollContext: IScrollNextScrollContext) => void; } diff --git a/test/utilities.ts b/test/utilities.ts index 8d01c73..b4d0027 100644 --- a/test/utilities.ts +++ b/test/utilities.ts @@ -45,7 +45,7 @@ export function validateState(virtualRepeat: VirtualRepeat, viewModel: any, item // validate contextual data for (let i = 0; i < views.length; i++) { - expect(views[i].bindingContext.item).toBe(viewModel.items[i]); + expect(views[i].bindingContext.item).toBe(viewModel.items[i], `view[${i}].bindingContext.item === items[${i}]`); let overrideContext = views[i].overrideContext; expect(overrideContext.parentOverrideContext.bindingContext).toBe(viewModel); expect(overrideContext.bindingContext).toBe(views[i].bindingContext); @@ -79,10 +79,10 @@ export function validateScrolledState(virtualRepeat: VirtualRepeat, viewModel: a // validate contextual data let startingLoc = viewModel.items.indexOf(views[0].bindingContext.item); for (let i = startingLoc; i < views.length; i++) { - expect(views[i].bindingContext.item).toBe(viewModel.items[i]); + expect(views[i].bindingContext.item).toBe(viewModel.items[i], `view(${i}).bindingContext.item`); let overrideContext = views[i].overrideContext; - expect(overrideContext.parentOverrideContext.bindingContext).toBe(viewModel); - expect(overrideContext.bindingContext).toBe(views[i].bindingContext); + expect(overrideContext.parentOverrideContext.bindingContext).toBe(viewModel, 'parentOverrideContext.bindingContext === viewModel'); + expect(overrideContext.bindingContext).toBe(views[i].bindingContext, `overrideContext sync`); let first = i === 0; let last = i === viewModel.items.length - 1; let even = i % 2 === 0; @@ -129,12 +129,12 @@ export async function scrollToIndex(virtualRepeat: VirtualRepeat, itemIndex: num /** * Wait for a small time for repeat to finish processing. - * + * * Default to 10 */ export async function ensureScrolled(time: number = 10): Promise { - await waitForTimeout(time); await waitForNextFrame(); + await waitForTimeout(time); } @@ -154,7 +154,9 @@ const kebabCaseLookup: Record = {}; const kebabCase = (input: string): string => { // benchmark: http://jsben.ch/v7K9T let value = kebabCaseLookup[input]; - if (value !== undefined) return value; + if (value !== undefined) { + return value; + } value = ''; let first = true; let char: string, lower: string; @@ -165,7 +167,7 @@ const kebabCase = (input: string): string => { first = false; } return kebabCaseLookup[input] = value; -} +}; const eventCmds = { delegate: 1, capture: 1, call: 1 }; @@ -256,7 +258,7 @@ export const h = (name: string, attrs: Record | null, ...childre } appender.shadowRoot.appendChild(child_child); } else { - appender.appendChild(child_child) + appender.appendChild(child_child); } } else { appender.appendChild(document.createTextNode('' + child_child)); @@ -272,12 +274,12 @@ export const h = (name: string, attrs: Record | null, ...childre } else { appender.appendChild(child); } - }else { + } else { appender.appendChild(document.createTextNode('' + child)); } } } return el; -} +}; const isFragment = (node: Node): node is DocumentFragment => node.nodeType === Node.DOCUMENT_FRAGMENT_NODE; diff --git a/test/virtual-repeat-integration.spec.ts b/test/virtual-repeat-integration.spec.ts index 10a982c..ed227a5 100644 --- a/test/virtual-repeat-integration.spec.ts +++ b/test/virtual-repeat-integration.spec.ts @@ -1,7 +1,7 @@ import './setup'; import { StageComponent } from './component-tester'; import { PLATFORM } from 'aurelia-pal'; -import { createAssertionQueue, validateState, validateScrolledState, AsyncQueue, waitForTimeout } from './utilities'; +import { createAssertionQueue, validateState, validateScrolledState, AsyncQueue, waitForTimeout, ensureScrolled } from './utilities'; import { VirtualRepeat } from '../src/virtual-repeat'; PLATFORM.moduleName('src/virtual-repeat'); @@ -540,16 +540,15 @@ describe('VirtualRepeat Integration', () => { 'scrollContainerPromise' ); }); - it('handles getting next data set with small page size', done => { + it('handles getting next data set with small page size', async done => { vm.items = []; - for (let i = 0; i < 7; ++i) { + for (let i = 0; i < 3; ++i) { vm.items.push('item' + i); } - create.then(() => { - validateScroll(virtualRepeat, viewModel, () => { - expect(vm.getNextPage).toHaveBeenCalled(); - done(); - }); + await create; + validateScroll(virtualRepeat, viewModel, () => { + expect(vm.getNextPage).toHaveBeenCalled(); + done(); }); }); // The following test used to pass because there was no getMore() invoked during initialization @@ -596,147 +595,5 @@ describe('VirtualRepeat Integration', () => { }, 'scrollContainerNested'); }); }); - - xdescribe('scrolling div', () => { - beforeEach(() => { - items = []; - for (let i = 0; i < 1000; ++i) { - items.push('item' + i); - } - - component = StageComponent - .withResources(['src/virtual-repeat']) - .inView(`
-
\${item}
-
`) - .boundTo({ items: items }); - - create = component.create().then(() => { - virtualRepeat = component.sut; - viewModel = component.viewModel; - }); - }); - - afterEach(() => { - component.cleanUp(); - }); - - it('handles splice when scrolled to end', done => { - create.then(() => { - validateScroll(virtualRepeat, viewModel, () => { - viewModel.items.splice(995, 1, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'); - nq(() => validateScrolledState(virtualRepeat, viewModel, itemHeight)); - nq(() => validateScroll(virtualRepeat, viewModel, () => { - let views = virtualRepeat.viewSlot.children; - setTimeout(() => { - expect(views[views.length - 1].bindingContext.item).toBe(viewModel.items[viewModel.items.length - 1]); - done(); - }, 500); - })); - }); - }); - }); - - it('handles splice removing non-consecutive when scrolled to end', done => { - create.then(() => { - validateScroll(virtualRepeat, viewModel, () => { - for (let i = 0, ii = 100; i < ii; i++) { - viewModel.items.splice(i + 1, 9); - } - nq(() => validateScrolledState(virtualRepeat, viewModel, itemHeight)); - nq(() => validateScroll(virtualRepeat, viewModel, () => { - let views = virtualRepeat.viewSlot.children; - setTimeout(() => { - expect(views[views.length - 1].bindingContext.item).toBe(viewModel.items[viewModel.items.length - 1]); - done(); - }, 500); - })); - }); - }); - }); - - it('handles splice non-consecutive when scrolled to end', done => { - create.then(() => { - validateScroll(virtualRepeat, viewModel, () => { - for (let i = 0, ii = 80; i < ii; i++) { - viewModel.items.splice(10 * i, 3, i); - } - nq(() => validateScrolledState(virtualRepeat, viewModel, itemHeight)); - nq(() => validateScroll(virtualRepeat, viewModel, () => { - let views = virtualRepeat.viewSlot.children; - setTimeout(() => { - expect(views[views.length - 1].bindingContext.item).toBe(viewModel.items[viewModel.items.length - 1]); - done(); - }, 500); - })); - }); - }); - }); - - it('handles splice removing many', done => { - create.then(() => { - // more items remaining than viewslot capacity - viewModel.items.splice(5, 1000 - virtualRepeat._viewsLength - 10); - nq(() => validateScrolledState(virtualRepeat, viewModel, itemHeight)); - nq(() => done()); - }); - }); - - it('handles splice removing more', done => { - // number of items remaining exactly as viewslot capacity - create.then(() => { - viewModel.items.splice(5, 1000 - virtualRepeat._viewsLength); - nq(() => expect(virtualRepeat.viewSlot.children.length).toBe(viewModel.items.length)); - nq(() => validateScrolledState(virtualRepeat, viewModel, itemHeight)); - nq(() => done()); - }); - }); - - it('handles splice removing even more', done => { - // less items remaining than viewslot capacity - create.then(() => { - viewModel.items.splice(5, 1000 - virtualRepeat._viewsLength + 10); - nq(() => expect(virtualRepeat.viewSlot.children.length).toBe(viewModel.items.length)); - nq(() => validateScrolledState(virtualRepeat, viewModel, itemHeight)); - nq(() => done()); - }); - }); - - it('handles splice removing non-consecutive', done => { - create.then(() => { - for (let i = 0, ii = 100; i < ii; i++) { - viewModel.items.splice(i + 1, 9); - } - nq(() => validateScrolledState(virtualRepeat, viewModel, itemHeight)); - nq(() => done()); - }); - }); - - it('handles splice non-consecutive', done => { - create.then(() => { - for (let i = 0, ii = 100; i < ii; i++) { - viewModel.items.splice(3 * (i + 1), 3, i); - } - nq(() => validateScrolledState(virtualRepeat, viewModel, itemHeight)); - nq(() => done()); - }); - }); - - it('handles splice removing many + add', done => { - create.then(() => { - viewModel.items.splice(5, 990, 'a', 'b', 'c'); - nq(() => validateScrolledState(virtualRepeat, viewModel, itemHeight)); - nq(() => done()); - }); - }); - - it('handles splice remove remaining + add', done => { - create.then(() => { - viewModel.items.splice(5, 995, 'a', 'b', 'c'); - nq(() => validateScrolledState(virtualRepeat, viewModel, itemHeight)); - nq(() => done()); - }); - }); - }); }); }); diff --git a/test/vr-integration.instance-changed.spec.ts b/test/vr-integration.instance-changed.spec.ts index 60b081e..df4c908 100644 --- a/test/vr-integration.instance-changed.spec.ts +++ b/test/vr-integration.instance-changed.spec.ts @@ -4,7 +4,7 @@ import { ComponentTester, StageComponent } from 'aurelia-testing'; import { VirtualRepeat } from '../src/virtual-repeat'; import { ITestAppInterface } from './interfaces'; import './setup'; -import { AsyncQueue, createAssertionQueue, ensureScrolled, validateState, createScrollEvent } from './utilities'; +import { AsyncQueue, createAssertionQueue, ensureScrolled } from './utilities'; PLATFORM.moduleName('src/virtual-repeat'); PLATFORM.moduleName('test/noop-value-converter'); @@ -65,15 +65,17 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat.elementsInView).toBe(Math.ceil(500 / 50) + 1, 'repeat.elementsInView'); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); expect(table.tBodies[0].rows.length).toBe(2 + virtualRepeat._viewsLength); // 2 buffers + 20 rows based on 50 height - + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength)); // start more difficult cases - + const scrollSpy = spyOn(virtualRepeat, '_handleScroll').and.callThrough(); // 1. mutate scroll state table.parentElement.scrollTop = table.parentElement.scrollHeight; - await ensureScrolled(50); + await ensureScrolled(0); + + expect(scrollSpy).toHaveBeenCalledTimes(1); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); // when scrolling, the first bound row is calculated differently compared to other scenarios // as it can be known exactly what the last process was @@ -82,7 +84,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat._bottomBufferHeight).toBe(0); viewModel.items = viewModel.items.slice(0).reverse(); - await ensureScrolled(); + await ensureScrolled(0); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); expect(table.tBodies[0].rows.length).toBe(2 + virtualRepeat._viewsLength, 'table > tr count'); // 2 buffers + 20 rows based on 50 height @@ -94,7 +96,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { const view = virtualRepeat.view(i); const currIndex = i + virtualRepeat._first; expect(view).not.toBeNull(`view-${i} !== null`); - expect(view.bindingContext.item).toBe(`item${viewModel.items.length - currIndex - 1}`); + expect(view.bindingContext.item).toBe(`item${viewModel.items.length - currIndex - 1}`, `view[${i}].bindingContext.item`); expect((view.firstChild as Element).firstElementChild.textContent).toBe(`item${viewModel.items.length - currIndex - 1}`); } expect(virtualRepeat._bottomBufferHeight).toBe(0); @@ -109,7 +111,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { it([ 'renders with 100 items', ' -- reduces to 30', - ' -- greater than (repeat._viewsLength)', + ' -- greater than (repeat._viewsLength)' ].join('\n\t'), async () => { const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }); @@ -117,7 +119,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat.elementsInView).toBe(Math.ceil(500 / 50) + 1, 'repeat.elementsInView'); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); expect(table.tBodies[0].rows.length).toBe(2 + virtualRepeat._viewsLength); // 2 buffers + 20 rows based on 50 height - + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength)); @@ -176,7 +178,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat.elementsInView).toBe(Math.ceil(500 / 50) + 1, 'repeat.elementsInView'); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); expect(table.tBodies[0].rows.length).toBe(2 + virtualRepeat._viewsLength); // 2 buffers + 20 rows based on 50 height - + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength)); @@ -226,7 +228,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { it([ 'renders with 100 items', ' -- reduces to 8', - ' -- lesser than (repeat.elementsInView)', + ' -- lesser than (repeat.elementsInView)' ].join('\n\t'), async () => { const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }); @@ -234,7 +236,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat.elementsInView).toBe(Math.ceil(500 / 50) + 1, 'repeat.elementsInView'); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); expect(table.tBodies[0].rows.length).toBe(2 + virtualRepeat._viewsLength); // 2 buffers + 20 rows based on 50 height - + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength)); @@ -301,7 +303,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); // buffers are TR element expect(table.tBodies.length).toBe(/*no buffer 2 +*/virtualRepeat._viewsLength, 'table.tBodies.length 1'); // 2 buffers + 20 rows based on 50 height - + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength)); @@ -340,7 +342,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { it([ 'renders with 100 items', ' -- reduces to 30', - ' -- greater than (repeat._viewsLength)', + ' -- greater than (repeat._viewsLength)' ].join('\n\t'), async () => { const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }); @@ -349,7 +351,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); // buffers are TR elements expect(table.tBodies.length).toBe(/*no buffer 2 +*/virtualRepeat._viewsLength, 'table.tBodies.length 1'); // 2 buffers + 20 rows based on 50 height - + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength), 'repeat._bottomBufferHeight'); @@ -404,7 +406,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); // buffers are TR elements expect(table.tBodies.length).toBe(/*no buffer 2 +*/virtualRepeat._viewsLength, 'table.tBodies.length'); // 2 buffers + 20 rows based on 50 height - + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength)); @@ -449,7 +451,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { it([ 'renders with 100 items', ' -- reduces to 8', - ' -- lesser than (repeat.elementsInView)', + ' -- lesser than (repeat.elementsInView)' ].join('\n\t'), async () => { const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }); @@ -458,7 +460,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); // buffers are tr elements expect(table.tBodies.length).toBe(/*no buffer 2 +*/virtualRepeat._viewsLength, 'table.tBodies.length 1'); // 2 buffers + 20 rows based on 50 height - + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength)); @@ -519,7 +521,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat.elementsInView).toBe(Math.ceil(500 / 50) + 1, 'repeat.elementsInView'); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); expect(scrollerEl.children.length).toBe(2 + virtualRepeat._viewsLength, 'scrollerEl.children.length 1'); // 2 buffers + 20 rows based on 50 height - + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe( 50 * (virtualRepeat.items.length - virtualRepeat._viewsLength), @@ -560,7 +562,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { it([ 'renders with 100 items', ' -- reduces to 30', - ' -- greater than (repeat._viewsLength)', + ' -- greater than (repeat._viewsLength)' ].join('\n\t'), async () => { const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }); @@ -568,7 +570,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat.elementsInView).toBe(Math.ceil(500 / 50) + 1, 'repeat.elementsInView'); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); expect(scrollerEl.children.length).toBe(2 + virtualRepeat._viewsLength, 'scrollerEl.children.length 1'); // 2 buffers + 20 rows based on 50 height - + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength), 'repeat._bottomBufferHeight'); @@ -621,7 +623,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat.elementsInView).toBe(Math.ceil(500 / 50) + 1, 'repeat.elementsInView'); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); expect(scrollerEl.children.length).toBe(2 + virtualRepeat._viewsLength, 'scrollerEl.children.length'); // 2 buffers + 20 rows based on 50 height - + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength)); @@ -665,7 +667,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { it([ 'renders with 100 items', ' -- reduces to 8', - ' -- lesser than (repeat.elementsInView)', + ' -- lesser than (repeat.elementsInView)' ].join('\n\t'), async () => { const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }); @@ -673,7 +675,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat.elementsInView).toBe(Math.ceil(500 / 50) + 1, 'repeat.elementsInView'); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); expect(scrollerEl.children.length).toBe(2 + virtualRepeat._viewsLength, 'scrollerEl.children.length 1'); // 2 buffers + 20 rows based on 50 height - + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength)); @@ -746,8 +748,10 @@ describe('VirtualRepeat Integration - Instance Changed', () => { const scrollerEl = (component['host'] as HTMLElement).querySelector('.scroller'); expect(virtualRepeat.elementsInView).toBe(Math.ceil(500 / 50) + 1, 'repeat.elementsInView'); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); - expect(scrollerEl.firstElementChild.children.length).toBe(2 + virtualRepeat._viewsLength, 'scrollerEl.children.length 1'); // 2 buffers + 20 rows based on 50 height - + expect(scrollerEl.firstElementChild.children.length).toBe( + 2 + virtualRepeat._viewsLength, + 'scrollerEl.children.length 1'); // 2 buffers + 20 rows based on 50 height + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe( 50 * (virtualRepeat.items.length - virtualRepeat._viewsLength), @@ -770,7 +774,9 @@ describe('VirtualRepeat Integration - Instance Changed', () => { await ensureScrolled(); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); - expect(scrollerEl.firstElementChild.children.length).toBe(2 + virtualRepeat._viewsLength, 'scrollerEl.children.length 2'); // 2 buffers + 20 rows based on 50 height + expect(scrollerEl.firstElementChild.children.length).toBe( + 2 + virtualRepeat._viewsLength, + 'scrollerEl.children.length 2'); // 2 buffers + 20 rows based on 50 height // This check is different from the above: // after instance changed, it restart the "_first" view based on safe number of views expect(virtualRepeat._first).toBe(/*items count*/100 - /*views count*/virtualRepeat._viewsLength, 'repeat._first 2'); @@ -788,15 +794,17 @@ describe('VirtualRepeat Integration - Instance Changed', () => { it([ 'renders with 100 items', ' -- reduces to 30', - ' -- greater than (repeat._viewsLength)', + ' -- greater than (repeat._viewsLength)' ].join('\n\t'), async () => { const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }, $view); const scrollerEl = (component['host'] as HTMLElement).querySelector('.scroller'); expect(virtualRepeat.elementsInView).toBe(Math.ceil(500 / 50) + 1, 'repeat.elementsInView'); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); - expect(scrollerEl.firstElementChild.children.length).toBe(2 + virtualRepeat._viewsLength, 'scrollerEl.children.length 1'); // 2 buffers + 20 rows based on 50 height - + expect(scrollerEl.firstElementChild.children.length).toBe( + 2 + virtualRepeat._viewsLength, + 'scrollerEl.children.length 1'); // 2 buffers + 20 rows based on 50 height + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength), 'repeat._bottomBufferHeight'); @@ -816,7 +824,9 @@ describe('VirtualRepeat Integration - Instance Changed', () => { await ensureScrolled(50); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); - expect(scrollerEl.firstElementChild.children.length).toBe(2 + virtualRepeat._viewsLength, 'scrollerEl.children.length 2'); // 2 buffers + 20 rows based on 50 height + expect(scrollerEl.firstElementChild.children.length).toBe( + 2 + virtualRepeat._viewsLength, + 'scrollerEl.children.length 2'); // 2 buffers + 20 rows based on 50 height // This check is different from the above: // after instance changed, it restart the "_first" view based on safe number of views @@ -848,8 +858,10 @@ describe('VirtualRepeat Integration - Instance Changed', () => { const scrollerEl = (component['host'] as HTMLElement).querySelector('.scroller'); expect(virtualRepeat.elementsInView).toBe(Math.ceil(500 / 50) + 1, 'repeat.elementsInView'); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); - expect(scrollerEl.firstElementChild.children.length).toBe(2 + virtualRepeat._viewsLength, 'scrollerEl.children.length'); // 2 buffers + 20 rows based on 50 height - + expect(scrollerEl.firstElementChild.children.length).toBe( + 2 + virtualRepeat._viewsLength, + 'scrollerEl.children.length'); // 2 buffers + 20 rows based on 50 height + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength)); @@ -893,15 +905,18 @@ describe('VirtualRepeat Integration - Instance Changed', () => { it([ 'renders with 100 items', ' -- reduces to 8', - ' -- lesser than (repeat.elementsInView)', + ' -- lesser than (repeat.elementsInView)' ].join('\n\t'), async () => { const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }, $view); const scrollerEl = (component['host'] as HTMLElement).querySelector('.scroller'); expect(virtualRepeat.elementsInView).toBe(Math.ceil(500 / 50) + 1, 'repeat.elementsInView'); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); - expect(scrollerEl.firstElementChild.children.length).toBe(2 + virtualRepeat._viewsLength, 'scrollerEl.children.length 1'); // 2 buffers + 20 rows based on 50 height - + expect(scrollerEl.firstElementChild.children.length).toBe( + 2 + virtualRepeat._viewsLength, + 'scrollerEl.children.length 1' + ); // 2 buffers + 20 rows based on 50 height + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength)); @@ -972,7 +987,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat.elementsInView).toBe(Math.ceil(500 / 50) + 1, 'repeat.elementsInView'); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); expect(scrollerEl.children.length).toBe(2 + virtualRepeat._viewsLength, 'scrollerEl.children.length 1'); // 2 buffers + 20 rows based on 50 height - + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe( 50 * (virtualRepeat.items.length - virtualRepeat._viewsLength), @@ -1013,7 +1028,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { it([ 'renders with 100 items', ' -- reduces to 30', - ' -- greater than (repeat._viewsLength)', + ' -- greater than (repeat._viewsLength)' ].join('\n\t'), async () => { const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }, $view); @@ -1021,7 +1036,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat.elementsInView).toBe(Math.ceil(500 / 50) + 1, 'repeat.elementsInView'); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); expect(scrollerEl.children.length).toBe(2 + virtualRepeat._viewsLength, 'scrollerEl.children.length 1'); // 2 buffers + 20 rows based on 50 height - + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength), 'repeat._bottomBufferHeight'); @@ -1074,7 +1089,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat.elementsInView).toBe(Math.ceil(500 / 50) + 1, 'repeat.elementsInView'); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); expect(scrollerEl.children.length).toBe(2 + virtualRepeat._viewsLength, 'scrollerEl.children.length'); // 2 buffers + 20 rows based on 50 height - + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength)); @@ -1118,7 +1133,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { it([ 'renders with 100 items', ' -- reduces to 8', - ' -- lesser than (repeat.elementsInView)', + ' -- lesser than (repeat.elementsInView)' ].join('\n\t'), async () => { const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }, $view); @@ -1126,7 +1141,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat.elementsInView).toBe(Math.ceil(500 / 50) + 1, 'repeat.elementsInView'); expect(virtualRepeat._viewsLength).toBe(22, 'repeat._viewsLength'); expect(scrollerEl.children.length).toBe(2 + virtualRepeat._viewsLength, 'scrollerEl.children.length 1'); // 2 buffers + 20 rows based on 50 height - + expect(virtualRepeat._first).toBe(0); expect(virtualRepeat._bottomBufferHeight).toBe(50 * (virtualRepeat.items.length - virtualRepeat._viewsLength)); @@ -1170,7 +1185,7 @@ describe('VirtualRepeat Integration - Instance Changed', () => { expect(virtualRepeat.bottomBufferEl.getBoundingClientRect().height).toBe(0); }); } - }); + }); async function bootstrapComponent($viewModel?: ITestAppInterface, $view?: string) { component = StageComponent diff --git a/test/vr-integration.instance-mutated.spec.ts b/test/vr-integration.instance-mutated.spec.ts new file mode 100644 index 0000000..1a63985 --- /dev/null +++ b/test/vr-integration.instance-mutated.spec.ts @@ -0,0 +1,229 @@ +import './setup'; +import { PLATFORM } from 'aurelia-pal'; +import { validateScrolledState, ensureScrolled, scrollToEnd, waitForNextFrame, validateState } from './utilities'; +import { VirtualRepeat } from '../src/virtual-repeat'; +import { StageComponent, ComponentTester } from 'aurelia-testing'; +import { bootstrap } from 'aurelia-bootstrapper'; +import { ITestAppInterface } from './interfaces'; + +PLATFORM.moduleName('src/virtual-repeat'); +PLATFORM.moduleName('test/noop-value-converter'); +PLATFORM.moduleName('src/infinite-scroll-next'); + +describe('VirtualRepeat Integration -- Mutation Handling', () => { + let component: ComponentTester; + let items: any[]; + let view: string; + let resources: any[]; + let itemHeight: number = 100; + + beforeEach(() => { + component = undefined; + items = createItems(1000); + resources = [ + 'src/virtual-repeat', + 'test/noop-value-converter' + ]; + view = + `
+
\${item}
+
`; + }); + + afterEach(() => { + try { + if (component) { + component.dispose(); + } + } catch (ex) { + console.log('Error disposing component'); + console.error(ex); + } + }); + + it('handles splice when scrolled to end', async () => { + const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }); + await scrollToEnd(virtualRepeat); + + viewModel.items.splice(995, 1, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'); + await waitForNextFrame(); + validateScrolledState(virtualRepeat, viewModel, itemHeight); + + await scrollToEnd(virtualRepeat); + let views = virtualRepeat.viewSlot.children; + expect(views[views.length - 1].bindingContext.item).toBe(viewModel.items[viewModel.items.length - 1]); + }); + + it('handles splice removing non-consecutive when scrolled to end', async () => { + const { virtualRepeat, viewModel } = await bootstrapComponent({ + items: items, + getNextPage: function() { + let itemLength = this.items.length; + for (let i = 0; i < 100; ++i) { + let itemNum = itemLength + i; + this.items.push('item' + itemNum); + } + } + }); + await scrollToEnd(virtualRepeat); + + for (let i = 0, ii = 100; i < ii; i++) { + viewModel.items.splice(i + 1, 9); + } + + await waitForNextFrame(); + validateScrolledState(virtualRepeat, viewModel, itemHeight); + + await scrollToEnd(virtualRepeat); + let views = virtualRepeat.viewSlot.children; + expect(views[views.length - 1].bindingContext.item).toBe(viewModel.items[viewModel.items.length - 1]); + }); + + it('handles splice non-consecutive when scrolled to end', async () => { + const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }); + await scrollToEnd(virtualRepeat); + + for (let i = 0, ii = 80; i < ii; i++) { + viewModel.items.splice(10 * i, 3, i as any); + } + + await waitForNextFrame(); + validateScrolledState(virtualRepeat, viewModel, itemHeight); + + await scrollToEnd(virtualRepeat); + let views = virtualRepeat.viewSlot.children; + expect(views[views.length - 1].bindingContext.item).toBe(viewModel.items[viewModel.items.length - 1]); + }); + + it('handles splice removing many', async () => { + const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }); + await scrollToEnd(virtualRepeat); + expect(virtualRepeat._viewsLength).toBe(12, 'virtualRepeat.viewsLength'); + // more items remaining than viewslot capacity + viewModel.items.splice(5, 1000 - virtualRepeat._viewsLength - 12); + + await waitForNextFrame(); + expect(virtualRepeat._first).toBe(1000 - (1000 - virtualRepeat._viewsLength), 'virtualRepeat._first 1'); + validateScrolledState(virtualRepeat, viewModel, itemHeight); + }); + + it('handles splice removing more', async () => { + // number of items remaining exactly as viewslot capacity + const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }); + await scrollToEnd(virtualRepeat); + + viewModel.items.splice(5, 1000 - virtualRepeat._viewsLength); + + await waitForNextFrame(); + + expect(virtualRepeat.viewSlot.children.length).toBe(viewModel.items.length); + validateScrolledState(virtualRepeat, viewModel, itemHeight); + }); + + // less items remaining than viewslot capacity + it('handles splice removing even more', async () => { + const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }); + await scrollToEnd(virtualRepeat); + + viewModel.items.splice(5, 1000 - virtualRepeat._viewsLength + 10); + + await waitForNextFrame(); + + expect(virtualRepeat.viewSlot.children.length).toBe(viewModel.items.length); + validateScrolledState(virtualRepeat, viewModel, itemHeight); + }); + + it('handles splice removing non-consecutive', async () => { + const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }); + await scrollToEnd(virtualRepeat); + + for (let i = 0, ii = 100; i < ii; i++) { + viewModel.items.splice(i + 1, 9); + } + + await waitForNextFrame(); + validateScrolledState(virtualRepeat, viewModel, itemHeight); + }); + + it('handles splice non-consecutive', async () => { + const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }); + await scrollToEnd(virtualRepeat); + + for (let i = 0, ii = 100; i < ii; i++) { + viewModel.items.splice(3 * (i + 1), 3, i as any); + } + await waitForNextFrame(); + + validateScrolledState(virtualRepeat, viewModel, itemHeight); + }); + + it('handles splice removing many + add', async () => { + let scrollCount = 0; + const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }); + virtualRepeat.element.parentElement.onscroll = () => { + scrollCount++; + }; + await scrollToEnd(virtualRepeat); + expect(scrollCount).toBe(2, '@scroll 1'); + expect(virtualRepeat.element.parentElement.scrollTop).toBe(100 * 995); + + viewModel.items.splice(5, 990, {}, {}); + expect(scrollCount).toBe(2, '@scroll 2'); + expect(virtualRepeat.items.length).toBe(12, 'items.length 1'); + expect(virtualRepeat.element.parentElement.scrollTop).toBe(100 * 995); + + await waitForNextFrame(); + expect(scrollCount).toBe(3, '@scroll 3'); + validateState(virtualRepeat, viewModel, itemHeight); + virtualRepeat.element.parentElement.onscroll = null; + }); + + // the number of views is 12 + // the number of items is 13 + // validating scroll state is a bit annoying + // but it's working fine + // todo: correctly assert this situation + xit('handles splice removing many + add', async () => { + let scrollCount = 0; + const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }); + virtualRepeat.element.parentElement.onscroll = () => { + scrollCount++; + }; + await scrollToEnd(virtualRepeat); + expect(scrollCount).toBe(2, '@scroll 1'); + expect(virtualRepeat.element.parentElement.scrollTop).toBe(100 * 995); + + viewModel.items.splice(5, 990, {}, {}, {}); + expect(scrollCount).toBe(2, '@scroll 2'); + expect(virtualRepeat.items.length).toBe(13, 'items.length 1'); + expect(virtualRepeat.element.parentElement.scrollTop).toBe(100 * 995); + + await waitForNextFrame(); + expect(scrollCount).toBe(3, '@scroll 3'); + validateState(virtualRepeat, viewModel, itemHeight); + virtualRepeat.element.parentElement.onscroll = null; + }); + + it('handles splice remove remaining + add', async () => { + const { virtualRepeat, viewModel } = await bootstrapComponent({ items: items }); + await scrollToEnd(virtualRepeat); + + viewModel.items.splice(5, 995, 'a', 'b', 'c'); + + await waitForNextFrame(); + validateScrolledState(virtualRepeat, viewModel, itemHeight); + }); + + async function bootstrapComponent($viewModel?: ITestAppInterface, $view?: string) { + component = StageComponent + .withResources(resources) + .inView($view || view) + .boundTo($viewModel); + await component.create(bootstrap); + return { virtualRepeat: component.viewModel, viewModel: $viewModel, component: component }; + } + + function createItems(amount: number, name: string = 'item') { + return Array.from({ length: amount }, (_, index) => name + index); + } +}); diff --git a/tslint.json b/tslint.json index 2eed036..8ef2e94 100644 --- a/tslint.json +++ b/tslint.json @@ -53,7 +53,6 @@ true, "check-open-brace", "check-catch", - "check-else", "check-finally", "check-whitespace" ],