falcor
Version:
A JavaScript library for efficient data fetching.
558 lines (489 loc) • 18.6 kB
JavaScript
const Rx = require("rx");
const testRunner = require("./testRunner");
const $ref = require("./../lib/types/ref");
const $error = require("./../lib/types/error");
const $atom = require("./../lib/types/atom");
const rxjs = require("rxjs");
const falcor = require("./../lib/");
const Model = falcor.Model;
function ResponseObservable(response) {
this.response = response;
}
ResponseObservable.prototype = Object.create(Rx.Observable.prototype);
ResponseObservable.prototype._subscribe = function (observer) {
return this.response.subscribe(observer);
};
ResponseObservable.prototype._toJSONG = function () {
return new ResponseObservable(
this.response._toJSONG.apply(this.response, arguments)
);
};
ResponseObservable.prototype.progressively = function () {
return new ResponseObservable(
this.response.progressively.apply(this.response, arguments)
);
};
ResponseObservable.prototype.then = function () {
return this.response.then.apply(this.response, arguments);
};
ResponseObservable.prototype[Symbol.observable] = function () {
return this.response[Symbol.observable].apply(this.response, arguments);
};
const modelGet = Model.prototype.get;
const modelSet = Model.prototype.set;
const modelCall = Model.prototype.call;
const modelPreload = Model.prototype.preload;
Model.prototype.get = function () {
return new ResponseObservable(modelGet.apply(this, arguments));
};
Model.prototype.set = function () {
return new ResponseObservable(modelSet.apply(this, arguments));
};
Model.prototype.preload = function () {
return new ResponseObservable(modelPreload.apply(this, arguments));
};
Model.prototype.call = function () {
return new ResponseObservable(modelCall.apply(this, arguments));
};
describe("Model", () => {
it("should construct a new Model", () => {
new Model();
});
it("should construct a new Model when calling the falcor module function", () => {
expect(falcor() instanceof falcor.Model).toBe(true);
});
it("should have access to static helper methods.", () => {
const ref = ["a", "b", "c"];
const err = { ohhh: "no!" };
let out = Model.ref(ref);
testRunner.compare({ $type: $ref, value: ref }, out);
out = Model.ref("a.b.c");
testRunner.compare({ $type: $ref, value: ref }, out);
out = Model.error(err);
testRunner.compare({ $type: $error, value: err }, out);
out = Model.atom(1337);
testRunner.compare({ $type: $atom, value: 1337 }, out);
});
it("unsubscribing should cancel DataSource request.", (done) => {
let onNextCalled = 0,
onErrorCalled = 0,
onCompletedCalled = 0,
unusubscribeCalled = 0,
dataSourceGetCalled = 0;
const model = new Model({
cache: {
list: {
0: { name: "test" },
},
},
source: {
get() {
return {
subscribe(observerOrOnNext, onError, onCompleted) {
dataSourceGetCalled++;
const handle = setTimeout(() => {
const response = {
jsonGraph: {
list: {
1: { name: "another test" },
},
},
paths: ["list", 1, "name"],
};
if (typeof observerOrOnNext === "function") {
observerOrOnNext(response);
onCompleted();
} else {
observerOrOnNext.onNext(response);
observerOrOnNext.onCompleted();
}
});
return {
dispose() {
unusubscribeCalled++;
clearTimeout(handle);
},
};
},
};
},
},
});
const subscription = model.get("list[0,1].name").subscribe(
(value) => {
onNextCalled++;
},
(error) => {
onErrorCalled++;
},
() => {
onCompletedCalled++;
}
);
subscription.dispose();
if (
dataSourceGetCalled === 1 &&
!onNextCalled &&
unusubscribeCalled === 1 &&
!onErrorCalled &&
!onCompletedCalled
) {
done();
} else {
done(new Error("DataSource unsubscribe not called."));
}
});
it("unsubscribing should dispose batched DataSource request.", (done) => {
let onNextCalled = 0,
onErrorCalled = 0,
onCompletedCalled = 0,
unusubscribeCalled = 0,
dataSourceGetCalled = 0;
let onDataSourceGet, onDisposedOrCompleted;
let model = new Model({
cache: {
list: {
0: { name: "test" },
},
},
source: {
get() {
return {
subscribe(observerOrOnNext, onError, onCompleted) {
dataSourceGetCalled++;
const handle = setTimeout(() => {
const response = {
jsonGraph: {
list: {
1: { name: "another test" },
},
},
paths: ["list", 1, "name"],
};
onDataSourceGet && onDataSourceGet();
if (typeof observerOrOnNext === "function") {
observerOrOnNext(response);
onCompleted();
} else {
observerOrOnNext.onNext(response);
observerOrOnNext.onCompleted();
}
onDisposedOrCompleted &&
onDisposedOrCompleted();
});
return {
dispose() {
unusubscribeCalled++;
clearTimeout(handle);
},
};
},
};
},
},
});
model = model.batch();
const subscription = model.get("list[0,1].name").subscribe(
(value) => {
onNextCalled++;
},
(error) => {
onErrorCalled++;
},
() => {
onCompletedCalled++;
}
);
onDataSourceGet = function () {
subscription.dispose();
};
onDisposedOrCompleted = function () {
if (
dataSourceGetCalled === 1 &&
!onNextCalled &&
unusubscribeCalled === 1 &&
!onErrorCalled &&
!onCompletedCalled
) {
done();
} else {
done(new Error("DataSource dispose not called."));
}
};
});
it('unsubscribing should "unsubscribe" batched DataSource request, if applicable.', (done) => {
let onNextCalled = 0,
onErrorCalled = 0,
onCompletedCalled = 0,
unusubscribeCalled = 0,
dataSourceGetCalled = 0;
let onDataSourceGet, onDisposedOrCompleted;
let model = new Model({
cache: {
list: {
0: { name: "test" },
},
},
source: {
get() {
return {
subscribe(observerOrOnNext, onError, onCompleted) {
dataSourceGetCalled++;
const handle = setTimeout(() => {
const response = {
jsonGraph: {
list: {
1: { name: "another test" },
},
},
paths: ["list", 1, "name"],
};
onDataSourceGet && onDataSourceGet();
if (typeof observerOrOnNext === "function") {
observerOrOnNext(response);
onCompleted();
} else {
observerOrOnNext.onNext(response);
observerOrOnNext.onCompleted();
}
onDisposedOrCompleted &&
onDisposedOrCompleted();
});
return {
unsubscribe() {
unusubscribeCalled++;
clearTimeout(handle);
},
};
},
};
},
},
});
model = model.batch();
const subscription = model.get("list[0,1].name").subscribe(
(value) => {
onNextCalled++;
},
(error) => {
onErrorCalled++;
},
() => {
onCompletedCalled++;
}
);
onDataSourceGet = function () {
subscription.dispose();
};
onDisposedOrCompleted = function () {
if (
dataSourceGetCalled === 1 &&
!onNextCalled &&
unusubscribeCalled === 1 &&
!onErrorCalled &&
!onCompletedCalled
) {
done();
} else {
done(new Error("DataSource unsubscribe not called."));
}
};
});
it("Supports RxJS 5.", (done) => {
let onNextCalled = 0,
onErrorCalled = 0,
onCompletedCalled = 0,
unusubscribeCalled = 0,
dataSourceGetCalled = 0;
const model = new Model({
cache: {
list: {
0: { name: "test" },
},
},
source: {
get() {
return {
subscribe(observerOrOnNext, onError, onCompleted) {
dataSourceGetCalled++;
const handle = setTimeout(() => {
const response = {
jsonGraph: {
list: {
1: { name: "another test" },
},
},
paths: ["list", 1, "name"],
};
if (typeof observerOrOnNext === "function") {
observerOrOnNext(response);
onCompleted();
} else {
observerOrOnNext.onNext(response);
observerOrOnNext.onCompleted();
}
});
return {
dispose() {
unusubscribeCalled++;
clearTimeout(handle);
},
};
},
};
},
},
});
const subscription = rxjs.Observable.from(
model.get("list[0,1].name")
).subscribe(
(value) => {
onNextCalled++;
},
(error) => {
onErrorCalled++;
},
() => {
onCompletedCalled++;
}
);
subscription.unsubscribe();
if (
dataSourceGetCalled === 1 &&
!onNextCalled &&
unusubscribeCalled === 1 &&
!onErrorCalled &&
!onCompletedCalled
) {
done();
} else {
done(new Error("DataSource unsubscribe not called."));
}
});
it("setMaxSize to a lower value forces a collect", () => {
const model = new Model({
cache: {
list: {
0: { name: "test" },
},
},
});
const cache = model._root.cache;
expect(cache.$size).toBeGreaterThan(0);
model._setMaxSize(0);
expect(cache.$size).toBe(0);
});
// https://github.com/Netflix/falcor/issues/915
it("maxRetries option is carried over to cloned Model instance", () => {
const model = new Model({
maxRetries: 10,
});
expect(model._maxRetries).toBe(10);
const batchingModel = model.batch(100);
expect(batchingModel._maxRetries).toBe(10);
});
it("cloned instance should retain custom type", () => {
function MyModel() {
Model.call(this);
}
MyModel.prototype = new Model();
MyModel.prototype.constructor = MyModel;
const model = new MyModel();
expect(model).toBeInstanceOf(MyModel);
const clonedModel = model.batch(100);
expect(clonedModel).toBeInstanceOf(MyModel);
});
describe("algorithm options", () => {
const notTrue = [false, 0, -1, 1, "no", ""];
describe("path collapse algorithm", () => {
it("accepts boolean to disable", () => {
let model = new Model({ disablePathCollapse: true });
expect(model._enablePathCollapse).toBe(false);
for (let index = 0; index < notTrue.length; index++) {
model = new Model({ disablePathCollapse: notTrue[index] });
expect(model._enablePathCollapse).toBe(true);
}
});
it("is enabled by default", () => {
let model = new Model();
expect(model._enablePathCollapse).toBe(true);
model = new Model({});
expect(model._enablePathCollapse).toBe(true);
});
it("is copied on clone", () => {
const model = new Model({ disablePathCollapse: true });
const clone = model._clone();
expect(clone._enablePathCollapse).toBe(
model._enablePathCollapse
);
});
});
describe("request deduplication algorithm", () => {
it("accepts boolean to disable", () => {
let model = new Model({ disableRequestDeduplication: true });
expect(model._enableRequestDeduplication).toBe(false);
for (let index = 0; index < notTrue.length; index++) {
model = new Model({
disableRequestDeduplication: notTrue[index],
});
expect(model._enableRequestDeduplication).toBe(true);
}
});
it("is enabled by default", () => {
let model = new Model();
expect(model._enableRequestDeduplication).toBe(true);
model = new Model({});
expect(model._enableRequestDeduplication).toBe(true);
});
it("is copied on clone", () => {
const model = new Model({ disableRequestDeduplication: true });
const clone = model._clone();
expect(clone._enableRequestDeduplication).toBe(
model._enableRequestDeduplication
);
});
});
});
describe("set and get", () => {
it("should throw an error on invalid input when calling Model:get", (done) => {
const model = new Model();
model.get('{"foobar":[]}').subscribe(
() => {},
(e) => {
expect(e).toBeInstanceOf(Error);
expect(e.message).toMatchInlineSnapshot(
`"Path syntax validation error -- Unexpected token. -- {\\""`
);
done();
},
() => {
done(
new Error(
"Did not receive an error when one was expected"
)
);
}
);
});
it("should throw an error on invalid input when calling Model:set", (done) => {
const model = new Model();
model.set({ path: '{"foobar":[]}' }).subscribe(
() => {},
(e) => {
expect(e).toBeInstanceOf(Error);
expect(e.message).toMatchInlineSnapshot(
`"Path syntax validation error -- Unexpected token. -- {\\""`
);
done();
},
() => {
done(
new Error(
"Did not receive an error when one was expected"
)
);
}
);
});
});
});