Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(animationFrames): Adds an observable of animationFrames #5021

Merged
merged 2 commits into from Sep 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions spec-dtslint/observables/dom/animationFrames-spec.ts
@@ -0,0 +1,13 @@
import { animationFrames } from 'rxjs';

it('should just be an observable of numbers', () => {
const o$ = animationFrames(); // $ExpectType Observable<number>
});

it('should allow the passing of a timestampProvider', () => {
const o$ = animationFrames(performance); // $ExpectType Observable<number>
});

it('should not allow the passing of an invalid timestamp provider', () => {
const o$ = animationFrames({ now() { return 'wee' } }); // $ExpectError
});
102 changes: 102 additions & 0 deletions spec/helpers/test-helper.ts
Expand Up @@ -4,6 +4,7 @@ import { of, asyncScheduler, Observable, scheduled, ObservableInput } from 'rxjs
import { root } from 'rxjs/internal/util/root';
import { observable } from 'rxjs/internal/symbol/observable';
import { iterator } from 'rxjs/internal/symbol/iterator';
import * as sinon from 'sinon';

export function lowerCaseO<T>(...args: Array<any>): Observable<T> {
const o = {
Expand Down Expand Up @@ -47,3 +48,104 @@ export const createObservableInputs = <T>(value: T) => of(
) as Observable<ObservableInput<T>>;

global.__root__ = root;

let _raf: any;
let _caf: any;
let _id = 0;

/**
* A type used to test `requestAnimationFrame`
*/
export interface RAFTestTools {
/**
* Synchronously fire the next scheduled animation frame
*/
tick(): void;

/**
* Synchronously fire all scheduled animation frames
*/
flush(): void;

/**
* Un-monkey-patch `requestAnimationFrame` and `cancelAnimationFrame`
*/
restore(): void;
}

/**
* Monkey patches `requestAnimationFrame` and `cancelAnimationFrame`, returning a
* toolset to allow animation frames to be synchronously controlled.
*
* ### Usage
* ```ts
* let raf: RAFTestTools;
*
* beforeEach(() => {
* // patch requestAnimationFrame
* raf = stubRAF();
* });
*
* afterEach(() => {
* // unpatch
* raf.restore();
* });
*
* it('should fire handlers', () => {
* let test = false;
* // use requestAnimationFrame as normal
* requestAnimationFrame(() => test = true);
* // no frame has fired yet (this would be generally true anyhow)
* expect(test).to.equal(false);
* // manually fire the next animation frame
* raf.tick();
* // frame as fired
* expect(test).to.equal(true);
* // raf is now a SinonStub that can be asserted against
* expect(requestAnimationFrame).to.have.been.calledOnce;
* });
* ```
*/
export function stubRAF(): RAFTestTools {
_raf = requestAnimationFrame;
_caf = cancelAnimationFrame;

const handlers: any[] = [];

(requestAnimationFrame as any) = sinon.stub().callsFake((handler: Function) => {
const id = _id++;
handlers.push({ id, handler });
return id;
});

(cancelAnimationFrame as any) = sinon.stub().callsFake((id: number) => {
const index = handlers.findIndex(x => x.id === id);
if (index >= 0) {
handlers.splice(index, 1);
}
});

function tick() {
if (handlers.length > 0) {
handlers.shift().handler();
}
}

function flush() {
while (handlers.length > 0) {
handlers.shift().handler();
}
}

return {
tick,
flush,
restore() {
(requestAnimationFrame as any) = _raf;
(cancelAnimationFrame as any) = _caf;
_raf = _caf = undefined;
handlers.length = 0;
_id = 0;
}
};
}
166 changes: 166 additions & 0 deletions spec/observables/dom/animationFrames-spec.ts
@@ -0,0 +1,166 @@
import { expect } from 'chai';
import { animationFrames, Subject } from 'rxjs';
import * as sinon from 'sinon';
import { take, takeUntil } from 'rxjs/operators';
import { RAFTestTools, stubRAF } from 'spec/helpers/test-helper';

describe('animationFrame', () => {
let raf: RAFTestTools;
let DateStub: sinon.SinonStub;
let now = 1000;

beforeEach(() => {
raf = stubRAF();
DateStub = sinon.stub(Date, 'now').callsFake(() => {
return ++now;
});
});

afterEach(() => {
raf.restore();
DateStub.restore();
});

it('should animate', function () {
const results: any[] = [];
const source$ = animationFrames();

const subs = source$.subscribe({
next: ts => results.push(ts),
error: err => results.push(err),
complete: () => results.push('done'),
});

expect(DateStub).to.have.been.calledOnce;

expect(results).to.deep.equal([]);

raf.tick();
expect(DateStub).to.have.been.calledTwice;
expect(results).to.deep.equal([1]);

raf.tick();
expect(DateStub).to.have.been.calledThrice;
expect(results).to.deep.equal([1, 2]);

raf.tick();
expect(results).to.deep.equal([1, 2, 3]);

// Stop the animation loop
subs.unsubscribe();
});

it('should use any passed timestampProvider', () => {
const results: any[] = [];
let i = 0;
const timestampProvider = {
now: sinon.stub().callsFake(() => {
return [100, 200, 210, 300][i++];
})
};

const source$ = animationFrames(timestampProvider);

const subs = source$.subscribe({
next: ts => results.push(ts),
error: err => results.push(err),
complete: () => results.push('done'),
});

expect(DateStub).not.to.have.been.called;
expect(timestampProvider.now).to.have.been.calledOnce;
expect(results).to.deep.equal([]);

raf.tick();
expect(DateStub).not.to.have.been.called;
expect(timestampProvider.now).to.have.been.calledTwice;
expect(results).to.deep.equal([100]);

raf.tick();
expect(DateStub).not.to.have.been.called;
expect(timestampProvider.now).to.have.been.calledThrice;
expect(results).to.deep.equal([100, 110]);

raf.tick();
expect(results).to.deep.equal([100, 110, 200]);

// Stop the animation loop
subs.unsubscribe();
});

it('should compose with take', () => {
const results: any[] = [];
const source$ = animationFrames();
expect(requestAnimationFrame).not.to.have.been.called;

source$.pipe(
take(2),
).subscribe({
next: ts => results.push(ts),
error: err => results.push(err),
complete: () => results.push('done'),
});

expect(DateStub).to.have.been.calledOnce;
expect(requestAnimationFrame).to.have.been.calledOnce;

expect(results).to.deep.equal([]);

raf.tick();
expect(DateStub).to.have.been.calledTwice;
expect(requestAnimationFrame).to.have.been.calledTwice;
expect(results).to.deep.equal([1]);

raf.tick();
expect(DateStub).to.have.been.calledThrice;
// It shouldn't reschedule, because there are no more subscribers
// for the animation loop.
expect(requestAnimationFrame).to.have.been.calledTwice;
expect(results).to.deep.equal([1, 2, 'done']);

// Since there should be no more subscribers listening on the loop
// the latest animation frame should be cancelled.
expect(cancelAnimationFrame).to.have.been.calledOnce;
});

it('should compose with takeUntil', () => {
const subject = new Subject();
const results: any[] = [];
const source$ = animationFrames();
expect(requestAnimationFrame).not.to.have.been.called;

source$.pipe(
takeUntil(subject),
).subscribe({
next: ts => results.push(ts),
error: err => results.push(err),
complete: () => results.push('done'),
});

expect(DateStub).to.have.been.calledOnce;
expect(requestAnimationFrame).to.have.been.calledOnce;

expect(results).to.deep.equal([]);

raf.tick();
expect(DateStub).to.have.been.calledTwice;
expect(requestAnimationFrame).to.have.been.calledTwice;
expect(results).to.deep.equal([1]);

raf.tick();
expect(DateStub).to.have.been.calledThrice;
expect(requestAnimationFrame).to.have.been.calledThrice;
expect(results).to.deep.equal([1, 2]);
expect(cancelAnimationFrame).not.to.have.been.called;

// Complete the observable via `takeUntil`.
subject.next();
expect(cancelAnimationFrame).to.have.been.calledOnce;
expect(results).to.deep.equal([1, 2, 'done']);

raf.tick();
expect(DateStub).to.have.been.calledThrice;
expect(requestAnimationFrame).to.have.been.calledThrice;
expect(results).to.deep.equal([1, 2, 'done']);
});
});
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -4,6 +4,7 @@ export { ConnectableObservable } from './internal/observable/ConnectableObservab
export { GroupedObservable } from './internal/operators/groupBy';
export { Operator } from './internal/Operator';
export { observable } from './internal/symbol/observable';
export { animationFrames } from './internal/observable/dom/animationFrames';

/* Subjects */
export { Subject } from './internal/Subject';
Expand Down