241 lines
5.1 KiB
JavaScript
241 lines
5.1 KiB
JavaScript
import { prop, hashableProperty, computed, hashableComputed } from '../src/index.js';
|
|
import { dirtyMock, hashSet } from '../src/testUtil.js';
|
|
|
|
describe('A computed', () => {
|
|
const add = (a, b) => a + b;
|
|
const square = a => a * a;
|
|
const setProp = hashableProperty(hashSet);
|
|
const computedSet = hashableComputed(hashSet);
|
|
|
|
it('returns the value computed from its dependencies', () => {
|
|
const a = prop(0);
|
|
const b = computed(square, [a]);
|
|
const c = computed(add, [a, b]);
|
|
|
|
expect(b()).toEqual(0);
|
|
expect(c()).toEqual(0);
|
|
|
|
a(1);
|
|
expect(b()).toEqual(1);
|
|
expect(c()).toEqual(2);
|
|
|
|
a(2);
|
|
expect(b()).toEqual(4);
|
|
expect(c()).toEqual(6);
|
|
|
|
a(3);
|
|
expect(b()).toEqual(9);
|
|
expect(c()).toEqual(12);
|
|
});
|
|
|
|
it('only computes when called', () => {
|
|
let runCount = 0;
|
|
let currentValue = 1;
|
|
const a = prop(0);
|
|
const b = computed(
|
|
val => {
|
|
runCount += 1;
|
|
expect(val).toEqual(currentValue);
|
|
return val * val;
|
|
},
|
|
[a]
|
|
);
|
|
|
|
a(1);
|
|
expect(runCount).toEqual(0);
|
|
// b evaluates
|
|
expect(b()).toEqual(1);
|
|
expect(runCount).toEqual(1);
|
|
// b does not evaluate
|
|
expect(b()).toEqual(1);
|
|
expect(runCount).toEqual(1);
|
|
currentValue = 3;
|
|
// b does not evaluate
|
|
a(3);
|
|
expect(runCount).toEqual(1);
|
|
// b evaluates
|
|
expect(b()).toEqual(9);
|
|
expect(runCount).toEqual(2);
|
|
});
|
|
|
|
it('computes automatically when subscribed', () => {
|
|
let runCount = 0;
|
|
let subRunCount = 0;
|
|
let currentValue = 1;
|
|
const a = prop(0);
|
|
const b = computed(
|
|
val => {
|
|
runCount += 1;
|
|
expect(val).toEqual(currentValue);
|
|
return val * val;
|
|
},
|
|
[a]
|
|
);
|
|
|
|
// b does not evaluate
|
|
a(1);
|
|
expect(runCount).toEqual(0);
|
|
// b evaluates
|
|
expect(b()).toEqual(1);
|
|
expect(runCount).toEqual(1);
|
|
// b does not evaluate
|
|
expect(b()).toEqual(1);
|
|
expect(runCount).toEqual(1);
|
|
|
|
const cancelSubscription = b.subscribe(val => {
|
|
subRunCount += 1;
|
|
expect(val).toEqual(currentValue * currentValue);
|
|
});
|
|
|
|
currentValue = 3;
|
|
// b evaluates
|
|
a(3);
|
|
expect(runCount).toEqual(2);
|
|
expect(subRunCount).toEqual(1);
|
|
// b does not evaluate
|
|
expect(b()).toEqual(9);
|
|
expect(runCount).toEqual(2);
|
|
expect(subRunCount).toEqual(1);
|
|
});
|
|
|
|
it('honors cancelled subscriptions', () => {
|
|
let runCount = 0;
|
|
let subRunCount = 0;
|
|
let currentValue = 1;
|
|
const a = prop(0);
|
|
const b = computed(
|
|
val => {
|
|
runCount += 1;
|
|
expect(val).toEqual(currentValue);
|
|
return val * val;
|
|
},
|
|
[a]
|
|
);
|
|
const cancelSubscription = b.subscribe(val => {
|
|
subRunCount += 1;
|
|
expect(val).toEqual(currentValue * currentValue);
|
|
});
|
|
|
|
const cancelSubscription2 = b.subscribe(val => {
|
|
subRunCount += 1;
|
|
});
|
|
|
|
// b evaluates
|
|
a(1);
|
|
expect(runCount).toEqual(1);
|
|
expect(subRunCount).toEqual(2);
|
|
// b does not evaluate
|
|
expect(b()).toEqual(1);
|
|
expect(runCount).toEqual(1);
|
|
expect(subRunCount).toEqual(2);
|
|
|
|
expect(cancelSubscription()).toEqual(1);
|
|
|
|
currentValue = 3;
|
|
// b evaluates
|
|
a(3);
|
|
expect(runCount).toEqual(2);
|
|
expect(subRunCount).toEqual(3);
|
|
// b does not evaluate
|
|
expect(b()).toEqual(9);
|
|
expect(runCount).toEqual(2);
|
|
expect(subRunCount).toEqual(3);
|
|
|
|
expect(cancelSubscription2()).toEqual(0);
|
|
});
|
|
|
|
it('can be detached', () => {
|
|
const a = prop(2);
|
|
const b = computed(square, [a]);
|
|
const c = computed(add, [a, b]);
|
|
|
|
expect(b()).toEqual(4);
|
|
expect(c()).toEqual(6);
|
|
|
|
b.detach();
|
|
|
|
a(3);
|
|
expect(b()).toEqual(4);
|
|
expect(c()).toEqual(7);
|
|
});
|
|
|
|
it('uses a hash function', () => {
|
|
let runCount = 0;
|
|
|
|
function intersection(a, b) {
|
|
runCount += 1;
|
|
return new Set([...a].filter(x => b.has(x)));
|
|
}
|
|
|
|
const a = setProp(new Set([1, 2]));
|
|
const b = setProp(new Set([2, 3]));
|
|
const ABintersection = computedSet(intersection, [a, b]);
|
|
|
|
expect(runCount).toEqual(0);
|
|
expect([...ABintersection()]).toEqual([2]);
|
|
expect(runCount).toEqual(1);
|
|
b(new Set([3, 2]));
|
|
expect([...ABintersection()]).toEqual([2]);
|
|
expect(runCount).toEqual(1);
|
|
b(new Set([3, 2, 1]));
|
|
expect(runCount).toEqual(1);
|
|
expect([...ABintersection()]).toEqual([1, 2]);
|
|
expect(runCount).toEqual(2);
|
|
});
|
|
|
|
it('flags all subscribers as dirty before propagating change', () => {
|
|
function intersection(a, b) {
|
|
return new Set([...a].filter(x => b.has(x)));
|
|
}
|
|
|
|
const a = setProp(new Set([1, 2]));
|
|
const b = setProp(new Set([2, 3]));
|
|
const ABintersection = computedSet(intersection, [a, b]);
|
|
|
|
const [dirtyA, dirtyB, checker] = dirtyMock(2);
|
|
|
|
ABintersection.subscribe(dirtyA.setDirty);
|
|
ABintersection.subscribe(dirtyB.setDirty);
|
|
|
|
a(new Set([3, 4]));
|
|
|
|
expect(checker()).toBe(true);
|
|
});
|
|
|
|
it('locks subscribers to delay propagating out-of-band changes', () => {
|
|
let aCount = 0;
|
|
|
|
const a = prop(1);
|
|
a.subscribe(val => {
|
|
aCount = val;
|
|
});
|
|
|
|
const aDep = computed(
|
|
() => {
|
|
expect(aCount).toBe(3);
|
|
a(2);
|
|
expect(aCount).toBe(3);
|
|
},
|
|
[a]
|
|
);
|
|
|
|
expect(aCount).toBe(0);
|
|
a(3);
|
|
expect(aCount).toBe(3);
|
|
aDep();
|
|
expect(aCount).toBe(2);
|
|
});
|
|
|
|
it('calls subscriptions in order', () => {
|
|
let order = '';
|
|
|
|
const a = prop(null);
|
|
const b = computed(a => a, [a]);
|
|
b.subscribe(() => (order += 'a'));
|
|
b.subscribe(() => (order += 'b'));
|
|
b.subscribe(() => (order += 'c'));
|
|
a(1);
|
|
expect(order).toEqual('abc');
|
|
});
|
|
});
|