falcor
Version:
A JavaScript library for efficient data fetching.
506 lines (479 loc) • 18.4 kB
JavaScript
const falcor = require("./../../../lib/");
const Model = falcor.Model;
const noOp = function() { };
const LocalDataSource = require("../../data/LocalDataSource");
const ErrorDataSource = require("../../data/ErrorDataSource");
const asyncifyDataSource = require("../../data/asyncifyDataSource");
const isPathValue = require("./../../../lib/support/isPathValue");
const cacheGenerator = require("./../../CacheGenerator");
const atom = require("falcor-json-graph").atom;
const MaxRetryExceededError = require("./../../../lib/errors/MaxRetryExceededError");
const strip = require("./../../cleanData").stripDerefAndVersionKeys;
const isAssertionError = require("./../../isAssertionError");
const toObservable = require("../../toObs");
describe("DataSource Only", () => {
let dataSource;
beforeEach(() => {
dataSource = new LocalDataSource(cacheGenerator(0, 2, ["title", "art"], false));
});
describe("Preload Functions", () => {
it("should get a value from falcor.", done => {
const model = new Model({ source: dataSource });
const onNext = jest.fn();
const secondOnNext = jest.fn();
toObservable(model.
preload(["videos", 0, "title"])).
doAction(onNext, noOp, () => {
expect(onNext).not.toHaveBeenCalled();
}).
defaultIfEmpty({}).
flatMap(() => {
return model.get(["videos", 0, "title"]);
}).
doAction(secondOnNext, noOp, () => {
expect(secondOnNext).toHaveBeenCalledTimes(1);
expect(strip(secondOnNext.mock.calls[0][0])).toEqual({
json: {
videos: { 0: { title: "Video 0" } }
}
});
}).
subscribe(noOp, done, done);
});
it("should perform multiple trips to a dataSource.", done => {
const get = jest.fn((source, paths) => {
if (paths.length === 0) {
paths.pop();
}
});
const model = new Model({
source: new LocalDataSource(cacheGenerator(0, 2, ["title", "art"]), { onGet: get })
});
const onNext = jest.fn();
const secondOnNext = jest.fn();
toObservable(model.
preload(["videos", 0, "title"],
["videos", 1, "art"])).
doAction(onNext).
doAction(noOp, noOp, () => {
expect(onNext).not.toHaveBeenCalled();
}).
defaultIfEmpty({}).
flatMap(() => {
return model.get(["videos", 0, "title"]);
}).
doAction(secondOnNext).
doAction(noOp, noOp, () => {
expect(secondOnNext).toHaveBeenCalledTimes(1);
expect(strip(secondOnNext.mock.calls[0][0])).toEqual({
json: { videos: { 0: { title: "Video 0" } } }
});
}).
subscribe(noOp, done, done);
});
});
describe("PathMap", () => {
it("should get a value from falcor.", done => {
const model = new Model({ source: dataSource });
const onNext = jest.fn();
toObservable(model.
get(["videos", 0, "title"])).
doAction(onNext, noOp, () => {
expect(strip(onNext.mock.calls[0][0])).toEqual({
json: { videos: { 0: { title: "Video 0" } } }
});
}).
subscribe(noOp, done, done);
});
it("should get a directly referenced value from falcor.", done => {
const cache = {
reference: {
$type: "ref",
value: ["foo", "bar"]
},
foo: {
bar: {
$type: "atom",
value: "value"
}
}
};
const model = new Model({ source: new LocalDataSource(cache) });
const onNext = jest.fn();
toObservable(model.
get(["reference", null])).
doAction(onNext, noOp, () => {
expect(strip(onNext.mock.calls[0][0])).toEqual({
json: { reference: "value" }
});
}).
subscribe(noOp, done, done);
});
});
describe("_toJSONG", () => {
it("should get a value from falcor.", done => {
const model = new Model({ source: dataSource });
const onNext = jest.fn();
toObservable(model.
get(["videos", 0, "title"]).
_toJSONG()).
doAction(onNext, noOp, () => {
expect(strip(onNext.mock.calls[0][0])).toEqual({
jsonGraph: {
videos: {
0: {
title: atom("Video 0")
}
}
},
paths: [["videos", 0, "title"]]
});
}).
subscribe(noOp, done, done);
});
});
it("should report errors from a dataSource with _treatDataSourceErrorsAsJSONGraphErrors.", done => {
const model = new Model({
_treatDataSourceErrorsAsJSONGraphErrors: true,
source: new ErrorDataSource(500, "Oops!")
});
toObservable(model.
get(["videos", 0, "title"])).
doAction(noOp, err => {
expect(err).toEqual([{
path: ["videos", 0, "title"],
value: {
message: "Oops!",
status: 500
}
}]);
}, () => {
throw new Error("On Completed was called. " +
"OnError should have been called.");
}).
subscribe(noOp, err => {
// ensure its the same error
if (Array.isArray(err) && isPathValue(err[0])) {
return done();
}
return done(err);
});
});
it("should report errors from a dataSource.", done => {
let outputError = null;
const model = new Model({
source: new ErrorDataSource(500, "Oops!")
});
toObservable(model.
get(["videos", 0, "title"])).
doAction(noOp, err => {
outputError = err;
expect(err).toEqual({
$type: "error",
value: {
message: "Oops!",
status: 500
}
});
}, () => {
throw new Error("On Completed was called. " +
"OnError should have been called.");
}).
subscribe(noOp, err => {
if (err === outputError) {
return done();
}
else {
return done(err);
}
});
});
it("should get all missing paths in a single request", done => {
let serviceCalls = 0;
const 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"
}
}
}
}
});
const model = new Model({
source: {
get(paths) {
serviceCalls++;
return cacheModel.get.apply(cacheModel, paths)._toJSONG();
}
}
});
const onNext = jest.fn();
toObservable(model.
get("lolomo.summary", "lolomo[0..2].summary")).
doAction(onNext, noOp, () => {
const data = onNext.mock.calls[0][0];
const json = data.json;
const lolomo = json.lolomo;
expect(lolomo.summary).toBeDefined();
expect(lolomo[0].summary).toBeDefined();
expect(lolomo[1].summary).toBeDefined();
expect(lolomo[2].summary).toBeDefined();
expect(serviceCalls).toBe(1);
}).
subscribe(noOp, done, done);
});
it("should be able to dispose of getRequests.", done => {
const onGet = jest.fn();
const source = new LocalDataSource(cacheGenerator(0, 2), {
onGet
});
const model = new Model({ source }).batch();
const onNext = jest.fn();
const disposable = toObservable(model.
get(["videos", 0, "title"])).
doAction(onNext, noOp, () => {
throw new Error("Should not of completed. It was disposed.");
}).
subscribe(noOp, done);
disposable.dispose();
setTimeout(() => {
try {
expect(onNext).not.toHaveBeenCalled();
expect(onGet).not.toHaveBeenCalled();
} catch (e) {
return done(e);
}
return done();
}, 200);
});
it("should ignore response-stuffed paths.", done => {
const onGet = jest.fn();
const source = new LocalDataSource(cacheGenerator(0, 2), {
onGet,
wait: 100
});
const model = new Model({ source }).batch(1);
const onNext = jest.fn();
const disposable1 = toObservable(model.
get(["videos", 0, "title"])).
doAction(onNext, noOp, () => {
throw new Error("Should not of completed. It was disposed.");
}).
subscribe(noOp, done);
toObservable(model.
get(["videos", 1, "title"])).
subscribe(noOp, done);
setTimeout(() => {
disposable1.dispose();
}, 30);
setTimeout(() => {
try {
expect(model._root.cache.videos[0]).toBeUndefined();
} catch (e) {
return done(e);
}
return done();
}, 200);
});
it("should honor response-stuffed paths with _useServerPaths == true.", done => {
const onGet = jest.fn();
const source = new LocalDataSource(cacheGenerator(0, 2), {
onGet,
wait: 100,
onResults(data) {
data.paths = [
["videos", 0, "title"],
["videos", 1, "title"]
];
}
});
const model = new Model({ source, _useServerPaths: true }).batch(1);
const onNext = jest.fn();
const disposable1 = toObservable(model.
get(["videos", 0, "title"])).
doAction(onNext, noOp, () => {
throw new Error("Should not of completed. It was disposed.");
}).
subscribe(noOp, done);
toObservable(model.
get(["videos", 1, "title"])).
subscribe(noOp, done);
setTimeout(() => {
disposable1.dispose();
}, 30);
setTimeout(() => {
try {
expect(model._root.cache.videos[0].$_absolutePath).toEqual(["videos", 0]);
} catch (e) {
return done(e);
}
return done();
}, 200);
});
it("should throw when server paths are missing and _useServerPaths == true.", done => {
const source = new LocalDataSource(cacheGenerator(0, 2));
const model = new Model({ source, _useServerPaths: true }).batch(1);
toObservable(model.
get(["videos", 0, "title"])).
subscribe(noOp, err => {
expect(err.message).toBe("Server responses must include a 'paths' field when Model._useServerPaths === true");
done();
});
});
it("should be able to dispose one of two get requests..", done => {
const onGet = jest.fn();
const source = new LocalDataSource(cacheGenerator(0, 2), {
onGet
});
const model = new Model({ source }).batch();
const onNext = jest.fn();
const disposable = toObservable(model.
get(["videos", 0, "title"])).
doAction(onNext, noOp, () => {
throw new Error("Should not of completed. It was disposed.");
}).
subscribe(noOp, done);
const onNext2 = jest.fn();
toObservable(model.
get(["videos", 0, "title"])).
doAction(onNext2).
subscribe(noOp, done);
disposable.dispose();
setTimeout(() => {
try {
expect(onNext).not.toHaveBeenCalled();
expect(onGet).toHaveBeenCalledTimes(1);
expect(onNext2).toHaveBeenCalledTimes(1);
expect(strip(onNext2.mock.calls[0][0])).toEqual({
json: {
videos: {
0: {
title: "Video 0"
}
}
}
});
} catch (e) {
return done(e);
}
return done();
}, 200);
});
it("should onError a MaxRetryExceededError when data source is sync.", done => {
const model = new Model({ source: new LocalDataSource({}) });
toObservable(model.
get(["videos", 0, "title"])).
doAction(noOp, e => {
expect(MaxRetryExceededError.is(e), "MaxRetryExceededError expected.").toBe(true);
}).
subscribe(noOp, e => {
if (isAssertionError(e)) {
return done(e);
}
return done();
}, done.bind(null, new Error("should not complete")));
});
it("should onError a MaxRetryExceededError when data source is async.", done => {
const model = new Model({ source: asyncifyDataSource(new LocalDataSource({})) });
toObservable(model.
get(["videos", 0, "title"])).
doAction(noOp, e => {
expect(MaxRetryExceededError.is(e), "MaxRetryExceededError expected.").toBe(true);
}).
subscribe(noOp, e => {
if (isAssertionError(e)) {
return done(e);
}
return done();
}, done.bind(null, new Error("should not complete")));
});
it("should return missing optimized paths with MaxRetryExceededError", done => {
const model = new Model({
source: asyncifyDataSource(new LocalDataSource({})),
cache: {
lolomo: {
0: {
$type: "ref",
value: ["videos", 1]
}
},
videos: {
0: {
title: "Revolutionary Road"
}
}
}
});
toObservable(model.
get(["lolomo", 0, "title"], "videos[0].title", "hall[0].ween")).
doAction(noOp, e => {
expect(MaxRetryExceededError.is(e), "MaxRetryExceededError expected.").toBe(true);
expect(e.missingOptimizedPaths).toEqual([
["videos", 1, "title"],
["hall", 0, "ween"]
]);
}).
subscribe(noOp, e => {
if (isAssertionError(e)) {
return done(e);
}
return done();
}, done.bind(null, new Error("should not complete")));
});
it("should throw MaxRetryExceededError after retrying said times", done => {
const onGet = jest.fn();
const model = new Model({
maxRetries: 5,
source: asyncifyDataSource(new LocalDataSource({}, {
onGet
}))
});
toObservable(model.
get("some.path")).
doAction(noOp, e => {
expect(MaxRetryExceededError.is(e), "MaxRetryExceededError expected").toBe(true);
expect(onGet).toHaveBeenCalledTimes(5);
}).
subscribe(noOp, e => {
if (isAssertionError(e)) { return done(e); }
return done();
}, done.bind(null, new Error("should not complete")));
});
it("passes the attempt count to the DataSource", () => {
const onGet = jest.fn();
const model = new Model({
source: asyncifyDataSource(new LocalDataSource({}, { onGet }))
});
const path = ["some", "path"];
return model.
get(path).
then(() => {
throw new Error("should have rejected with MaxRetryExceededError");
}, e => {
expect(e).toBeInstanceOf(MaxRetryExceededError);
expect(onGet).toHaveBeenCalledTimes(3);
expect(onGet).toHaveBeenNthCalledWith(1, expect.anything(), [path], 1);
expect(onGet).toHaveBeenNthCalledWith(2, expect.anything(), [path], 2);
expect(onGet).toHaveBeenNthCalledWith(3, expect.anything(), [path], 3);
});
});
});