Skip to content

Commit

Permalink
feat(infinite list): threshold emissions throttling (#2948)
Browse files Browse the repository at this point in the history
BREAKING CHANGE:
`NbScrollableContainerDimentions` class renamed to `NbScrollableContainerDimensions`.
  • Loading branch information
evtkhvch committed Dec 1, 2021
1 parent ebb8133 commit 718dd0a
Show file tree
Hide file tree
Showing 2 changed files with 253 additions and 80 deletions.
239 changes: 187 additions & 52 deletions src/framework/theme/components/list/infinite-list.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ let listElementRef: DebugElement;
let layoutComponent: NbLayoutComponent;
let infiniteListDirective: NbInfiniteListDirective;

// First change detection run must take place inside a `fakeAsync` zone,
// so rxjs interval scheduled in the `throttle` (by `interval` observable)
// use patched `setInterval`. Then we are able to control this interval via
// `tick`.
function setup() {
fixture.detectChanges();
tick();

listElementRef = fixture.debugElement.query(By.directive(NbListComponent));
layoutComponent = fixture.debugElement.query(By.directive(NbLayoutComponent)).componentInstance;
infiniteListDirective = listElementRef.injector.get(NbInfiniteListDirective);
testComponent = fixture.componentInstance;
}

@Component({
template: `
<nb-layout [withScroll]="withScroll">
Expand All @@ -33,6 +47,7 @@ let infiniteListDirective: NbInfiniteListDirective;
class="scroller"
[class.element-scroll]="!listenWindowScroll"
nbInfiniteList
[throttleTime]="throttleTime"
[threshold]="threshold"
[listenWindowScroll]="listenWindowScroll"
(bottomThreshold)="bottomThreshold()"
Expand Down Expand Up @@ -68,6 +83,7 @@ class ScrollTestComponent {
listenWindowScroll = false;
threshold = THRESHOLD;
withScroll = false;
throttleTime = 0;

bottomThreshold() {}
topThreshold() {}
Expand All @@ -85,13 +101,6 @@ describe('Directive: NbScrollDirective', () => {
providers: [NbLayoutScrollService, { provide: APP_BASE_HREF, useValue: '/' }],
declarations: [ScrollTestComponent],
}).createComponent(ScrollTestComponent);

fixture.detectChanges();

listElementRef = fixture.debugElement.query(By.directive(NbListComponent));
layoutComponent = fixture.debugElement.query(By.directive(NbLayoutComponent)).componentInstance;
infiniteListDirective = listElementRef.injector.get(NbInfiniteListDirective);
testComponent = fixture.componentInstance;
});

afterEach(fakeAsync(() => {
Expand All @@ -100,16 +109,18 @@ describe('Directive: NbScrollDirective', () => {
fixture.nativeElement.remove();
}));

it('should listen to window scroll', () => {
it('should listen to window scroll', fakeAsync(() => {
setup();
const checkPositionSpy = spyOn(infiniteListDirective, 'checkPosition');
testComponent.listenWindowScroll = true;
fixture.detectChanges();

window.dispatchEvent(new Event('scroll'));
expect(checkPositionSpy).toHaveBeenCalledTimes(1);
});
}));

it('should listen to layout scroll', () => {
it('should listen to layout scroll', fakeAsync(() => {
setup();
const checkPositionSpy = spyOn(infiniteListDirective, 'checkPosition');
testComponent.listenWindowScroll = true;
testComponent.withScroll = true;
Expand All @@ -118,15 +129,17 @@ describe('Directive: NbScrollDirective', () => {
layoutComponent.scrollableContainerRef.nativeElement.dispatchEvent(new Event('scroll'));

expect(checkPositionSpy).toHaveBeenCalledTimes(1);
});
}));

it('should listen to element scroll', () => {
it('should listen to element scroll', fakeAsync(() => {
setup();
const elementScrollHandlerSpy = spyOn(infiniteListDirective, 'onElementScroll');
listElementRef.nativeElement.dispatchEvent(new Event('scroll'));
expect(elementScrollHandlerSpy).toHaveBeenCalledTimes(1);
});
}));

it('should ignore window and layout scroll when listening to element scroll', () => {
it('should ignore window and layout scroll when listening to element scroll', fakeAsync(() => {
setup();
const checkPositionSpy = spyOn(infiniteListDirective, 'checkPosition');

window.dispatchEvent(new Event('scroll'));
Expand All @@ -138,141 +151,263 @@ describe('Directive: NbScrollDirective', () => {

listElementRef.nativeElement.dispatchEvent(new Event('scroll'));
expect(checkPositionSpy).toHaveBeenCalledTimes(1);
});
}));

it('should ignore element scroll when listening to window or layout scroll', () => {
it('should ignore element scroll when listening to window or layout scroll', fakeAsync(() => {
setup();
testComponent.listenWindowScroll = true;
fixture.detectChanges();

const checkPositionSpy = spyOn(infiniteListDirective, 'checkPosition');

listElementRef.nativeElement.dispatchEvent(new Event('scroll'));
tick(infiniteListDirective.throttleTime);
expect(checkPositionSpy).toHaveBeenCalledTimes(0);

window.dispatchEvent(new Event('scroll'));
tick(infiniteListDirective.throttleTime);
expect(checkPositionSpy).toHaveBeenCalledTimes(1);

testComponent.withScroll = true;
fixture.detectChanges();

listElementRef.nativeElement.dispatchEvent(new Event('scroll'));
tick(infiniteListDirective.throttleTime);
expect(checkPositionSpy).toHaveBeenCalledTimes(1);

const layoutScrollContainer = layoutComponent.scrollableContainerRef.nativeElement;
layoutScrollContainer.dispatchEvent(new Event('scroll'));
tick(infiniteListDirective.throttleTime);
expect(checkPositionSpy).toHaveBeenCalledTimes(2);
});
}));

it('should trigger bottomThreshold only when treshold reached (element scroll)', fakeAsync(() => {
it('should trigger bottomThreshold only when threshold reached (element scroll)', fakeAsync(() => {
setup();
const scrollingNativeElement = listElementRef.nativeElement;
const tresholdSpy = spyOn(testComponent, 'bottomThreshold');
const thresholdSpy = spyOn(testComponent, 'bottomThreshold');

const positionUnderThreshold = CONTENT_HEIGHT - THRESHOLD - ELEMENT_HEIGHT - 1;
scrollingNativeElement.scrollTop = positionUnderThreshold;
scrollingNativeElement.dispatchEvent(new Event('scroll'));
tick();
expect(tresholdSpy).toHaveBeenCalledTimes(0);
tick(infiniteListDirective.throttleTime);
expect(thresholdSpy).toHaveBeenCalledTimes(0);

const positionBelowThreshold = CONTENT_HEIGHT - THRESHOLD / 2;
scrollingNativeElement.scrollTop = positionBelowThreshold;
scrollingNativeElement.dispatchEvent(new Event('scroll'));
tick();
expect(tresholdSpy).toHaveBeenCalledTimes(1);
tick(infiniteListDirective.throttleTime);
expect(thresholdSpy).toHaveBeenCalledTimes(1);
}));

it('should trigger bottomThreshold only when treshold reached (window scroll)', fakeAsync(() => {
it('should trigger bottomThreshold only when threshold reached (window scroll)', fakeAsync(() => {
setup();
const { documentElement } = document;

testComponent.listenWindowScroll = true;
fixture.detectChanges();

const tresholdSpy = spyOn(testComponent, 'bottomThreshold');
const thresholdSpy = spyOn(testComponent, 'bottomThreshold');

const reporterHeight = 1000;
const positionUnderThreshold = CONTENT_HEIGHT - THRESHOLD - reporterHeight;
documentElement.scrollTop = positionUnderThreshold;
window.dispatchEvent(new Event('scroll'));
tick();
expect(tresholdSpy).toHaveBeenCalledTimes(0);
tick(infiniteListDirective.throttleTime);
expect(thresholdSpy).toHaveBeenCalledTimes(0);

const positionBelowThreshold = CONTENT_HEIGHT - THRESHOLD / 2;
documentElement.scrollTop = positionBelowThreshold;
window.dispatchEvent(new Event('scroll'));
tick();
expect(tresholdSpy).toHaveBeenCalledTimes(1);
tick(infiniteListDirective.throttleTime);
expect(thresholdSpy).toHaveBeenCalledTimes(1);
}));

it('should trigger bottomThreshold only when treshold reached (layout scroll)', fakeAsync(() => {
it('should trigger bottomThreshold only when threshold reached (layout scroll)', fakeAsync(() => {
setup();
const scroller: Element = layoutComponent.scrollableContainerRef.nativeElement;

testComponent.listenWindowScroll = true;
testComponent.withScroll = true;
fixture.detectChanges();

const tresholdSpy = spyOn(testComponent, 'bottomThreshold');
const thresholdSpy = spyOn(testComponent, 'bottomThreshold');

const positionUnderThreshold = CONTENT_HEIGHT - THRESHOLD - scroller.clientHeight - 1;
scroller.scrollTop = positionUnderThreshold;
scroller.dispatchEvent(new Event('scroll'));
tick();
expect(tresholdSpy).toHaveBeenCalledTimes(0);
tick(infiniteListDirective.throttleTime);
expect(thresholdSpy).toHaveBeenCalledTimes(0);

const positionBelowThreshold = CONTENT_HEIGHT - THRESHOLD / 2;
scroller.scrollTop = positionBelowThreshold;
scroller.dispatchEvent(new Event('scroll'));
tick();
expect(tresholdSpy).toHaveBeenCalledTimes(1);
tick(infiniteListDirective.throttleTime);
expect(thresholdSpy).toHaveBeenCalledTimes(1);
}));

it('should trigger topThreshold when treshold reached (element)', fakeAsync(() => {
it('should trigger topThreshold when threshold reached (element)', fakeAsync(() => {
setup();
const scrollingElement = listElementRef.nativeElement;
const tresholdSpy = spyOn(testComponent, 'topThreshold');
const thresholdSpy = spyOn(testComponent, 'topThreshold');

scrollingElement.scrollTop = THRESHOLD + 1;
scrollingElement.dispatchEvent(new Event('scroll'));
tick();
expect(tresholdSpy).toHaveBeenCalledTimes(0);
tick(infiniteListDirective.throttleTime);
expect(thresholdSpy).toHaveBeenCalledTimes(0);

scrollingElement.scrollTop = THRESHOLD - 1;
scrollingElement.dispatchEvent(new Event('scroll'));
tick();
expect(tresholdSpy).toHaveBeenCalledTimes(1);
tick(infiniteListDirective.throttleTime);
expect(thresholdSpy).toHaveBeenCalledTimes(1);
}));

it('should trigger topThreshold when treshold reached (window)', fakeAsync(() => {
it('should trigger topThreshold when threshold reached (window)', fakeAsync(() => {
setup();
testComponent.listenWindowScroll = true;
fixture.detectChanges();

const { documentElement } = document;
const tresholdSpy = spyOn(testComponent, 'topThreshold');
const thresholdSpy = spyOn(testComponent, 'topThreshold');

documentElement.scrollTop = THRESHOLD + 1;
window.dispatchEvent(new Event('scroll'));
tick();
expect(tresholdSpy).toHaveBeenCalledTimes(0);
tick(infiniteListDirective.throttleTime);
expect(thresholdSpy).toHaveBeenCalledTimes(0);

documentElement.scrollTop = THRESHOLD - 1;
window.dispatchEvent(new Event('scroll'));
tick();
expect(tresholdSpy).toHaveBeenCalledTimes(1);
tick(infiniteListDirective.throttleTime);
expect(thresholdSpy).toHaveBeenCalledTimes(1);
}));

it('should trigger topThreshold when treshold reached (layout scroll)', fakeAsync(() => {
it('should trigger topThreshold when threshold reached (layout scroll)', fakeAsync(() => {
setup();
testComponent.listenWindowScroll = true;
testComponent.withScroll = true;
fixture.detectChanges();

const layoutElement = layoutComponent.scrollableContainerRef.nativeElement;
const tresholdSpy = spyOn(testComponent, 'topThreshold');
const thresholdSpy = spyOn(testComponent, 'topThreshold');

layoutElement.scrollTop = THRESHOLD + 1;
layoutElement.dispatchEvent(new Event('scroll'));
tick();
expect(tresholdSpy).toHaveBeenCalledTimes(0);
tick(infiniteListDirective.throttleTime);
expect(thresholdSpy).toHaveBeenCalledTimes(0);

layoutElement.scrollTop = THRESHOLD - 1;
layoutElement.dispatchEvent(new Event('scroll'));
tick(infiniteListDirective.throttleTime);
expect(thresholdSpy).toHaveBeenCalledTimes(1);
}));

it('should prevent subsequent bottomThreshold emissions for throttleTime duration (window scroll)', fakeAsync(() => {
setup();
const { documentElement } = document;
const THROTTLE = 200;

testComponent.listenWindowScroll = true;
testComponent.throttleTime = THROTTLE;
fixture.detectChanges();

const thresholdSpy = spyOn(testComponent, 'bottomThreshold');

documentElement.scrollTop = CONTENT_HEIGHT - THRESHOLD / 2;

window.dispatchEvent(new Event('scroll'));
tick(THROTTLE / 2); // 100ms passed
window.dispatchEvent(new Event('scroll'));
tick(THROTTLE / 2 - 1); // 199ms passed, resent scroll event should be throttled
expect(thresholdSpy).toHaveBeenCalledTimes(1);
tick(1); // 200ms passed, throttling has stopped

window.dispatchEvent(new Event('scroll'));
tick();
expect(thresholdSpy).toHaveBeenCalledTimes(2);
tick(THROTTLE); // waiting for the end of the throttle interval
}));

it('should prevent subsequent topThreshold emissions for throttleTime duration (window scroll)', fakeAsync(() => {
setup();
const { documentElement } = document;
const THROTTLE = 200;

testComponent.listenWindowScroll = true;
testComponent.throttleTime = THROTTLE;

fixture.detectChanges();

documentElement.scrollTop = THRESHOLD + 1;
window.dispatchEvent(new Event('scroll'));

const thresholdSpy = spyOn(testComponent, 'topThreshold');

documentElement.scrollTop -= 1;
window.dispatchEvent(new Event('scroll'));
tick(THROTTLE / 2); // 100ms passed
documentElement.scrollTop -= 1;
window.dispatchEvent(new Event('scroll'));
tick(THROTTLE / 2 - 1); // 199ms passed, resent scroll event should be throttled
expect(thresholdSpy).toHaveBeenCalledTimes(1);
tick(1); // 200ms passed, throttling has stopped

documentElement.scrollTop -= 1;
window.dispatchEvent(new Event('scroll'));
tick();
expect(thresholdSpy).toHaveBeenCalledTimes(2);
tick(THROTTLE); // waiting for the end of the throttle interval
}));

it('should prevent subsequent bottomThreshold emissions for throttleTime duration (element scroll)', fakeAsync(() => {
setup();
const scrollingNativeElement = listElementRef.nativeElement;
const THROTTLE = 200;

testComponent.throttleTime = THROTTLE;
fixture.detectChanges();

const thresholdSpy = spyOn(testComponent, 'bottomThreshold');

scrollingNativeElement.scrollTop = CONTENT_HEIGHT - THRESHOLD / 2;

scrollingNativeElement.dispatchEvent(new Event('scroll'));
tick(THROTTLE / 2); // 100ms passed
scrollingNativeElement.dispatchEvent(new Event('scroll'));
tick(THROTTLE / 2 - 1); // 199ms passed, resent scroll event should be throttled
expect(thresholdSpy).toHaveBeenCalledTimes(1);
tick(1); // 200ms passed, throttling has stopped

scrollingNativeElement.dispatchEvent(new Event('scroll'));
tick();
expect(thresholdSpy).toHaveBeenCalledTimes(2);
tick(THROTTLE); // waiting for the end of the throttle interval
}));

it('should prevent subsequent topThreshold emissions for throttleTime duration (element scroll)', fakeAsync(() => {
setup();
const scrollingElement = listElementRef.nativeElement;
const THROTTLE = 200;

testComponent.throttleTime = THROTTLE;
fixture.detectChanges();

scrollingElement.scrollTop = THRESHOLD + 1;
scrollingElement.dispatchEvent(new Event('scroll'));

const thresholdSpy = spyOn(testComponent, 'topThreshold');

scrollingElement.scrollTop -= 1;
scrollingElement.dispatchEvent(new Event('scroll'));
tick(THROTTLE / 2); // 100ms passed
scrollingElement.scrollTop -= 1;
scrollingElement.dispatchEvent(new Event('scroll'));
tick(THROTTLE / 2 - 1); // 199ms passed, resent scroll event should be throttled
expect(thresholdSpy).toHaveBeenCalledTimes(1);
tick(1); // 200ms passed, throttling has stopped

scrollingElement.scrollTop -= 1;
scrollingElement.dispatchEvent(new Event('scroll'));
tick();
expect(tresholdSpy).toHaveBeenCalledTimes(1);
expect(thresholdSpy).toHaveBeenCalledTimes(2);
tick(THROTTLE); // waiting for the end of the throttle interval
}));
});
Loading

0 comments on commit 718dd0a

Please sign in to comment.