UNPKG

@platform/state

Version:

A small, simple, strongly typed, [rx/observable] state-machine.

1,045 lines 52.8 kB
import { Subject } from 'rxjs'; import { TreeState } from '.'; import { expect } from '../test'; import { TreeIdentity } from '../TreeIdentity'; import { TreeQuery } from '../TreeQuery'; import { helpers } from './helpers'; import { isDraft } from 'immer'; const query = TreeQuery.create; const create = TreeState.create; describe('TreeState', () => { describe('create', () => { it('without parent', () => { const root = { id: 'root' }; const state = create({ root }); expect(state.root).to.not.equal(root); expect(state.parent).to.eql(undefined); expect(state.children).to.eql([]); expect(state.id).to.eql(state.root.id); expect(state.key).to.eql('root'); expect(state.namespace.length).to.greaterThan(10); }); it('with parent', () => { const root = { id: 'myLeaf' }; const state = create({ root, parent: 'myParent' }); expect(state.parent).to.eql('myParent'); }); it('create with no id (defaults to "node")', () => { const state = create(); expect(helpers.id.stripNamespace(state.id)).to.eql('node'); expect(helpers.id.namespace(state.id)).to.eql(state.namespace); }); it('from root id (string)', () => { const state = create({ root: 'foo' }); const id = `${state.namespace}:foo`; expect(state.id).to.eql(id); expect(state.root.id).to.eql(id); }); it('from root id (parses <namespace>:<id>)', () => { const state = create({ root: 'ns:foo' }); expect(state.namespace).to.eql('ns'); expect(state.id).to.eql('ns:foo'); }); it('readonly', () => { const root = { id: 'root' }; const state = create({ root }); expect(state.readonly).to.equal(state); }); it('throw: id contains "/" character', () => { const fn = () => create({ root: 'foo/bar' }); expect(fn).to.throw(/Tree node IDs cannot contain the "\/" character/); }); }); describe('dispose', () => { it('dispose', () => { const state = create(); expect(state.isDisposed).to.eql(false); let count = 0; state.dispose$.subscribe((e) => count++); expect(state.isDisposed).to.eql(false); state.dispose(); state.dispose(); expect(state.isDisposed).to.eql(true); }); it('dispose: event', () => { const state = create(); state.change((root, ctx) => ctx.props(root, (p) => (p.label = 'foo'))); const fired = []; state.event.$.subscribe((e) => fired.push(e)); state.dispose(); state.dispose(); expect(fired.length).to.eql(1); const event = fired[0]; expect(event.type).to.eql('TreeState/disposed'); expect(event.payload.final).to.eql(state.root); }); it('disposes of all children', () => { const state = create(); expect(state.isDisposed).to.eql(false); const child1 = state.add({ root: 'foo' }); const child2 = child1.add({ root: 'bar' }); const child3 = child2.add({ root: 'zoo' }); child2.dispose(); expect(child3.isDisposed).to.eql(true); state.dispose(); expect(state.isDisposed).to.eql(true); expect(child1.isDisposed).to.eql(true); expect(child2.isDisposed).to.eql(true); expect(child3.isDisposed).to.eql(true); }); it('takes a [dispose$] within constructor', () => { const dispose$ = new Subject(); const state = create({ dispose$ }); expect(state.isDisposed).to.eql(false); let count = 0; state.dispose$.subscribe(() => count++); dispose$.next(); dispose$.next(); dispose$.next(); expect(state.isDisposed).to.eql(true); expect(count).to.eql(1); }); }); describe('static', () => { it('isInstance', () => { const test = (input, expected) => { expect(TreeState.isInstance(input)).to.eql(expected); }; const instance = create({ root: 'foo' }); test(instance, true); test(undefined, false); test(null, false); test('', false); test({ id: 'foo' }, false); }); it('identity', () => { expect(TreeState.identity).to.equal(TreeIdentity); }); }); describe('rewrite IDs with namespace prefix', () => { it('simple', () => { const root = { id: 'root' }; const state = create({ root }); const start = `${state.namespace}:`; expect(state.id.startsWith(start)).to.eql(true); expect(state.root.id.startsWith(start)).to.eql(true); }); it('deep', () => { const root = { id: 'root', children: [{ id: 'child-1' }, { id: 'child-2', children: [{ id: 'child-2-1' }] }], }; const state = create({ root }); const ids = []; const start = `${state.namespace}:`; query(state.root).walkDown((e) => ids.push(e.node.id)); expect(ids.length).to.eql(4); expect(ids.every((id) => id.startsWith(start))).to.eql(true); }); }); describe('add', () => { it('add: root as {object} (TreeNode)', () => { const root = { id: 'root' }; const state = create({ root }); expect(state.children).to.eql([]); const child = state.add({ root: { id: 'foo' } }); expect(state.children.length).to.eql(1); expect(state.children[0]).to.equal(child); expect(child.parent).to.eql(`${state.namespace}:root`); }); it('add: root as string ("id")', () => { const root = { id: 'root' }; const state = create({ root }); const child = state.add({ root: 'foo' }); expect(state.children.length).to.eql(1); expect(child.id).to.eql(`${child.namespace}:foo`); }); it('add (pre-existing): { root: [TreeState] }', () => { const state = create({ root: 'root' }); expect(state.root.children).to.eql(undefined); const child = create({ root: 'foo' }); expect(state.namespace).to.not.eql(child.namespace); state.add({ root: child }); expect(helpers.children(state.root)[0].id).to.eql(child.id); expect(state.children.includes(child)).to.eql(true); }); it('add (pre-existing): [TreeState] as base argument', () => { const state = create({ root: 'root' }); expect(state.root.children).to.eql(undefined); const child = create({ root: 'foo' }); expect(state.namespace).to.not.eql(child.namespace); state.add(child); expect(state.children.includes(child)).to.eql(true); expect(helpers.children(state.root)[0].id).to.eql(child.id); }); it('add (pre-existing): [TreeState] as base argument', () => { const state = create({ root: 'root' }); expect(state.root.children).to.eql(undefined); const child = create({ root: 'foo' }); expect(state.namespace).to.not.eql(child.namespace); state.add(child); expect(state.children.includes(child)).to.eql(true); expect(helpers.children(state.root)[0].id).to.eql(child.id); }); it('add (pre-existing): within sub-node of parent', () => { var _a; const state = create({ root: { id: 'root', children: [{ id: 'foo' }] } }); const subnode = state.query.find((e) => e.key === 'foo'); expect(subnode === null || subnode === void 0 ? void 0 : subnode.id.endsWith(':foo')).to.eql(true); const child = create({ root: 'child' }); state.add({ root: child, parent: subnode === null || subnode === void 0 ? void 0 : subnode.id }); expect((_a = state.children.find((e) => e.id === child.id)) === null || _a === void 0 ? void 0 : _a.id).to.eql(child.id); const children = helpers.children(state.root); expect(children.length).to.eql(1); const grandchildren = children[0].children || []; expect(grandchildren.length).to.eql(1); expect(grandchildren[0].id).to.eql(child.id); }); it('add: no parent id (root id assumed)', () => { const root = { id: 'root' }; const state = create({ root }); const child = state.add({ root: 'foo' }); expect(state.children.length).to.eql(1); expect(child.id).to.eql(`${child.namespace}:foo`); }); it('adds multiple children with same id (geneated namespaces differs)', () => { const root = { id: 'root' }; const state = create({ root }); const child1 = state.add({ root: { id: 'foo' } }); const child2 = state.add({ root: { id: 'foo' } }); expect(child1.id).to.not.eql(child2.id); }); it('inserts node into parent state-tree data', () => { const root = { id: 'root', children: [{ id: 'mary' }] }; const state = create({ root }); expect((state.root.children || []).length).to.eql(1); const fired = []; state.event .payload('TreeState/changed') .subscribe((e) => fired.push(e)); const child1 = state.add({ root: { id: 'foo' } }); const children = state.root.children || []; expect(children.length).to.eql(2); expect(children[0].id).to.match(/\:mary$/); expect(children[1].id).to.eql(child1.id); expect(fired.length).to.eql(1); expect((fired[0].from.children || []).length).to.eql(1); expect((fired[0].to.children || []).length).to.eql(2); }); it('child added to more than one parent [StateTree]', () => { const state1 = create({ root: 'root-1' }); const state2 = create({ root: 'root-2' }); const child = create({ root: 'child' }); expect(state1.namespace).to.not.eql(state2.namespace); state1.add(child); state2.add(child); const childAt = (state, i) => helpers.children(state.root)[i]; const firstChild = (state) => childAt(state, 0); expect(firstChild(state1).id).to.eql(child.id); expect(firstChild(state2).id).to.eql(child.id); child.change((draft, ctx) => { ctx.props(draft).label = 'hello'; }); expect(firstChild(state1).props).to.eql({ label: 'hello' }); expect(firstChild(state2).props).to.eql({ label: 'hello' }); state1.remove(child); expect(firstChild(state1)).to.eql(undefined); expect(firstChild(state2).id).to.eql(child.id); child.dispose(); expect(firstChild(state1)).to.eql(undefined); expect(firstChild(state2)).to.eql(undefined); }); it('event: added', () => { const root = { id: 'root' }; const state = create({ root }); const fired = []; state.event .payload('TreeState/child/added') .subscribe((e) => fired.push(e)); const child1 = state.add({ root: { id: 'foo' } }); const child2 = state.add({ root: { id: 'foo' } }); expect(fired.length).to.eql(2); expect(fired[0].child).to.equal(child1); expect(fired[1].child).to.equal(child2); expect(fired[0].parent).to.equal(state); expect(fired[1].parent).to.equal(state); }); it('throw: "parent" does not exist', () => { const state = create({ root: { id: 'root' } }); const fn = () => state.add({ parent: '404', root: { id: 'foo' } }); expect(fn).to.throw(/parent node '404' does not exist/); }); it('throw: (pre-existing) "parent" does not exist', () => { const state = create({ root: { id: 'root' } }); const child = create({ root: 'child' }); const fn = () => state.add({ parent: '404', root: child }); expect(fn).to.throw(new RegExp(`parent sub-node '404' within '${state.id}' does not exist`)); }); it('throw: child already added', () => { const state = create({ root: 'root' }); const child = state.add({ root: 'child' }); expect(state.children.length).to.eql(1); expect(state.namespace).to.not.eql(child.namespace); const fn = () => state.add({ root: child }); expect(fn).to.throw(/already exists/); }); }); describe('remove', () => { it('removes (but does not dispose)', () => { const root = { id: 'root' }; const state = create({ root }); const fired = []; state.event .payload('TreeState/child/removed') .subscribe((e) => fired.push(e)); const child1 = state.add({ parent: 'root', root: { id: 'foo' } }); const child2 = state.add({ parent: 'root', root: { id: 'foo' } }); expect(state.children.length).to.eql(2); state.remove(child1); expect(state.children.length).to.eql(1); state.remove(child2); expect(state.children.length).to.eql(0); expect(child1.isDisposed).to.eql(false); expect(child2.isDisposed).to.eql(false); expect(fired.length).to.eql(2); expect(fired[0].child).to.eql(child1); expect(fired[1].child).to.eql(child2); }); it('removes on [child.dispose()]', () => { const root = { id: 'root' }; const state = create({ root }); const fired = []; state.event .payload('TreeState/child/removed') .subscribe((e) => fired.push(e)); const child1 = state.add({ parent: 'root', root: { id: 'foo' } }); const child2 = state.add({ parent: 'root', root: 'foo' }); expect(state.children.length).to.eql(2); child1.dispose(); expect(state.children.length).to.eql(1); child2.dispose(); expect(state.children.length).to.eql(0); expect(fired.length).to.eql(2); expect(fired[0].child).to.eql(child1); expect(fired[1].child).to.eql(child2); }); it('removes node from parent state-tree data', () => { const root = { id: 'root' }; const state = create({ root }); const child1 = state.add({ parent: 'root', root: 'foo' }); const child2 = state.add({ parent: 'root', root: 'foo' }); const children = () => state.root.children || []; const count = () => children().length; const includes = (id) => (state.root.children || []).some((c) => c.id === id); expect(count()).to.eql(2); expect(includes(child1.id)).to.eql(true); expect(includes(child2.id)).to.eql(true); child1.dispose(); expect(count()).to.eql(1); expect(includes(child1.id)).to.eql(false); expect(includes(child2.id)).to.eql(true); state.remove(child2); expect(count()).to.eql(0); expect(includes(child1.id)).to.eql(false); expect(includes(child2.id)).to.eql(false); }); it('throw: remove child that does not exist', () => { const root = { id: 'root' }; const state = create({ root }); const child1 = state.add({ parent: 'root', root: { id: 'foo' } }); const child2 = state.add({ parent: 'root', root: { id: 'foo' } }); const test = (child) => { const state = create({ root }); const fn = () => state.remove(child); expect(fn).to.throw(/Cannot remove child-state as it does not exist in the parent/); }; test(child1.root.id); test(child1.id); test(child2); }); }); describe('clear', () => { it('empty', () => { const root = { id: 'root' }; const state = create({ root }); expect(state.children.length).to.eql(0); state.clear(); expect(state.children.length).to.eql(0); }); it('removes all children', () => { const root = { id: 'root' }; const state = create({ root }); const fired = []; state.event.childRemoved$.subscribe((e) => fired.push(e)); const parent = 'root'; state.add({ parent, root: 'foo' }); state.add({ parent, root: 'bar' }); expect(state.children.length).to.eql(2); state.clear(); expect(state.children.length).to.eql(0); expect(fired.length).to.eql(2); }); }); describe('change', () => { const root = { id: 'root', children: [{ id: 'child-1' }, { id: 'child-2', children: [{ id: 'child-2-1' }] }], }; it('simple', () => { var _a, _b, _c, _d, _e; const state = create({ root }); const res = state.change((draft, ctx) => { ctx.props(draft, (p) => { p.label = 'Hello!'; p.icon = 'face'; }); }); expect((_a = state.root.props) === null || _a === void 0 ? void 0 : _a.label).to.eql('Hello!'); expect((_b = state.root.props) === null || _b === void 0 ? void 0 : _b.icon).to.eql('face'); expect(res.op).to.eql('update'); expect(res.cid.length).to.greaterThan(10); expect((_c = res.changed) === null || _c === void 0 ? void 0 : _c.from.props).to.eql(undefined); expect((_e = (_d = res.changed) === null || _d === void 0 ? void 0 : _d.to.props) === null || _e === void 0 ? void 0 : _e.label).to.eql('Hello!'); const { prev, next } = res.patches; expect(prev.length).to.eql(1); expect(next.length).to.eql(1); expect(prev[0]).to.eql({ op: 'remove', path: 'props' }); expect(next[0]).to.eql({ op: 'add', path: 'props', value: { label: 'Hello!', icon: 'face' }, }); }); it('child array: insert (updates id namespaces)', () => { const state = create({ root: 'root' }); state.change((draft, ctx) => { const children = TreeState.children(draft); children.push(...[{ id: 'foo', children: [{ id: 'foo.1' }] }, { id: 'bar' }]); }); const ns = TreeState.identity.hasNamespace; const children = state.root.children || []; expect(children.length).to.eql(2); expect(ns(children[0].id)).to.eql(true); expect(ns(children[1].id)).to.eql(true); expect(ns((children[0].children || [])[0].id)).to.eql(true); }); it('updates parent state-tree when child changes', () => { const state = create({ root }); const child1 = state.add({ root: 'foo' }); const child2 = state.add({ root: 'bar' }); const children = () => state.root.children || []; const count = () => children().length; expect(count()).to.eql(4); expect(children()[2].props).to.eql(undefined); child1.change((draft, ctx) => ctx.props(draft, (p) => (p.label = 'foo'))); expect(children()[2].props).to.eql({ label: 'foo' }); child1.dispose(); child1.change((draft, ctx) => ctx.props(draft, (p) => (p.label = 'bar'))); expect(count()).to.eql(3); child2.change((root, ctx) => ctx.props(root, (p) => (p.label = 'hello'))); expect(children()[2].props).to.eql({ label: 'hello' }); }); it('updates parent state-tree when child changes (deep)', () => { const state = create({ root: { id: 'root' } }); const child1 = state.add({ root: 'foo' }); const child2 = child1.add({ root: 'bar' }); const children = (node) => node.children || []; const grandchild = () => children(children(state.root)[0])[0]; expect(grandchild().props).to.eql(undefined); child2.change((draft, ctx) => (ctx.props(draft).label = 'hello')); expect(grandchild().props).to.eql({ label: 'hello' }); }); it('updates from found/queried node', () => { var _a, _b; const state = create({ root }); expect((_a = state.query.findById('child-1')) === null || _a === void 0 ? void 0 : _a.props).to.eql(undefined); state.change((draft, ctx) => { const child = ctx.findById('child-1'); if (child) { ctx.props(child, (p) => (p.label = 'hello')); } }); expect((_b = state.query.findById('child-1')) === null || _b === void 0 ? void 0 : _b.props).to.eql({ label: 'hello' }); }); }); describe('change (events)', () => { const root = { id: 'root', children: [{ id: 'child-1' }, { id: 'child-2', children: [{ id: 'child-2-1' }] }], }; it('event: changed', () => { var _a, _b; const state = create({ root }); const fired = []; state.event .payload('TreeState/changed') .subscribe((e) => fired.push(e)); const res = state.change((root, ctx) => { ctx.props(root, (p) => (p.label = 'foo')); }); expect(fired.length).to.eql(1); const event = fired[0]; expect((_a = event.from.props) === null || _a === void 0 ? void 0 : _a.label).to.eql(undefined); expect((_b = event.to.props) === null || _b === void 0 ? void 0 : _b.label).to.eql('foo'); expect(event.patches).to.eql(res.patches); }); it('event: changed (fires from root when child changes)', () => { const state = create({ root }); const child = state.add({ root: 'foo' }); const firedRoot = []; state.event .payload('TreeState/changed') .subscribe((e) => firedRoot.push(e)); const firedChild = []; child.event .payload('TreeState/changed') .subscribe((e) => firedChild.push(e)); child.change((draft, ctx) => ctx.props(draft, (p) => (p.label = 'foo'))); expect(firedRoot.length).to.eql(1); expect(firedChild.length).to.eql(1); expect(firedRoot[0].patches.next[0].op).to.eql('replace'); expect(firedChild[0].patches.next[0].op).to.eql('add'); }); it('event: patched', () => { const state = create({ root }); const fired = []; state.event .payload('TreeState/patched') .subscribe((e) => fired.push(e)); const res = state.change((root, ctx) => { ctx.props(root, (p) => (p.label = 'foo')); }); expect(fired.length).to.eql(1); const event = fired[0]; expect(event.prev).to.eql(res.patches.prev); expect(event.next).to.eql(res.patches.next); }); it('event: does not fire when nothing changes', () => { const state = create({ root }); const fired = []; state.event .payload('TreeState/changed') .subscribe((e) => fired.push(e)); const res = state.change((root) => { }); expect(fired.length).to.eql(0); expect(res.changed).to.eql(undefined); }); }); describe('change: ctx (context)', () => { const root = { id: 'root', children: [{ id: 'child-1' }, { id: 'child-2', children: [{ id: 'child-2-1' }] }], }; it('toObject', () => { const state = create({ root }); state.change((draft, ctx) => { const child = ctx.findById('child-2-1'); expect(child).to.exist; if (child) { expect(isDraft(draft)).to.eql(true); expect(isDraft(child)).to.eql(true); expect(isDraft(ctx.toObject(draft))).to.eql(false); expect(isDraft(ctx.toObject(child))).to.eql(false); } }); }); }); describe('query', () => { const root = { id: 'root', children: [{ id: 'child-1' }, { id: 'child-2', children: [{ id: 'child-2-1' }] }], }; it('has query', () => { const state = create({ root }); const query = state.query; expect(query.root).to.eql(state.root); expect(query.namespace).to.eql(state.namespace); }); describe('walkDown', () => { it('walkDown', () => { const state = create({ root }); const walked = []; state.query.walkDown((e) => { expect(e.namespace).to.eql(state.namespace); expect(e.node.id.endsWith(`:${e.key}`)).to.eql(true); walked.push(e); }); expect(walked.length).to.eql(4); expect(walked[0].key).to.eql('root'); expect(walked[3].key).to.eql('child-2-1'); }); it('walkDown: stop', () => { const state = create({ root }); const walked = []; state.query.walkDown((e) => { walked.push(e); if (e.key === 'child-1') { e.stop(); } }); expect(walked.length).to.eql(2); expect(walked[0].key).to.eql('root'); expect(walked[1].key).to.eql('child-1'); }); it('walkDown: skip (children)', () => { const state = create({ root }); const walked = []; state.query.walkDown((e) => { walked.push(e); if (e.key === 'child-2') { e.skip(); } }); expect(walked.length).to.eql(3); expect(walked[0].key).to.eql('root'); expect(walked[1].key).to.eql('child-1'); expect(walked[2].key).to.eql('child-2'); }); it('walkDown: does not walk down into child namespace', () => { var _a; const state = create({ root }); const child = state.add({ parent: 'child-2-1', root: { id: 'foo' } }); expect(child.namespace).to.not.eql(state.namespace); expect((_a = query(state.root).findById(child.id)) === null || _a === void 0 ? void 0 : _a.id).to.eql(child.id); const walked = []; state.query.walkDown((e) => walked.push(e)); const ids = walked.map((e) => e.id); expect(ids.length).to.greaterThan(0); expect(ids.includes('foo')).to.eql(false); }); }); describe('walkUp', () => { it('walkUp', () => { const state = create({ root }); const test = (startAt) => { const walked = []; state.query.walkUp(startAt, (e) => walked.push(e)); expect(walked.map((e) => e.key)).to.eql(['child-2-1', 'child-2', 'root']); }; test('child-2-1'); test(state.query.findById('child-2-1')); }); it('walkUp: startAt not found', () => { const state = create({ root }); const test = (startAt) => { const walked = []; state.query.walkUp(startAt, (e) => walked.push(e)); expect(walked).to.eql([]); }; test(); test('404'); test({ id: '404' }); }); it('walkUp: does not walk up into parent namespace', () => { const state = create({ root }); const child = state.add({ parent: 'child-2-1', root: { id: 'foo', children: [{ id: 'foo.child' }] }, }); const fooChild = child.query.findById('foo.child'); expect(fooChild).to.exist; const test = (startAt) => { const walked = []; child.query.walkUp(startAt, (e) => walked.push(e)); expect(walked.map((e) => e.key)).to.eql(['foo.child', 'foo']); }; test(fooChild); test(fooChild === null || fooChild === void 0 ? void 0 : fooChild.id); test('foo.child'); }); it('walkUp: not within namespace', () => { const state = create({ root }); const child = state.add({ parent: 'child-2-1', root: { id: 'foo', children: [{ id: 'foo.child' }] }, }); const fooChild = child.query.findById('foo.child'); expect(fooChild).to.exist; const walked = []; state.query.walkUp(fooChild, (e) => walked.push(e)); expect(walked.map((e) => e.id)).to.eql([]); }); }); describe('find', () => { it('find', () => { const state = create({ root }); const walked = []; state.query.walkDown((e) => walked.push(e)); const res1 = state.query.findById('404'); const res2 = state.query.findById('root'); const res3 = state.query.findById('child-2-1'); expect(res1).to.eql(undefined); expect(res2).to.eql(walked[0].node); expect(res3).to.eql(walked[3].node); }); it('find: root (immediate)', () => { const state = create({ root: 'root' }); const res = state.query.findById('root'); expect(res === null || res === void 0 ? void 0 : res.id).to.eql(state.id); }); it('find: does not walk down into child namespace', () => { var _a, _b, _c, _d; const state = create({ root }); const child = state.add({ parent: 'child-2-1', root: { id: 'foo' } }); expect(child.namespace).to.not.eql(state.namespace); expect((_a = query(state.root).findById(child.id)) === null || _a === void 0 ? void 0 : _a.id).to.eql(child.id); expect(state.query.findById('foo')).to.eql(undefined); expect(TreeIdentity.key((_b = state.query.findById('child-2-1')) === null || _b === void 0 ? void 0 : _b.id)).to.eql('child-2-1'); expect((_c = child.query.findById('foo')) === null || _c === void 0 ? void 0 : _c.id).to.eql(child.id); expect((_d = child.query.findById('child-2-1')) === null || _d === void 0 ? void 0 : _d.id).to.eql(undefined); }); }); describe('exists', () => { it('does exist', () => { const state = create({ root }); const res = state.query.findById('child-2-1'); expect(TreeState.identity.parse(res === null || res === void 0 ? void 0 : res.id).key).to.eql('child-2-1'); }); it('does not exist', () => { const state = create({ root }); const res = state.query.findById('404'); expect(res).to.eql(undefined); }); }); }); describe('child (find)', () => { it('empty', () => { const root = { id: 'root' }; const tree = create({ root }); const list = []; const res = tree.find((e) => { list.push(e); return false; }); expect(res).to.eql(undefined); expect(list).to.eql([]); }); it('undefined/null', () => { const root = { id: 'root' }; const tree = create({ root }); expect(tree.find()).to.eql(undefined); expect(tree.find(null)).to.eql(undefined); }); it('deep', () => { const tree = create(); const child1 = tree.add({ root: 'child-1' }); const child2a = child1.add({ root: 'child-2a' }); child1.add({ root: 'child-2a' }); const child3 = child2a.add({ root: 'child-3' }); const list = []; const res = tree.find((e) => { list.push(e); return e.key === 'child-3'; }); expect(list.length).to.eql(3); expect(res === null || res === void 0 ? void 0 : res.id).to.equal(child3.id); expect(list[0].tree.id).to.eql(child1.id); expect(list[1].tree.id).to.eql(child2a.id); expect(list[2].tree.id).to.eql(child3.id); }); it('flat', () => { const root = { id: 'root' }; const tree = create({ root }); const child1 = tree.add({ root: 'child-1' }); const child2 = tree.add({ root: 'child-2' }); const child3 = tree.add({ root: 'child-3' }); const list = []; const res = tree.find((e) => { list.push(e); return e.key === 'child-3'; }); expect(res === null || res === void 0 ? void 0 : res.id).to.equal(child3.id); expect(list.length).to.eql(3); expect(list[0].tree.id).to.eql(child1.id); expect(list[1].tree.id).to.eql(child2.id); expect(list[2].tree.id).to.eql(child3.id); }); it('via node-identifier (param)', () => { const tree = create(); const child1 = tree.add({ root: 'child-1' }); const child2a = child1.add({ root: 'child-2a' }); expect(tree.find(child2a.id)).to.equal(child2a); expect(tree.find(child2a)).to.equal(child2a); expect(tree.find('404')).to.equal(undefined); }); it('node-identifer descendent of child module', () => { const tree = create(); const child1 = tree.add({ root: 'child-1' }); const child2a = child1.add({ root: { id: 'child-2a', children: [{ id: 'foo' }] } }); const query = TreeQuery.create({ root: tree.root }); const node = query.find((e) => e.key === 'foo'); expect(node).to.exist; expect(tree.find(node)).to.equal(child2a); expect(tree.find(node === null || node === void 0 ? void 0 : node.id)).to.equal(child2a); }); it('toString => fully qualified identifier (<namespace>:<id>)', () => { const tree = create(); const child1 = tree.add({ root: 'child-1' }); child1.add({ root: 'ns:child-2a' }); const res1 = tree.find((e) => e.toString() === 'ns:child-2a'); const res2 = tree.find((e) => e.id === 'ns:child-2a'); expect(res1 === null || res1 === void 0 ? void 0 : res1.id).to.eql('ns:child-2a'); expect(res2 === null || res2 === void 0 ? void 0 : res2.id).to.eql('ns:child-2a'); }); it('stop (walking)', () => { const tree = create(); const child1 = tree.add({ root: 'child-1' }); const child2a = child1.add({ root: 'child-2a' }); child1.add({ root: 'child-2b' }); child2a.add({ root: 'child-3' }); const list = []; const res = tree.find((e) => { list.push(e); if (e.key === 'child-2a') { e.stop(); } return e.key === 'child-3'; }); expect(list.map((e) => e.key)).to.eql(['child-1', 'child-2a']); expect(res).to.eql(undefined); }); }); describe('contains', () => { const tree = create(); const child1 = tree.add({ root: 'ns1:child-1' }); const child2a = child1.add({ root: { id: 'child-2a', children: [{ id: 'foo' }, { id: 'ns2:bar' }] }, }); const child3 = create({ root: 'child-3' }); const query = TreeQuery.create({ root: tree.root }); const foo = query.find((e) => e.key === 'foo'); const bar = query.find((e) => e.key === 'bar'); it('empty', () => { expect(tree.contains('')).to.eql(false); expect(tree.contains(' ')).to.eql(false); expect(tree.contains(undefined)).to.eql(false); expect(tree.contains(null)).to.eql(false); }); it('does not contain', () => { expect(tree.contains('404')).to.eql(false); expect(tree.contains({ id: '404' })).to.eql(false); expect(tree.contains((e) => false)).to.eql(false); expect(tree.contains(child3)).to.eql(false); expect(tree.contains(tree)).to.eql(false); }); it('does contain (via match function)', () => { const res = tree.contains((e) => e.id === child2a.id); expect(res).to.eql(true); }); it('does contain (via node-identifier)', () => { expect(foo).to.exist; expect(bar).to.exist; expect(tree.contains(child2a.id)).to.eql(true); expect(tree.contains(child2a)).to.eql(true); expect(tree.contains(foo)).to.eql(true); expect(tree.contains(foo === null || foo === void 0 ? void 0 : foo.id)).to.eql(true); }); it('does not contain child nodes within different descendent namespace', () => { expect(tree.contains(bar)).to.eql(false); expect(tree.contains(bar === null || bar === void 0 ? void 0 : bar.id)).to.eql(false); }); }); describe('walkDown', () => { const state = create({ root: 'root' }); const child1 = state.add({ root: { id: 'child-1' } }); const child2 = child1.add({ root: { id: 'child-2' } }); const child3 = child1.add({ root: { id: 'child-3' } }); const child4 = child3.add({ root: { id: 'child-4' } }); it('walkDown: no children (visits root)', () => { const state = create({ root: 'root' }); const items = []; state.walkDown((e) => items.push(e)); expect(items.length).to.eql(1); expect(items[0].id).to.eql(state.id); expect(items[0].key).to.eql(state.key); expect(items[0].namespace).to.eql(state.namespace); expect(items[0].level).to.eql(0); expect(items[0].index).to.eql(-1); }); it('walkDown: deep', () => { var _a; const items = []; state.walkDown((e) => items.push(e)); expect(items.length).to.eql(5); expect(items[0].id).to.eql(state.id); expect(items[1].id).to.eql(child1.id); expect(items[2].id).to.eql(child2.id); expect(items[3].id).to.eql(child3.id); expect(items[4].id).to.eql(child4.id); expect(items[0].level).to.eql(0); expect(items[1].level).to.eql(1); expect(items[2].level).to.eql(2); expect(items[3].level).to.eql(2); expect(items[4].level).to.eql(3); expect(items[0].index).to.eql(-1); expect(items[1].index).to.eql(0); expect(items[2].index).to.eql(0); expect(items[3].index).to.eql(1); expect(items[4].index).to.eql(0); expect(items[0].parent).to.eql(undefined); expect((_a = items[1].parent) === null || _a === void 0 ? void 0 : _a.id).to.eql(state.id); }); it('walkDown: stop', () => { const items = []; state.walkDown((e) => { if (e.level > 0) { e.stop(); } items.push(e); }); expect(items.length).to.eql(2); expect(items[0].id).to.eql(state.id); expect(items[1].id).to.eql(child1.id); }); it('walkDown: skip', () => { const items = []; state.walkDown((e) => { if (e.key === 'child-3') { e.skip(); } items.push(e); }); expect(items.length).to.eql(4); expect(items.map((e) => e.key)).to.not.include('child-4'); }); }); describe('syncFrom', () => { const tree1 = { id: 'foo:tree', children: [{ id: 'foo:child-1' }, { id: 'foo:child-2' }], }; const tree2 = { id: 'bar:tree', props: { label: 'hello' }, children: [{ id: 'bar:child-1' }, { id: 'bar:child-2' }], }; const tree3 = { id: 'zoo:tree', children: [{ id: 'zoo:child-1' }, { id: 'zoo:child-2' }], }; it('inserts within parent (new node)', () => { var _a, _b; const state1 = create({ root: tree1 }); const state2 = create({ root: tree2, parent: 'foo:child-1' }); const state3 = create({ root: tree3, parent: 'foo:child-1' }); expect((_a = state1.query.findById('foo:child-1')) === null || _a === void 0 ? void 0 : _a.children).to.eql(undefined); const res1 = state1.syncFrom({ source: state2 }); const res2 = state1.syncFrom({ source: state3 }); expect(res1.parent).to.eql('foo:child-1'); expect(res2.parent).to.eql('foo:child-1'); expect(state1.query.findById('foo:child-2')).to.eql({ id: 'foo:child-2' }); const node = state1.query.findById('foo:child-1'); expect(node === null || node === void 0 ? void 0 : node.props).to.eql(undefined); expect((_b = node === null || node === void 0 ? void 0 : node.children) === null || _b === void 0 ? void 0 : _b.length).to.eql(2); expect(((node === null || node === void 0 ? void 0 : node.children) || [])[0]).to.eql(state2.root); expect(((node === null || node === void 0 ? void 0 : node.children) || [])[1]).to.eql(state3.root); }); it('inserts within parent (replace existing node)', () => { var _a; const state1 = create({ root: tree1 }); const state2 = create({ root: tree2, parent: 'foo:child-1' }); const state3 = create({ root: tree3, parent: 'foo:child-1' }); state1.change((draft, ctx) => { const node = ctx.findById('foo:child-1'); if (node) { ctx.children(node, (children) => { children.push({ id: 'zoo:tree', props: { label: 'banging' } }); }); } }); const res1 = state1.syncFrom({ source: state2 }); const res2 = state1.syncFrom({ source: state3 }); expect(res1.parent).to.eql('foo:child-1'); expect(res2.parent).to.eql('foo:child-1'); const node = state1.query.findById('foo:child-1'); expect(node === null || node === void 0 ? void 0 : node.props).to.eql(undefined); expect((_a = node === null || node === void 0 ? void 0 : node.children) === null || _a === void 0 ? void 0 : _a.length).to.eql(2); expect(((node === null || node === void 0 ? void 0 : node.children) || [])[0]).to.eql(state3.root); expect(((node === null || node === void 0 ? void 0 : node.children) || [])[1]).to.eql(state2.root); }); it('no initial value inserted into target (observable passed rather than [TreeState])]', () => { var _a, _b; const state1 = create({ root: tree1 }); const state2 = create({ root: tree2, parent: 'foo:child-1' }); expect(((_a = state1.query.findById('foo:child-1')) === null || _a === void 0 ? void 0 : _a.children) || []).to.eql([]); const res = state1.syncFrom({ source: { event$: state2.event.$, parent: 'foo:child-1' } }); expect(res.parent).to.eql('foo:child-1'); expect(((_b = state1.query.findById('foo:child-1')) === null || _b === void 0 ? void 0 : _b.children) || []).to.eql([]); }); it('stays in sync', () => { var _a, _b, _c, _d, _e; const state1 = create({ root: tree1 }); const state2 = create({ root: tree2, parent: 'foo:child-1' }); const state3 = create({ root: tree3, parent: 'foo:child-1' }); state1.syncFrom({ source: state2 }); state1.syncFrom({ source: state3 }); const getChildren = () => { var _a; return ((_a = state1.query.findById('foo:child-1')) === null || _a === void 0 ? void 0 : _a.children) || []; }; let children = getChildren(); expect(children).to.eql([state2.root, state3.root]); state2.change((draft, ctx) => { ctx.children(draft, (children) => { ctx.props(draft, (props) => (props.label = 'derp')); ctx.props(children[0], (props) => (props.label = 'boo')); children.pop(); }); }); children = getChildren(); expect((_a = children[0].props) === null || _a === void 0 ? void 0 : _a.label).to.eql('derp'); expect((_b = children[0].children) === null || _b === void 0 ? void 0 : _b.length).to.eql(1); expect((_c = (children[0].children || [])[0].props) === null || _c === void 0 ? void 0 : _c.label).to.eql('boo'); state3.change((draft, ctx) => { ctx.props(draft, (props) => (props.label = 'barry')); }); children = getChildren(); expect((_d = children[0].props) === null || _d === void 0 ? void 0 : _d.label).to.eql('derp'); expect((_e = children[1].props) === null || _e === void 0 ? void 0 : _e.label).to.eql('barry'); }); it('stops syncing on dispose()', () => { var _a, _b, _c; const state1 = create({ root: tree1 }); const state2 = create({ root: tree2, parent: 'foo:child-1' }); const state3 = create({ root: tree3, parent: 'foo:child-1' }); const res1 = state1.syncFrom({ source: state2 }); const res2 = state1.syncFrom({ source: state3 }); const getChildren = () => { var _a; return ((_a = state1.query.findById('foo:child-1')) === null || _a === void 0 ? void 0 : _a.children) || []; }; expect(res1.isDisposed).to.eql(false); expect(res2.isDisposed).to.eql(false); res1.dispose(); expect(res1.isDisposed).to.eql(true); expect(res2.isDisposed).to.eql(false); state2.change((draft, ctx) => { ctx.props(draft, (props) => (props.label = 'change-1')); }); state3.change((draft, ctx) => { ctx.props(draft, (props) => (props.label = 'change-2')); }); let children = getChildren(); expect((_a = children[0].props) === null || _a === void 0 ? void 0 : _a.label).to.eql('hello'); expect((_b = children[1].props) === null || _b === void 0 ? void 0 : _b.label).to.eql('change-2'); res2.dispose(); expect(res2.isDisposed).to.eql(true); state3.change((draft, ctx) => { ctx.props(draft, (props) => (props.label = 'change-3')); }); children = getChildren(); expect((_c = children[1].props) === null || _c === void 0 ? void 0 : _c.label).to.eql('change-2'); }); it('stops syncing when [until$] fires', () => { var _a, _b; const state1 = create({ root: tree1 }); const state2 = create({ root: tree2, parent: 'foo:child-1' }); const until$ = new Subject(); state1.syncFrom({ source: state2, until$ }); const getChildren = () => { var _a; return ((_a = state1.query.findById('foo:child-1')) === null || _a === void 0 ? void 0 : _a.children) || []; }; state2.change((draft, ctx) => { ctx.props(draft, (props) => (props.label = 'change-1')); }); let children = getChildren(); expect((_a = children[0].props) === null || _a === void 0 ? void 0 : _a.label).to.eql('change-1'); until$.next(); state2.change((draft, ctx) => { ctx.props(draft, (props) => (props.label = 'change-2')); }); children = getChildren(); expect((_b = children[0]