UNPKG

@efflore/ui-element

Version:

UIElement - minimal reactive framework based on Web Components

526 lines (450 loc) 18.2 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Cause & Effect Test</title> </head> <body> <script type="module"> import { runTests } from '@web/test-runner-mocha'; import { assert } from '@esm-bundle/chai'; import { cause, derive, effect } from '../cause-effect.js'; const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); const paint = () => new Promise(requestAnimationFrame); runTests(() => { describe('Cause', function () { describe('Empty cause', function () { it('should be undefined by default', function () { const state = cause(); assert.isUndefined(state(), 'no initial value for state defined'); }); }); describe('Boolean cause', function () { it('should be boolean', function () { const state = cause(false); assert.isBoolean(state(), 'should be boolean'); }); it('should set initial value to false', function () { const state = cause(false); assert.isFalse(state(), 'should have initial value false'); }); it('should set initial value to true', function () { const state = cause(true); assert.isTrue(state(), 'should have initial value true'); }); it('should set new value with .set(true)', function () { const state = cause(false); state.set(true); assert.isTrue(state(), 'should be true with any initial value'); }); it('should toggle initial value with .set(v => !v)', function () { const state = cause(false); state.set(v => !v); assert.isTrue(state(), 'should be true with initial value false'); }); }); describe('Number cause', function () { it('should be number', function () { const state = cause(0); assert.isNumber(state(), 'should be number'); }); it('should set initial value to 0', function () { const state = cause(0); assert.equal(state(), 0, 'should have initial value 0'); }); it('should set new value with .set(42)', function () { const state = cause(0); state.set(42); assert.equal(state(), 42, 'should be 42 with any initial value'); }); it('should increment value with .set(v => ++v)', function () { const state = cause(0); state.set(v => ++v); assert.equal(state(), 1, 'should be 1 with initial value 0'); }); }); describe('String cause', function () { it('should be string', function () { const state = cause('foo'); assert.isString(state(), 'should be string'); }); it('should set initial value to "foo"', function () { const state = cause('foo'); assert.equal(state(), 'foo', 'should have initial value "foo"'); }); it('should set new value with .set("bar")', function () { const state = cause('foo'); state.set('bar'); assert.equal(state(), 'bar', 'should be "bar" with any initial value'); }); it('should upper case value with .set(v => v.toUpperCase())', function () { const state = cause('foo'); state.set(v => v.toUpperCase()); assert.equal(state(), "FOO", 'should be "FOO" with initial value "foo"'); }); }); describe('Function cause', function () { it('should be a function', function () { const x = 42; const state = cause(() => x * 2); assert.isFunction(state(), 'should be function'); }); it('should be result of function', function () { const x = 42; const state = cause(() => x * 2); assert.equal(state().call(), 84, 'should be 84 with initial value () => x * 2 and x = 42'); }); it('should be result of async function after promise is resolved', async function () { const x = 42; const state = cause(() => { new Promise(resolve => setTimeout(() => resolve(state.set(x * 2)), 100)); return; }); assert.isUndefined(state().call(), 'should be undefined while promise is pending with promise resolver () => x * 2 and x = 42'); await wait(100); assert.equal(state(), 84, 'should be 84 after a delay with resolved promise for () => x * 2 and x = 42'); }); it('should set error state in async function after promise is rejected', async function () { const x = 42; const error = cause(); const state = cause(() => { new Promise((resolve, reject) => setTimeout(() => reject('error occurred')), 100).catch(reason => error.set(reason)); return; }); assert.isUndefined(state().call(), 'should be undefined while promise is pending'); await wait(100); assert.equal(error(), 'error occurred', 'should set error message after a delay with rejected promise'); }); it('should be result of function dependent on another signal', function () { const x = cause(42); const state = cause(() => x() * 2); assert.equal(state().call(), 84, 'should be 84 with initial value () => x * 2 and x() = 42'); }); it('should be result of function dependent on a signal changed after declaration', function () { const x = cause(42); const state = cause(() => x() * 2); x.set(24); assert.equal(state().call(), 48, 'should be 48 with initial value () => x * 2 and x() = 24'); }); it('should set new value with .set(() => x / 2)', function () { const x = 42; const state = cause(() => x * 2); state.set(() => x / 2); assert.equal(state().call(), 21, 'should be 21 with any initial value and x = 21'); }); it('should upper case value with v => v.toUpperCase()', function () { const x = 'foo'; const state = cause(() => x + 'bar'); state.set(v => () => v().toUpperCase()); assert.equal(state().call(), 'FOOBAR', 'should be "FOOBAR" with initial value () => x + "bar" and x = "foo"'); }); }); describe('Array cause', function () { it('should be array', function () { const state = cause([1, 2, 3]); assert.isArray(state(), 'should be array'); }); it('should set initial value to [1, 2, 3]', function () { const state = cause([1, 2, 3]); assert.deepEqual(state(), [1, 2, 3], 'should be [1, 2, 3] with initial value [1, 2, 3]'); }); it('should set new value with .set([4, 5, 6])', function () { const state = cause([1, 2, 3]); state.set([4, 5, 6]); assert.deepEqual(state(), [4, 5, 6], 'should be [4, 5, 6] with any inital value'); }); it('should reflect current value of array after modification', function () { const array = [1, 2, 3]; const state = cause(array); array.push(4); // don't do this! the result will be correct, but we can't trigger effects assert.deepEqual(state(), [1, 2, 3, 4], 'should be [1, 2, 3, 4] with initial value [1, 2, 3]'); }); it('should set new value with .set([...array, 4])', function () { const array = [1, 2, 3]; const state = cause(array); state.set([...array, 4]); // use destructuring instead! assert.deepEqual(state(), [1, 2, 3, 4], 'should be [1, 2, 3, 4] with initial value [1, 2, 3]'); }); }); describe('Object cause', function () { it('should be object', function () { const state = cause({ a: 'a', b: 1 }); assert.isObject(state(), 'should be object'); }); it('should set initial value to { a: "a", b: 1 }', function () { const state = cause({ a: 'a', b: 1 }); assert.deepEqual(state(), { a: 'a', b: 1 }, 'should be { a: "a", b: 1 } with initial value { a: "a", b: 1 }'); }); it('should set new value with .set({ c: true })', function () { const state = cause({ a: 'a', b: 1 }); state.set({ c: true }); assert.deepEqual(state(), { c: true }, 'should be { c: true } with any inital value'); }); it('should reflect current value of object after modification', function () { const obj = { a: 'a', b: 1 }; const state = cause(obj); obj.c = true; // don't do this! the result will be correct, but we can't trigger effects assert.deepEqual(state(), { a: 'a', b: 1, c: true }, 'should be { a: "a", b: 1, c: true } with initial value { a: "a", b: 1 }'); }); it('should set new value with .set({...obj, c: true})', function () { const obj = { a: 'a', b: 1 }; const state = cause(obj); state.set({...obj, c: true}); // use destructuring instead! assert.deepEqual(state(), { a: 'a', b: 1, c: true }, 'should be { a: "a", b: 1, c: true } with initial value { a: "a", b: 1 }'); }); }); }); describe('Derive', function () { it('should compute a function', function() { const computed = derive(() => 1 + 2); assert.equal(computed(), 3); }); it('should compute function dependent on a signal', function() { const state = cause(42); const computed = derive(() => 1 + state()); assert.equal(computed(), 43); }); /* it('should be added to state.effects', function () { const state = cause(); const computed = derive(() => 1 + state()); effect(() => computed()); assert.equal(state.effects.size, 1); const computed2 = derive(() => 2 + state()); effect(() => computed2()); assert.equal(state.effects.size, 2); effect(() => computed() + computed2()); assert.equal(state.effects.size, 2); }); */ it('should compute function dependent on an updated signal', function() { const state = cause(42); const computed = derive(() => 1 + state()); state.set(24); assert.equal(computed(), 25); }); it('should compute function dependent on an async signal', async function() { const status = cause('unset'); const state = cause(() => { new Promise(resolve => { status.set('pending'); setTimeout(() => resolve(state.set(42)), 100); }).then(() => status.set('success')); return undefined; }); const computed = derive(() => { const value = state() return typeof value === 'function' ? value.call() : value + 1 }); assert.isUndefined(computed(), 'should be undefined while pending'); assert.equal(status(), 'pending'); await wait(100); assert.equal(computed(), 43, 'should be 43 after promise is resolved'); assert.equal(status(), 'success'); }); it('should handle errors from an async signal gracefully', async function() { const status = cause('unset'); const error = cause(); const state = cause(() => { new Promise((resolve, reject) => { status.set('pending'); setTimeout(() => reject('error occurred'), 100); }).catch(reason => { status.set('error'); error.set(reason); }); return undefined; }); const computed = derive(() => { const value = state() return typeof value === 'function' ? value.call() : value + 1 }); assert.isUndefined(computed(), 'should be undefined while pending'); assert.equal(status(), 'pending'); await wait(100); assert.equal(error(), 'error occurred', 'should set error message after promise is rejected'); assert.equal(status(), 'error'); }); it('should compute function dependent on a chain of computed states dependent on a signal', function() { const state = cause(42); const computed1 = derive(() => 1 + state()); const computed2 = derive(() => computed1() * 2); const computed3 = derive(() => computed2() + 1); assert.equal(computed3(), 87); }); it('should compute function dependent on a chain of computed states dependent on an updated signal', function() { const state = cause(42); const computed1 = derive(() => 1 + state()); const computed2 = derive(() => computed1() * 2); const computed3 = derive(() => computed2() + 1); state.set(24); assert.equal(computed3(), 51); }); it('should drop X->B->X updates', function () { let count = 0; const x = cause(2); const a = derive(() => x() - 1); const b = derive(() => x() + a()); const c = derive(() => { count++; return 'c: ' + b(); }); assert.equal(c(), 'c: 3'); assert.equal(count, 1); x.set(4); assert.equal(c(), 'c: 7'); assert.equal(count, 2); }); it('should only update every signal once (diamond graph)', function() { let count = 0; const x = cause('a'); const a = derive(() => x()); const b = derive(() => x()); const c = derive(() => { count++; return a() + ' ' + b(); }); assert.equal(c(), 'a a'); assert.equal(count, 1); x.set('aa'); assert.equal(c(), 'aa aa'); assert.equal(count, 2); }); it('should only update every signal once (diamond graph + tail)', function() { let count = 0; const x = cause('a'); const a = derive(() => x()); const b = derive(() => x()); const c = derive(() => a() + ' ' + b()); const d = derive(() => { count++; return c(); }); assert.equal(d(), 'a a'); assert.equal(count, 1); x.set('aa'); assert.equal(d(), 'aa aa'); assert.equal(count, 2); }); it('should bail out if result is the same', function() { let count = 0 const x = cause('a') const a = derive(() => { x() return 'foo' }) const b = derive(() => { count++; return a() }, true) // turn memoization on assert.equal(b(), 'foo') assert.equal(count, 1) x.set('aa') assert.equal(b(), 'foo') assert.equal(count, 1) }) it('should block if result remains unchanged', function() { let count = 0 const x = cause(42) const a = derive(() => x() % 2) const b = derive(() => a() ? 'odd' : 'even', true) const c = derive(() => { count++ return `c: ${b()}` }, true) assert.equal(c(), 'c: even') assert.equal(count, 1) x.set(44) assert.equal(c(), 'c: even') assert.equal(count, 1) }) it('should block if an error occurred', function() { let count = 0 const x = cause(0) const a = derive(() => { if (x() === 1) throw new Error('Calculation error') return 1 }, true) const b = derive(() => a() ? 'success' : 'pending') const c = derive(() => { count++ return `c: ${b()}` }, true) assert.equal(a(), 1) assert.equal(c(), 'c: success') assert.equal(count, 1) x.set(1) try { assert.equal(a(), 1) } catch (error) { assert.fail(`Error during reactive computation ${error.message}`); } finally { assert.equal(c(), 'c: success') assert.equal(count, 1) } }) }); describe('Effect', function () { /* it('should be added to state.effects', function () { const state = cause(); effect(() => state()); assert.equal(state.effects.size, 1); effect(() => state()); assert.equal(state.effects.size, 2); }); it('should be added to to computed.effects', function () { const state = cause(); const computed = derive(() => 1 + state()); effect(() => computed()); assert.equal(computed.effects.size, 1); const computed2 = derive(() => 2 + state()); effect(() => computed() + computed2()); assert.equal(computed.effects.size, 2); assert.equal(computed2.effects.size, 1); }); */ it('should be triggered after a state change', function() { const state = cause('foo'); let effectDidRun = false; effect(() => { state(); effectDidRun = true; return; }); state.set('bar'); assert.isTrue(effectDidRun); }); it('should be triggered repeatedly after repeated state change', async function() { const state = cause(0); let count = 0; effect(() => { state(); count++; }); for (let i = 0; i < 10; i++) { state.set(i); await paint(); assert.equal(count, i + 1); // + initial effect execution } }); }); describe('Batch', function () { it('should be triggered only once after repeated state change', async function() { const state = cause(0); let result = 0; let count = 0; effect(enqueue => { result = state() enqueue(document.documentElement, 'count', () => () => count++) }); (() => { for (let i = 1; i <= 10; i++) { state.set(i); } })(); await paint(); assert.equal(result, 10); assert.equal(count, 1); }); }); }); </script> </body> </html>