518 lines
17 KiB
TypeScript
518 lines
17 KiB
TypeScript
import { Injector, ReflectiveInjector } from '@angular/core';
|
|
import { fakeAsync, tick } from '@angular/core/testing';
|
|
import { DOCUMENT } from '@angular/common';
|
|
|
|
import { ScrollService } from 'app/shared/scroll.service';
|
|
import { ScrollItem, ScrollSpiedElement, ScrollSpiedElementGroup, ScrollSpyService } from 'app/shared/scroll-spy.service';
|
|
|
|
|
|
describe('ScrollSpiedElement', () => {
|
|
it('should expose the spied element and index', () => {
|
|
const elem = {} as Element;
|
|
const spiedElem = new ScrollSpiedElement(elem, 42);
|
|
|
|
expect(spiedElem.element).toBe(elem);
|
|
expect(spiedElem.index).toBe(42);
|
|
});
|
|
|
|
describe('#calculateTop()', () => {
|
|
it('should calculate the `top` value', () => {
|
|
const elem = {getBoundingClientRect: () => ({top: 100})} as Element;
|
|
const spiedElem = new ScrollSpiedElement(elem, 42);
|
|
|
|
spiedElem.calculateTop(0, 0);
|
|
expect(spiedElem.top).toBe(100);
|
|
|
|
spiedElem.calculateTop(20, 0);
|
|
expect(spiedElem.top).toBe(120);
|
|
|
|
spiedElem.calculateTop(0, 10);
|
|
expect(spiedElem.top).toBe(90);
|
|
|
|
spiedElem.calculateTop(20, 10);
|
|
expect(spiedElem.top).toBe(110);
|
|
});
|
|
});
|
|
});
|
|
|
|
|
|
describe('ScrollSpiedElementGroup', () => {
|
|
describe('#calibrate()', () => {
|
|
it('should calculate `top` for all spied elements', () => {
|
|
const spy = spyOn(ScrollSpiedElement.prototype, 'calculateTop').and.returnValue(0);
|
|
const elems = [{}, {}, {}] as Element[];
|
|
const group = new ScrollSpiedElementGroup(elems);
|
|
|
|
expect(spy).not.toHaveBeenCalled();
|
|
|
|
group.calibrate(20, 10);
|
|
const callInfo = spy.calls.all();
|
|
|
|
expect(spy).toHaveBeenCalledTimes(3);
|
|
expect(callInfo[0].object.index).toBe(0);
|
|
expect(callInfo[1].object.index).toBe(1);
|
|
expect(callInfo[2].object.index).toBe(2);
|
|
expect(callInfo[0].args).toEqual([20, 10]);
|
|
expect(callInfo[1].args).toEqual([20, 10]);
|
|
expect(callInfo[2].args).toEqual([20, 10]);
|
|
});
|
|
});
|
|
|
|
describe('#onScroll()', () => {
|
|
let group: ScrollSpiedElementGroup;
|
|
let activeItems: (ScrollItem|null)[];
|
|
|
|
const activeIndices = () => activeItems.map(x => x && x.index);
|
|
|
|
beforeEach(() => {
|
|
const tops = [50, 150, 100];
|
|
|
|
spyOn(ScrollSpiedElement.prototype, 'calculateTop').and.callFake(
|
|
function(this: ScrollSpiedElement, scrollTop: number, topOffset: number) {
|
|
this.top = tops[this.index];
|
|
});
|
|
|
|
activeItems = [];
|
|
group = new ScrollSpiedElementGroup([{}, {}, {}] as Element[]);
|
|
group.activeScrollItem.subscribe(item => activeItems.push(item));
|
|
group.calibrate(20, 10);
|
|
});
|
|
|
|
|
|
it('should emit a `ScrollItem` on `activeScrollItem`', () => {
|
|
expect(activeItems.length).toBe(0);
|
|
|
|
group.onScroll(20, 140);
|
|
expect(activeItems.length).toBe(1);
|
|
|
|
group.onScroll(20, 140);
|
|
expect(activeItems.length).toBe(2);
|
|
});
|
|
|
|
it('should emit the lower-most element that is above `scrollTop`', () => {
|
|
group.onScroll(45, 200);
|
|
group.onScroll(55, 200);
|
|
expect(activeIndices()).toEqual([null, 0]);
|
|
|
|
activeItems.length = 0;
|
|
group.onScroll(95, 200);
|
|
group.onScroll(105, 200);
|
|
expect(activeIndices()).toEqual([0, 2]);
|
|
|
|
activeItems.length = 0;
|
|
group.onScroll(145, 200);
|
|
group.onScroll(155, 200);
|
|
expect(activeIndices()).toEqual([2, 1]);
|
|
|
|
activeItems.length = 0;
|
|
group.onScroll(75, 200);
|
|
group.onScroll(175, 200);
|
|
group.onScroll(125, 200);
|
|
group.onScroll(25, 200);
|
|
expect(activeIndices()).toEqual([0, 1, 2, null]);
|
|
});
|
|
|
|
it('should always emit the lower-most element if scrolled to the bottom', () => {
|
|
group.onScroll(140, 140);
|
|
group.onScroll(145, 140);
|
|
group.onScroll(138.5, 140);
|
|
group.onScroll(139.5, 140);
|
|
|
|
expect(activeIndices()).toEqual([1, 1, 2, 1]);
|
|
});
|
|
|
|
it('should emit null if all elements are below `scrollTop`', () => {
|
|
group.onScroll(0, 140);
|
|
expect(activeItems).toEqual([null]);
|
|
|
|
group.onScroll(49, 140);
|
|
expect(activeItems).toEqual([null, null]);
|
|
});
|
|
|
|
it('should emit null if there are no spied elements (even if scrolled to the bottom)', () => {
|
|
group = new ScrollSpiedElementGroup([]);
|
|
group.activeScrollItem.subscribe(item => activeItems.push(item));
|
|
|
|
group.onScroll(20, 140);
|
|
expect(activeItems).toEqual([null]);
|
|
|
|
group.onScroll(140, 140);
|
|
expect(activeItems).toEqual([null, null]);
|
|
|
|
group.onScroll(145, 140);
|
|
expect(activeItems).toEqual([null, null, null]);
|
|
});
|
|
});
|
|
});
|
|
|
|
|
|
describe('ScrollSpyService', () => {
|
|
let injector: Injector;
|
|
let scrollSpyService: ScrollSpyService;
|
|
|
|
beforeEach(() => {
|
|
injector = ReflectiveInjector.resolveAndCreate([
|
|
{ provide: DOCUMENT, useValue: { body: {} } },
|
|
{ provide: ScrollService, useValue: { topOffset: 50 } },
|
|
ScrollSpyService
|
|
]);
|
|
|
|
scrollSpyService = injector.get(ScrollSpyService);
|
|
});
|
|
|
|
|
|
describe('#spyOn()', () => {
|
|
let getSpiedElemGroups: () => ScrollSpiedElementGroup[];
|
|
|
|
beforeEach(() => {
|
|
getSpiedElemGroups = () => (scrollSpyService as any).spiedElementGroups;
|
|
});
|
|
|
|
|
|
it('should create a `ScrollSpiedElementGroup` when called', () => {
|
|
expect(getSpiedElemGroups().length).toBe(0);
|
|
|
|
scrollSpyService.spyOn([]);
|
|
expect(getSpiedElemGroups().length).toBe(1);
|
|
});
|
|
|
|
it('should initialize the newly created `ScrollSpiedElementGroup`', () => {
|
|
const calibrateSpy = spyOn(ScrollSpiedElementGroup.prototype, 'calibrate');
|
|
const onScrollSpy = spyOn(ScrollSpiedElementGroup.prototype, 'onScroll');
|
|
|
|
scrollSpyService.spyOn([]);
|
|
expect(calibrateSpy).toHaveBeenCalledTimes(1);
|
|
expect(onScrollSpy).toHaveBeenCalledTimes(1);
|
|
|
|
scrollSpyService.spyOn([]);
|
|
expect(calibrateSpy).toHaveBeenCalledTimes(2);
|
|
expect(onScrollSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should call `onResize()` if it is the first `ScrollSpiedElementGroup`', () => {
|
|
const actions: string[] = [];
|
|
|
|
const onResizeSpy = spyOn(ScrollSpyService.prototype as any, 'onResize')
|
|
.and.callFake(() => actions.push('onResize'));
|
|
const calibrateSpy = spyOn(ScrollSpiedElementGroup.prototype, 'calibrate')
|
|
.and.callFake(() => actions.push('calibrate'));
|
|
|
|
expect(onResizeSpy).not.toHaveBeenCalled();
|
|
expect(calibrateSpy).not.toHaveBeenCalled();
|
|
|
|
scrollSpyService.spyOn([]);
|
|
expect(actions).toEqual(['onResize', 'calibrate']);
|
|
|
|
scrollSpyService.spyOn([]);
|
|
expect(actions).toEqual(['onResize', 'calibrate', 'calibrate']);
|
|
});
|
|
|
|
it('should forward `ScrollSpiedElementGroup#activeScrollItem` as `active`', () => {
|
|
const activeIndices1: (number | null)[] = [];
|
|
const activeIndices2: (number | null)[] = [];
|
|
|
|
const info1 = scrollSpyService.spyOn([]);
|
|
const info2 = scrollSpyService.spyOn([]);
|
|
const spiedElemGroups = getSpiedElemGroups();
|
|
|
|
info1.active.subscribe(item => activeIndices1.push(item && item.index));
|
|
info2.active.subscribe(item => activeIndices2.push(item && item.index));
|
|
activeIndices1.length = 0;
|
|
activeIndices2.length = 0;
|
|
|
|
spiedElemGroups[0].activeScrollItem.next({index: 1} as ScrollItem);
|
|
spiedElemGroups[0].activeScrollItem.next({index: 2} as ScrollItem);
|
|
spiedElemGroups[1].activeScrollItem.next({index: 3} as ScrollItem);
|
|
spiedElemGroups[0].activeScrollItem.next(null);
|
|
spiedElemGroups[1].activeScrollItem.next({index: 4} as ScrollItem);
|
|
spiedElemGroups[1].activeScrollItem.next(null);
|
|
spiedElemGroups[0].activeScrollItem.next({index: 5} as ScrollItem);
|
|
spiedElemGroups[1].activeScrollItem.next({index: 6} as ScrollItem);
|
|
|
|
expect(activeIndices1).toEqual([1, 2, null, 5]);
|
|
expect(activeIndices2).toEqual([3, 4, null, 6]);
|
|
});
|
|
|
|
it('should remember and emit the last active item to new subscribers', () => {
|
|
const items = [{index: 1}, {index: 2}, {index: 3}] as ScrollItem[];
|
|
let lastActiveItem: ScrollItem|null;
|
|
|
|
const info = scrollSpyService.spyOn([]);
|
|
const spiedElemGroup = getSpiedElemGroups()[0];
|
|
|
|
spiedElemGroup.activeScrollItem.next(items[0]);
|
|
spiedElemGroup.activeScrollItem.next(items[1]);
|
|
spiedElemGroup.activeScrollItem.next(items[2]);
|
|
spiedElemGroup.activeScrollItem.next(null);
|
|
spiedElemGroup.activeScrollItem.next(items[1]);
|
|
info.active.subscribe(item => lastActiveItem = item);
|
|
|
|
expect(lastActiveItem!).toBe(items[1]);
|
|
|
|
spiedElemGroup.activeScrollItem.next(null);
|
|
info.active.subscribe(item => lastActiveItem = item);
|
|
|
|
expect(lastActiveItem!).toBeNull();
|
|
});
|
|
|
|
it('should only emit distinct values on `active`', () => {
|
|
const items = [{index: 1}, {index: 2}] as ScrollItem[];
|
|
const activeIndices: (number | null)[] = [];
|
|
|
|
const info = scrollSpyService.spyOn([]);
|
|
const spiedElemGroup = getSpiedElemGroups()[0];
|
|
|
|
info.active.subscribe(item => activeIndices.push(item && item.index));
|
|
activeIndices.length = 0;
|
|
|
|
spiedElemGroup.activeScrollItem.next(items[0]);
|
|
spiedElemGroup.activeScrollItem.next(items[0]);
|
|
spiedElemGroup.activeScrollItem.next(items[1]);
|
|
spiedElemGroup.activeScrollItem.next(items[1]);
|
|
spiedElemGroup.activeScrollItem.next(null);
|
|
spiedElemGroup.activeScrollItem.next(null);
|
|
spiedElemGroup.activeScrollItem.next(items[0]);
|
|
spiedElemGroup.activeScrollItem.next(items[1]);
|
|
spiedElemGroup.activeScrollItem.next(null);
|
|
|
|
expect(activeIndices).toEqual([1, 2, null, 1, 2, null]);
|
|
});
|
|
|
|
it('should remove the corresponding `ScrollSpiedElementGroup` when calling `unspy()`', () => {
|
|
const info1 = scrollSpyService.spyOn([]);
|
|
const info2 = scrollSpyService.spyOn([]);
|
|
const info3 = scrollSpyService.spyOn([]);
|
|
const groups = getSpiedElemGroups().slice();
|
|
|
|
expect(getSpiedElemGroups()).toEqual(groups);
|
|
|
|
info2.unspy();
|
|
expect(getSpiedElemGroups()).toEqual([groups[0], groups[2]]);
|
|
|
|
info1.unspy();
|
|
expect(getSpiedElemGroups()).toEqual([groups[2]]);
|
|
|
|
info3.unspy();
|
|
expect(getSpiedElemGroups()).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('window resize events', () => {
|
|
const RESIZE_EVENT_DELAY = 300;
|
|
let onResizeSpy: jasmine.Spy;
|
|
|
|
beforeEach(() => {
|
|
onResizeSpy = spyOn(ScrollSpyService.prototype as any, 'onResize');
|
|
});
|
|
|
|
|
|
it('should be subscribed to when the first group of elements is spied on', fakeAsync(() => {
|
|
window.dispatchEvent(new Event('resize'));
|
|
expect(onResizeSpy).not.toHaveBeenCalled();
|
|
|
|
scrollSpyService.spyOn([]);
|
|
onResizeSpy.calls.reset();
|
|
|
|
window.dispatchEvent(new Event('resize'));
|
|
expect(onResizeSpy).not.toHaveBeenCalled();
|
|
|
|
tick(RESIZE_EVENT_DELAY);
|
|
expect(onResizeSpy).toHaveBeenCalled();
|
|
}));
|
|
|
|
it('should be unsubscribed from when the last group of elements is removed', fakeAsync(() => {
|
|
const info1 = scrollSpyService.spyOn([]);
|
|
const info2 = scrollSpyService.spyOn([]);
|
|
onResizeSpy.calls.reset();
|
|
|
|
window.dispatchEvent(new Event('resize'));
|
|
tick(RESIZE_EVENT_DELAY);
|
|
expect(onResizeSpy).toHaveBeenCalled();
|
|
|
|
info1.unspy();
|
|
onResizeSpy.calls.reset();
|
|
|
|
window.dispatchEvent(new Event('resize'));
|
|
tick(RESIZE_EVENT_DELAY);
|
|
expect(onResizeSpy).toHaveBeenCalled();
|
|
|
|
info2.unspy();
|
|
onResizeSpy.calls.reset();
|
|
|
|
window.dispatchEvent(new Event('resize'));
|
|
tick(RESIZE_EVENT_DELAY);
|
|
expect(onResizeSpy).not.toHaveBeenCalled();
|
|
}));
|
|
|
|
it(`should only fire every ${RESIZE_EVENT_DELAY}ms`, fakeAsync(() => {
|
|
scrollSpyService.spyOn([]);
|
|
onResizeSpy.calls.reset();
|
|
|
|
window.dispatchEvent(new Event('resize'));
|
|
tick(RESIZE_EVENT_DELAY - 2);
|
|
expect(onResizeSpy).not.toHaveBeenCalled();
|
|
|
|
window.dispatchEvent(new Event('resize'));
|
|
tick(1);
|
|
expect(onResizeSpy).not.toHaveBeenCalled();
|
|
|
|
window.dispatchEvent(new Event('resize'));
|
|
tick(1);
|
|
expect(onResizeSpy).toHaveBeenCalledTimes(1);
|
|
|
|
onResizeSpy.calls.reset();
|
|
tick(RESIZE_EVENT_DELAY / 2);
|
|
|
|
window.dispatchEvent(new Event('resize'));
|
|
tick(RESIZE_EVENT_DELAY - 2);
|
|
expect(onResizeSpy).not.toHaveBeenCalled();
|
|
|
|
window.dispatchEvent(new Event('resize'));
|
|
tick(1);
|
|
expect(onResizeSpy).not.toHaveBeenCalled();
|
|
|
|
window.dispatchEvent(new Event('resize'));
|
|
tick(1);
|
|
expect(onResizeSpy).toHaveBeenCalledTimes(1);
|
|
}));
|
|
});
|
|
|
|
describe('window scroll events', () => {
|
|
const SCROLL_EVENT_DELAY = 10;
|
|
let onScrollSpy: jasmine.Spy;
|
|
|
|
beforeEach(() => {
|
|
onScrollSpy = spyOn(ScrollSpyService.prototype as any, 'onScroll');
|
|
});
|
|
|
|
|
|
it('should be subscribed to when the first group of elements is spied on', fakeAsync(() => {
|
|
window.dispatchEvent(new Event('scroll'));
|
|
expect(onScrollSpy).not.toHaveBeenCalled();
|
|
|
|
scrollSpyService.spyOn([]);
|
|
|
|
window.dispatchEvent(new Event('scroll'));
|
|
expect(onScrollSpy).not.toHaveBeenCalled();
|
|
|
|
tick(SCROLL_EVENT_DELAY);
|
|
expect(onScrollSpy).toHaveBeenCalled();
|
|
}));
|
|
|
|
it('should be unsubscribed from when the last group of elements is removed', fakeAsync(() => {
|
|
const info1 = scrollSpyService.spyOn([]);
|
|
const info2 = scrollSpyService.spyOn([]);
|
|
|
|
window.dispatchEvent(new Event('scroll'));
|
|
tick(SCROLL_EVENT_DELAY);
|
|
expect(onScrollSpy).toHaveBeenCalled();
|
|
|
|
info1.unspy();
|
|
onScrollSpy.calls.reset();
|
|
|
|
window.dispatchEvent(new Event('scroll'));
|
|
tick(SCROLL_EVENT_DELAY);
|
|
expect(onScrollSpy).toHaveBeenCalled();
|
|
|
|
info2.unspy();
|
|
onScrollSpy.calls.reset();
|
|
|
|
window.dispatchEvent(new Event('scroll'));
|
|
tick(SCROLL_EVENT_DELAY);
|
|
expect(onScrollSpy).not.toHaveBeenCalled();
|
|
}));
|
|
|
|
it(`should only fire every ${SCROLL_EVENT_DELAY}ms`, fakeAsync(() => {
|
|
scrollSpyService.spyOn([]);
|
|
|
|
window.dispatchEvent(new Event('scroll'));
|
|
tick(SCROLL_EVENT_DELAY - 2);
|
|
expect(onScrollSpy).not.toHaveBeenCalled();
|
|
|
|
window.dispatchEvent(new Event('scroll'));
|
|
tick(1);
|
|
expect(onScrollSpy).not.toHaveBeenCalled();
|
|
|
|
window.dispatchEvent(new Event('scroll'));
|
|
tick(1);
|
|
expect(onScrollSpy).toHaveBeenCalledTimes(1);
|
|
|
|
onScrollSpy.calls.reset();
|
|
tick(SCROLL_EVENT_DELAY / 2);
|
|
|
|
window.dispatchEvent(new Event('scroll'));
|
|
tick(SCROLL_EVENT_DELAY - 2);
|
|
expect(onScrollSpy).not.toHaveBeenCalled();
|
|
|
|
window.dispatchEvent(new Event('scroll'));
|
|
tick(1);
|
|
expect(onScrollSpy).not.toHaveBeenCalled();
|
|
|
|
window.dispatchEvent(new Event('scroll'));
|
|
tick(1);
|
|
expect(onScrollSpy).toHaveBeenCalledTimes(1);
|
|
}));
|
|
});
|
|
|
|
describe('#onResize()', () => {
|
|
it('should re-calibrate each `ScrollSpiedElementGroup`', () => {
|
|
scrollSpyService.spyOn([]);
|
|
scrollSpyService.spyOn([]);
|
|
scrollSpyService.spyOn([]);
|
|
|
|
const spiedElemGroups: ScrollSpiedElementGroup[] = (scrollSpyService as any).spiedElementGroups;
|
|
const calibrateSpies = spiedElemGroups.map(group => spyOn(group, 'calibrate'));
|
|
|
|
calibrateSpies.forEach(spy => expect(spy).not.toHaveBeenCalled());
|
|
|
|
(scrollSpyService as any).onResize();
|
|
calibrateSpies.forEach(spy => expect(spy).toHaveBeenCalled());
|
|
});
|
|
});
|
|
|
|
describe('#onScroll()', () => {
|
|
it('should propagate to each `ScrollSpiedElementGroup`', () => {
|
|
scrollSpyService.spyOn([]);
|
|
scrollSpyService.spyOn([]);
|
|
scrollSpyService.spyOn([]);
|
|
|
|
const spiedElemGroups: ScrollSpiedElementGroup[] = (scrollSpyService as any).spiedElementGroups;
|
|
const onScrollSpies = spiedElemGroups.map(group => spyOn(group, 'onScroll'));
|
|
|
|
onScrollSpies.forEach(spy => expect(spy).not.toHaveBeenCalled());
|
|
|
|
(scrollSpyService as any).onScroll();
|
|
onScrollSpies.forEach(spy => expect(spy).toHaveBeenCalled());
|
|
});
|
|
|
|
it('should first re-calibrate if the content height has changed', () => {
|
|
const body = injector.get(DOCUMENT).body as any;
|
|
|
|
scrollSpyService.spyOn([]);
|
|
scrollSpyService.spyOn([]);
|
|
scrollSpyService.spyOn([]);
|
|
|
|
const spiedElemGroups: ScrollSpiedElementGroup[] = (scrollSpyService as any).spiedElementGroups;
|
|
const onScrollSpies = spiedElemGroups.map(group => spyOn(group, 'onScroll'));
|
|
const calibrateSpies = spiedElemGroups.map((group, i) => spyOn(group, 'calibrate')
|
|
.and.callFake(() => expect(onScrollSpies[i]).not.toHaveBeenCalled()));
|
|
|
|
calibrateSpies.forEach(spy => expect(spy).not.toHaveBeenCalled());
|
|
onScrollSpies.forEach(spy => expect(spy).not.toHaveBeenCalled());
|
|
|
|
// No content height change...
|
|
(scrollSpyService as any).onScroll();
|
|
calibrateSpies.forEach(spy => expect(spy).not.toHaveBeenCalled());
|
|
onScrollSpies.forEach(spy => expect(spy).toHaveBeenCalled());
|
|
|
|
onScrollSpies.forEach(spy => spy.calls.reset());
|
|
body.scrollHeight = 100;
|
|
|
|
// Viewport changed...
|
|
(scrollSpyService as any).onScroll();
|
|
calibrateSpies.forEach(spy => expect(spy).toHaveBeenCalled());
|
|
onScrollSpies.forEach(spy => expect(spy).toHaveBeenCalled());
|
|
});
|
|
});
|
|
});
|