@graphistry/falcor
Version:
A JavaScript library for efficient data fetching.
334 lines (316 loc) • 12.7 kB
JavaScript
var falcor = require('./../../../falcor.js');
var Model = falcor.Model;
var Observable = require('rx').Observable;
var noOp = function() {};
var LocalDataSource = require('../../data/LocalDataSource');
var ErrorDataSource = require('../../data/ErrorDataSource');
var isPathValue = require('./../../../lib/support/isPathValue');
var expect = require('chai').expect;
var sinon = require('sinon');
var TestCache = require('./../../data/Cache');
var cacheGenerator = require('./../../CacheGenerator');
var atom = require('@graphistry/falcor-json-graph').atom;
var MaxRetryExceededError = require('./../../../lib/errors/MaxRetryExceededError');
var strip = require('./../../cleanData').stripDerefAndVersionKeys;
describe('DataSource Only', function() {
var dataSource = new LocalDataSource(cacheGenerator(0, 2, ['title', 'art'], false));
it('should get all missing paths in a single request', function(done) {
var serviceCalls = 0;
var cacheModel = new Model({
cache: {
lolomo: {
summary: {
$type: 'atom',
value: 'hello'
},
0: {
summary: {
$type: 'atom',
value: 'hello-0'
}
},
1: {
summary: {
$type: 'atom',
value: 'hello-1'
}
},
2: {
summary: {
$type: 'atom',
value: 'hello-2'
}
}
}
}
});
var model = new Model({ source: {
get: function(paths) {
serviceCalls++;
return cacheModel.get.apply(cacheModel, paths)._toJSONG();
}
}});
var onNext = sinon.spy();
toObservable(model.
get('lolomo.summary', 'lolomo[0..2].summary')).
doAction(onNext, noOp, function() {
var data = onNext.getCall(0).args[0];
var json = data.json;
var lolomo = json.lolomo;
expect(lolomo.summary).to.be.ok;
expect(lolomo[0].summary).to.be.ok;
expect(lolomo[1].summary).to.be.ok;
expect(lolomo[2].summary).to.be.ok;
expect(serviceCalls).to.equal(1);
}).
subscribe(noOp, done, done);
});
it('should report errors from a dataSource.', function(done) {
var model = new Model({
source: new ErrorDataSource(500, 'Oops!')
});
toObservable(model.
get(['videos', 0, 'title'])).
doAction(noOp, function(err) {
expect(err).to.deep.equals([{
path: ['videos', 0, 'title'],
value: {
message: 'Oops!',
status: 500
}
}]);
}, function() {
throw new Error('On Completed was called. ' +
'OnError should have been called.');
}).
subscribe(noOp, function(err) {
// ensure its the same error
if (Array.isArray(err) && isPathValue(err[0])) {
return done();
}
return done(err);
});
});
it('should be able to dispose of getRequests.', function(done) {
var onGet = sinon.spy();
var source = new LocalDataSource(cacheGenerator(0, 2), {
onGet: onGet
});
var model = new Model({source: source}).batch();
var onNext = sinon.spy();
var disposable = toObservable(model.
get(['videos', 0, 'title'])).
doAction(onNext, noOp, function() {
throw new Error('Should not have completed.');
}).
subscribe(noOp, done);
disposable.dispose();
setTimeout(function() {
try {
expect(onNext.callCount).to.equal(0);
expect(onGet.callCount).to.equal(0);
} catch(e) {
return done(e);
}
return done();
}, 10);
});
it('should be able to dispose one of two get requests..', function(done) {
var onGet = sinon.spy();
var source = new LocalDataSource(cacheGenerator(0, 2), {
onGet: onGet
});
var model = new Model({source: source}).batch();
var onNext = sinon.spy();
var disposable = toObservable(model.
get(['videos', 0, 'title'])).
doAction(onNext, noOp, function() {
throw new Error('Should not have completed.');
}).
subscribe(noOp, done);
var onNext2 = sinon.spy();
toObservable(model.
get(['videos', 0, 'title'])).
doAction(onNext2).
subscribe(noOp, done);
disposable.dispose();
setTimeout(function() {
try {
expect(onNext.callCount).to.equal(0);
expect(onGet.callCount).to.equal(1);
expect(onNext2.calledOnce, 'onNext2 should have been called').to.be.ok;
expect(strip(onNext2.getCall(0).args[0])).to.deep.equals({
json: {
videos: {
0: {
title: 'Video 0'
}
}
}
});
} catch(e) {
return done(e);
}
return done();
}, 100);
});
it('should throw a MaxRetryExceededError.', function(done) {
var model = new Model({ source: new LocalDataSource({}) });
toObservable(model.
get(['videos', 0, 'title'])).
doAction(noOp, function(e) {
expect(MaxRetryExceededError.is(e), 'MaxRetryExceededError expected.').to.be.ok;
}).
subscribe(noOp, function(e) {
if (isAssertionError(e)) {
return done(e);
}
return done();
}, done.bind('should not complete'));
});
describe('Preload Functions', function() {
it('should get a value from falcor.', function(done) {
var model = new Model({source: dataSource});
var onNext = sinon.spy();
var secondOnNext = sinon.spy();
toObservable(model.
preload(['videos', 0, 'title'])).
doAction(onNext, noOp, function() {
expect(onNext.callCount).to.equal(0);
}).
defaultIfEmpty({}).
flatMap(function() {
return model.get(['videos', 0, 'title']);
}).
doAction(secondOnNext, noOp, function() {
expect(secondOnNext.calledOnce).to.be.ok;
expect(strip(secondOnNext.getCall(0).args[0])).to.deep.equals({
json: {
videos: {0: {title: 'Video 0'}}}
});
}).
subscribe(noOp, done, done);
});
it('should perform multiple trips to a dataSource.', function(done) {
var datasourceGetCount = 0;
var get = sinon.spy(function(source, paths) {
datasourceGetCount++;
if (paths.length === 2) {
paths.pop();
}
});
var model = new Model({
source: new LocalDataSource(cacheGenerator(0, 2, ['title', 'art']), {onGet: get})
});
var onNext = sinon.spy();
var secondOnNext = sinon.spy();
toObservable(model.
preload(['videos', 0, 'title'], ['videos', 1, 'art'])).
doAction(onNext).
doAction(noOp, noOp, function() {
expect(onNext.callCount).to.equal(0);
expect(datasourceGetCount).to.equal(2);
}).
defaultIfEmpty({}).
flatMap(function() {
return model.get(['videos', 0, 'title']);
}).
doAction(secondOnNext).
doAction(noOp, noOp, function() {
expect(datasourceGetCount).to.equal(2);
expect(secondOnNext.calledOnce).to.be.ok;
expect(strip(secondOnNext.getCall(0).args[0])).to.deep.equals({
json: {videos: {0: {title: 'Video 0'}}}
});
}).
subscribe(noOp, done, done);
});
});
describe('JSON', function() {
it('should get a value from falcor.', function(done) {
var model = new Model({source: dataSource});
var onNext = sinon.spy();
toObservable(model.
get(['videos', 0, 'title'])).
doAction(onNext, noOp, function() {
expect(strip(onNext.getCall(0).args[0])).to.deep.equals({
json: {videos: {0: {title: 'Video 0'}}}
});
}).
subscribe(noOp, done, done);
});
it('should persist immediately expired values until the next transaction.', function(done) {
var datasourceGetCount = 0;
var model = new Model({
source: new LocalDataSource(TestCache(), {
onGet: function() {
datasourceGetCount++;
}
})
});
var getObs = toObservable(model.
get(['genreList', 'expires-now', [0, 1], 'summary']));
Observable.concat(
Observable.defer(function() {
var onNextSpy = sinon.spy();
return getObs.doAction(onNextSpy, noOp, validateOnCompleted(1, onNextSpy))
}),
Observable.defer(function() {
var onNextSpy = sinon.spy();
return getObs.doAction(onNextSpy, noOp, validateOnCompleted(2, onNextSpy))
})
).
subscribe(noOp, done, done);
function validateOnCompleted(expectedGetCount, onNextSpy) {
return function() {
expect(datasourceGetCount).to.equal(expectedGetCount);
expect(strip(onNextSpy.getCall(0).args[0])).to.deep.equals({
json: {
genreList: {
'expires-now': {
0: {
summary: {
'title': 'House of Cards',
'url': '/movies/1234'
}
},
1: {
summary: {
'title': 'Terminator 3',
'url': '/movies/766'
}
}
}
}
}
});
};
}
});
});
describe('_toJSONG', function() {
it('should get a value from falcor.', function(done) {
var model = new Model({source: dataSource});
var onNext = sinon.spy();
toObservable(model.
get(['videos', 0, 'title']).
_toJSONG()).
doAction(onNext, noOp, function() {
expect(strip(onNext.getCall(0).args[0])).to.deep.equals({
jsonGraph: {
videos: {
0: {
title: atom('Video 0')
}
}
},
paths: [['videos', 0, 'title']]
});
}).
subscribe(noOp, done, done);
});
});
});
function isAssertionError(e) {
return e.hasOwnProperty('expected') && e.hasOwnProperty('actual');
}