@platform/state
Version:
A small, simple, strongly typed, [rx/observable] state-machine.
1,045 lines • 52.8 kB
JavaScript
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]