UNPKG

@difizen/mana-observable

Version:

698 lines (657 loc) 17 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import assert from 'assert'; import { noop } from '@difizen/mana-common'; import { prop, origin, Trackable, Tracker, Observability, Notifier, tryInvokeGetter, } from './index'; describe('Tracker', () => { it('#trackable', () => { const originList: string[] = []; const originMap = new Map(); const originObj = {}; const originWeakMap = new WeakMap(); const originPromise = Promise.resolve(); class Foo { @prop() list = originList; @prop() map = originMap; @prop() obj = originObj; @prop() weakMap = originWeakMap; @prop() promise = originPromise; } const foo = new Foo(); const callback = () => { // }; const f = Tracker.track(foo, callback); const f1 = Tracker.track(foo, callback); assert(f === f1); assert(Trackable.is(f)); assert(Observability.getOrigin(f) === foo); assert(Observability.getOrigin(f.list) === originList); assert(Observability.getOrigin(f.map) === originMap); assert(Observability.getOrigin(f.obj) === originObj); assert(f.weakMap === originWeakMap); assert(f.promise === originPromise); assert(Trackable.tryGetOrigin(f) === foo); assert(null === Trackable.tryGetOrigin(null)); }); it('#track basic', () => { class Foo { @prop() info = ''; } const foo = new Foo(); let changeTimes = 0; const reaction = () => { changeTimes += 1; }; const f = Tracker.track(foo, reaction); f.info; f.info = 'foo'; assert(f !== foo); assert(Trackable.tryGetOrigin(f) === foo); assert( Tracker.track(null as any, () => { // }) === null, ); const empty = {}; assert( Tracker.track(empty, () => { // }) !== empty, ); const f1 = Tracker.track(f, reaction); f1.info; f1.info = 'foo1'; assert(changeTimes === 2); }); it('#track observable deep', () => { class Foo { @prop() info = ''; } class Bar { foo = new Foo(); } const bar = new Bar(); let changeTimes = 0; const reaction = () => { changeTimes += 1; }; const b = Tracker.track(bar, reaction); b.foo.info; // bar.foo.info = 'foo'; b.foo.info = 'foo'; assert(changeTimes === 1); }); it('#track by function', () => { class Foo { @prop() info = ''; @prop() info1 = ''; get infoLength() { return this.info.length; } getInfoLength() { return this.info1.length; } } const foo = new Foo(); let changeTimes = 0; const reaction = () => { changeTimes += 1; }; const f = Tracker.track(foo, reaction); f.infoLength; f.info = 'foo'; f.getInfoLength(); f.info1 = 'foo1'; assert(changeTimes === 2); }); it('#track reactable array', () => { class Foo { @prop() info = ''; } class Bar { @prop() list: Foo[] = []; } const bar = new Bar(); const foo = new Foo(); bar.list.push(foo); bar.list.push(new Foo()); let changeTimes = 0; const reaction = () => { changeTimes += 1; }; const b = Tracker.track(bar, reaction); b.list.filter((item) => item.info.length > 0); foo.info = 'foo'; assert(b.list === b.list); assert(changeTimes === 1); }); it('#track reactable map', () => { class Foo { @prop() info = ''; } class Bar { @prop() map = new Map<string, Foo | undefined>(); } const bar = new Bar(); const foo = new Foo(); bar.map.set('foo', foo); bar.map.set('empty', undefined); let changeTimes = 0; const reaction = () => { changeTimes += 1; }; const b = Tracker.track(bar, reaction); for (const key of b.map.keys()) { b.map.get(key)?.info; } foo.info = 'foo'; assert(b.map === b.map); assert(changeTimes === 1); }); it('#track reactable object', () => { class Foo { @prop() info = ''; } class Bar { @prop() obj: { foo: Foo | undefined; bar: Foo | undefined } = { foo: undefined, bar: undefined, }; } const bar = new Bar(); const foo = new Foo(); let changeTimes = 0; const reaction = () => { changeTimes += 1; }; const b = Tracker.track(bar, reaction); b.obj.bar?.info; bar.obj.foo = foo; b.obj.foo && b.obj.foo.info; foo.info = 'foo'; assert(b.obj === b.obj); assert(changeTimes === 2); }); it('#track deep', () => { class Foo { @prop() info = ''; } class Bar { foo = new Foo(); } const info = { bar: new Bar() }; let changeTimes = 0; const reaction = () => { changeTimes += 1; }; const i = Tracker.track(info, reaction); i.bar.foo.info; i.bar.foo.info = 'foo'; assert(changeTimes === 1); }); it('#tracker transform', () => { assert( Tracker.tramsform(null as any, () => { // }) === null, ); }); it('#track plain object', () => { const info = { name: 'info', age: 18 }; let changeTimes = 0; const reaction = () => { changeTimes += 1; }; const i = Tracker.track(info, reaction); i.name = 'foo'; assert(changeTimes === 1); }); it('#track array', () => { const arr: any[] = ['track array']; let changeTimes = 0; const reaction = () => { Tracker.track(arr, reaction); changeTimes += 1; }; const a = Tracker.track(arr, reaction); const a1 = Tracker.track(arr, reaction); assert(a === a1); a.push('a'); a1.push('a1'); const a2 = Tracker.track(arr, reaction); a2.push('a2'); assert(changeTimes === 6); }); it('#track map', () => { const map = new Map<string, string>(); let changeTimes = 0; const reaction = () => { Tracker.track(map, reaction); changeTimes += 1; }; const a = Tracker.track(map, reaction); const a1 = Tracker.track(map, reaction); assert(a === a1); a1.set('a', 'a'); a1.set('a1', 'a1'); assert(changeTimes === 2); }); it('#track plainObject', () => { const obj: Record<string, any> = {}; let changeTimes = 0; const reaction = () => { Tracker.track(obj, reaction); changeTimes += 1; }; const a = Tracker.track(obj, reaction); const a1 = Tracker.track(obj, reaction); assert(a === a1); a['a'] = 'a'; a1['a1'] = 'a1'; assert(changeTimes === 2); }); it('#track plainObject deep', () => { const obj: Record<string, any> = {}; obj['info'] = { a: { b: { c: {} } } }; obj['arr'] = []; let changeTimes = 0; const reaction = () => { Tracker.track(obj, reaction); changeTimes += 1; }; const a = Tracker.track(obj, reaction); const a1 = Tracker.track(obj, reaction); assert(a === a1); a['a'] = 'a'; a1['info'].a1 = 'a1'; a1['info'].a.b.c = 'c'; a1['arr'].push('a'); assert(changeTimes === 4); }); it('#track plainObject deep with class instance', () => { class Foo { @prop() info = ''; @prop() info1 = ''; } const obj: Record<string, any> = {}; obj['foo'] = new Foo(); let changeTimes = 0; const reaction = () => { const object = Tracker.track(obj, reaction); object['foo'].info; changeTimes += 1; }; const a = Tracker.track(obj, reaction); const a1 = Tracker.track(obj, reaction); a1['foo'].info; assert(a === a1); a1['foo'].info = 'a'; a1['foo'].info1 = 'a1'; assert(changeTimes === 1); }); it('#track class instance deep with arr', () => { class Bar { @prop() name = ''; } class Foo { @prop() arr: Bar[] = []; } const foo = new Foo(); let changeTimes = 0; const reaction = () => { const trackable = Tracker.track(foo, reaction); trackable.arr.forEach((item) => item.name); changeTimes += 1; }; const a = Tracker.track(foo, reaction); const a1 = Tracker.track(foo, reaction); assert(a === a1); a.arr.forEach((item) => item.name); a.arr.push(new Bar()); // 2 a1.arr.push(new Bar()); // 4 a.arr[0].name = 'a'; // 5 assert(changeTimes === 5); }); it('#track class instance with normal arr', () => { class Foo { arr: string[] = []; } const foo = new Foo(); let changeTimes = 0; const reaction = () => { const trackable = Tracker.track(foo, reaction); trackable.arr; changeTimes += 1; }; const a = Tracker.track(foo, reaction); const a1 = Tracker.track(foo, reaction); assert(a === a1); a.arr.push('a'); // 0 a1.arr.push('b'); // 0 assert(changeTimes === 0); }); it('#track unobservable array property', () => { class Foo { map = new Map<string, string>(); getArr() { return this.map; } } const foo = new Foo(); let changeTimes = 0; const reaction = () => { const trackable = Tracker.track(foo, reaction); trackable.getArr(); changeTimes += 1; }; const a = Tracker.track(foo, reaction); const a1 = Tracker.track(foo, reaction); assert(a === a1); a.getArr().set('a', 'a'); a1.getArr().set('a1', 'a1'); const valueA = a1.getArr().get('a'); assert(changeTimes === 0); assert(valueA === 'a'); }); it('#track observable array property', () => { class Bar { @prop() count = 0; } class Foo { @prop() arr: Bar[] = []; } const foo = new Foo(); let changeCount = 0; const reaction = () => { const trackable = Tracker.track(foo, reaction); trackable.arr; changeCount += 1; }; reaction(); // 1 foo.arr.push(new Bar()); // 3 foo.arr.push(new Bar()); // 5 foo.arr.push(new Bar()); // 7 assert(changeCount === 7); }); it('#deep track observable array property', () => { class Bar { @prop() count = 0; } class Foo { @prop() arr: Bar[] = []; } const foo = new Foo(); let changeCount = 0; const reaction = () => { const trackable = Tracker.track(foo, reaction); for (let i = 0; i < trackable.arr.length; i++) { trackable.arr[i]?.count; } changeCount += 1; }; reaction(); // 1 foo.arr.push(new Bar()); // 3 foo.arr.push(new Bar()); // 5 foo.arr.push(new Bar()); // 7 const obj = foo.arr[0]; if (obj) { obj.count = 1; // 8 } assert(changeCount === 8); }); it('#track observable map property', () => { class Foo { @prop() objMap = new Map<string, { count: number }>(); } const foo = new Foo(); let changeCount = 0; const reaction = () => { const trackable = Tracker.track(foo, reaction); trackable.objMap; changeCount += 1; }; reaction(); // 1 foo.objMap.set('a', { count: 1 }); // 2 foo.objMap.set('b', { count: 2 }); // 3 foo.objMap.set('c', { count: 3 }); // 4 assert(changeCount === 4); }); it('#deep track observable map property', () => { class Foo { @prop() objMap = new Map<string, { count: number }>(); } const foo = new Foo(); let changeCount = 0; const reaction = () => { const trackable = Tracker.track(foo, reaction); const keys = trackable.objMap.keys(); for (const key of keys) { const obj = trackable.objMap.get(key); obj?.count; } changeCount += 1; }; reaction(); // 1 foo.objMap.set('a', { count: 1 }); // 2 foo.objMap.set('b', { count: 2 }); // 3 foo.objMap.set('c', { count: 3 }); // 4 const obj = foo.objMap.get('a'); if (obj) { obj.count = 0; // 5 } assert(changeCount === 5); }); it('#track observable plainObject property', () => { class Foo { @prop() obj = { a: 'a', b: { c: 'c', d: {} } }; } const foo = new Foo(); let changeCount = 0; const reaction = () => { const trackable = Tracker.track(foo, reaction); trackable.obj.a; trackable.obj.b.c; changeCount += 1; }; reaction(); // 1 foo.obj.a = 'aa'; // 2 foo.obj.b.c = 'cc'; // 3 assert(changeCount === 3); }); it('#deep track observable plainObject property', () => { class Bar { @prop() count = 0; } class Foo { @prop() obj = { a: 'a', b: new Bar() }; } const foo = new Foo(); let changeCount = 0; const reaction = () => { const trackable = Tracker.track(foo, reaction); trackable.obj.a; trackable.obj.b.count; changeCount += 1; }; reaction(); // 1 foo.obj.a = 'aa'; // 2 foo.obj.b.count = 1; // 3 assert(changeCount === 3); }); it('#renders', () => { class Model { @prop() enabled = false; } const model = new Model(); let changeTimes = 0; let changeTimes1 = 0; const render = () => { const trackable = Tracker.track(model, render); trackable.enabled; changeTimes += 1; }; const render1 = () => { const trackable = Tracker.track(model, render1); trackable.enabled; changeTimes1 += 1; }; render(); // 1 render1(); // 1 model.enabled = true; // 2 assert(changeTimes === 2); assert(changeTimes1 === 2); }); it('#track skip Date object', () => { const now = new Date(); const obj = { arr: [0, 1, 2], key: 'obj', now }; const tracked = Tracker.track(obj, noop); const objStr = JSON.stringify(tracked); assert(objStr); }); it('#track skip Set object', () => { const set = new Set(); const obj = { arr: [0, 1, 2], key: 'obj', set }; const tracked = Tracker.track(obj, noop); const objStr = JSON.stringify(tracked); assert(tracked.set.add(1)); assert(objStr); }); it('#track skip RegExp object', () => { const reg = new RegExp('[\\w]*'); const obj = { arr: [0, 1, 2], key: 'obj', reg }; const tracked = Tracker.track(obj, noop); const objStr = JSON.stringify(tracked); assert(tracked.reg.test('test')); assert(objStr); }); it('#find same notifer after track', () => { class Foo { @prop() name?: string; } const raw = new Foo(); const tracked = Tracker.track(raw, noop); const event0 = Notifier.toEvent(raw, 'name'); const event1 = Notifier.toEvent(tracked, 'name'); assert(event0 === event1); }); it('#invoke getter origin', () => { let baz: any = undefined; class Foo { bar() { if (this !== baz) { throw new TypeError('Illegal invocation'); } return true; } } baz = new Foo(); const proxyBaz = new Proxy(baz, {}); try { proxyBaz.bar(); assert(false); } catch (e) { assert(e instanceof TypeError); assert(e.message === 'Illegal invocation'); } assert(!!tryInvokeGetter(proxyBaz.bar, proxyBaz, baz)); }); it('#get readonly & non-configurable property', () => { class Foo {} class Bar { name = 'Bar'; } const foo = new Foo(); const bar = new Bar(); Object.defineProperty(foo, 'prop', { value: bar, writable: false, configurable: false, }); try { let changeCount = 0; const reaction = () => { const trackable = Tracker.track(foo, reaction); if ('prop' in trackable) { const b = trackable['prop'] as Bar; b.name; } changeCount += 1; }; reaction(); // 1 assert(changeCount === 1); } catch (e) { assert(!e); } }); it('#target frozen', () => { const t = {}; const arr = [0, t]; const map = new Map(); const obj = { arr: [] }; map.set('obj', t); class Foo { @prop() obj = obj; @prop() arr = arr; @prop() map = map; } const foo = new Foo(); let changeCount = 0; let trackable: any; const reaction = () => { trackable = Tracker.track(foo, reaction); Object.freeze(obj); Object.freeze(arr); Object.freeze(map); changeCount += 1; }; reaction(); // 1 trackable.obj; const arr1 = trackable.obj.arr; const obj1 = trackable.map.get('obj'); const t1 = trackable.arr[1]; assert(changeCount === 1); assert(arr1 === obj.arr); assert(Observability.getOrigin(obj1) === map.get('obj')); assert(t1 === arr[1]); }); it('#origin property', () => { const t = {}; const arr = [0, t]; const map = new Map(); const obj = { arr: [] }; map.set('obj', t); class Foo { @origin() obj = obj; @origin() arr = arr; @prop() map = map; } const foo = new Foo(); let trackable: Foo; const reaction = () => { trackable = Tracker.track(foo, reaction); }; reaction(); // 1 assert(trackable!.arr === arr); assert(trackable!.map !== map); assert(trackable!.obj === obj); }); });