import { expect } from 'chai'; import { component, ReduxApp, withId } from 'src'; import { Component } from 'src/components'; // tslint:disable:no-unused-expression describe(nameof(ReduxApp), () => { describe('constructor', () => { it("does not throw on null values", () => { @component class Root { public value: string = null; } // create component tree const app = new ReduxApp(new Root()); app.dispose(); }); it("components nested inside standard objects are constructed", () => { @component class Root { public first = { second: new Level2() }; } class Level2 { public third = new Level3(); } class Level3 { public some = new ThisIsAComponent(); } @component class ThisIsAComponent { public dispatchMe() { /* noop */ } } // create component tree const app = new ReduxApp(new Root()); expect(app.root.first.second.third.some).to.be.an.instanceOf(Component); app.dispose(); }); it("handles pre-loaded state", () => { @component class Root { public first = { second: new Level2() }; } class Level2 { public theComponent = new ThisIsAComponent(); } @component class ThisIsAComponent { public value: string = 'before'; public changeValue() { this.value = 'after'; } } const preLoadedState = { first: { second: { theComponent: { value: 'I am here!' } } } }; // create component tree const root = new Root(); expect(root.first.second.theComponent).to.be.an.instanceOf(ThisIsAComponent); expect(root.first.second.theComponent.value).to.eql('before'); // create the app const app = new ReduxApp(root, undefined, preLoadedState); expect(app.root.first.second.theComponent).to.be.an.instanceOf(Component); expect(app.root.first.second.theComponent.value).to.eql('I am here!'); // verify state is updating app.root.first.second.theComponent.changeValue(); expect(app.root.first.second.theComponent).to.be.an.instanceOf(Component); expect(app.root.first.second.theComponent.value).to.eql('after'); app.dispose(); }); }); describe('updateState', () => { it("component tree is not updated when 'updateState' options is turned off", () => { @component class App { public num = 0; public increment() { this.num = this.num + 1; } } const app = new ReduxApp(new App(), { updateState: false }); expect(app.root.num).to.eq(0); app.root.increment(); expect(app.root.num).to.eq(0); app.dispose(); }); it("store still updates when 'updateState' options is turned off", () => { @component class App { public num = 0; public increment() { this.num = this.num + 1; } } const app = new ReduxApp(new App(), { updateState: false }); expect(app.store.getState().num).to.eq(0); app.root.increment(); expect(app.store.getState().num).to.eq(1); app.dispose(); }); it('removes component properties that do not exists on the new state', () => { // create the component @component class MyComponent { public prop1: string = undefined; public prop2: string = undefined; public setAndRemove() { delete this.prop1; this.prop2 = 'hello'; } } const app = new ReduxApp(new MyComponent()); // test before expect(app.root).to.haveOwnProperty('prop1'); expect(app.root).to.haveOwnProperty('prop2'); // test after app.root.setAndRemove(); expect(app.root).to.not.haveOwnProperty('prop1'); expect(app.root).to.haveOwnProperty('prop2'); app.dispose(); }); it('does not remove component properties that exists on the new state but are undefined', () => { // create the component @component class MyComponent { public prop1: string = undefined; public prop2: string = undefined; public updateProp2Only() { this.prop2 = 'hello'; } } const app = new ReduxApp(new MyComponent()); // test before expect(app.root).to.haveOwnProperty('prop1'); expect(app.root).to.haveOwnProperty('prop2'); // test after app.root.updateProp2Only(); expect(app.root).to.haveOwnProperty('prop1'); expect(app.root).to.haveOwnProperty('prop2'); app.dispose(); }); it("components nested inside standard objects are synced with the store's state", () => { @component class Root { public first = { second: new Level2() }; } class Level2 { public third = new Level3(); } class Level3 { public some = new ThisIsAComponent(); } @component class ThisIsAComponent { public value = 0; public dispatchMe() { this.value = 1; } } // create component tree const app = new ReduxApp(new Root()); // before dispatching expect(app.root.first.second.third.some.value).to.eql(0); // after dispatching app.root.first.second.third.some.dispatchMe(); expect(app.root.first.second.third.some.value).to.eql(1); app.dispose(); }); it("methods of components nested inside standard objects can be invoked multiple times", () => { @component class Root { public first = { second: new Level2() }; } class Level2 { public third = new Level3(); } class Level3 { public counter = new Counter(); } @component class Counter { public value = 0; public increment() { this.value = this.value + 1; } } // create component tree const app = new ReduxApp(new Root()); expect(app.root.first.second.third.counter.value).to.eql(0); app.root.first.second.third.counter.increment(); expect(app.root.first.second.third.counter.value).to.eql(1); app.root.first.second.third.counter.increment(); expect(app.root.first.second.third.counter.value).to.eql(2); app.root.first.second.third.counter.increment(); expect(app.root.first.second.third.counter.value).to.eql(3); app.dispose(); }); it("updates arrays and contained components correctly", () => { @component class App { public parents = [new ParentComponent()]; } @component class ParentComponent { public arr: Component1[] = []; public push() { this.arr = this.arr.concat(new Component1()); } public pop() { this.arr = this.arr.slice(0, this.arr.length - 1); } public assign() { const newComp = new Component1(); newComp.value = 5; this.arr = this.arr.map((val, index) => index === 0 ? newComp : val); } public updateOdds() { this.arr.forEach((item, index) => { if (index % 2 === 1) { item.increment(); item.child.setMessage('hello_' + item.value); } }); } } @component class Component1 { public value = 0; @withId public child = new Component2(); public increment() { this.value = this.value + 1; } } @component class Component2 { public message = 'hello'; public setMessage(newMessage: string): void { this.message = newMessage; } } const app = new ReduxApp(new App()); // push expect(app.root.parents[0].arr.length).to.eql(0); app.root.parents[0].push(); app.root.parents[0].push(); expect(app.root.parents[0].arr.length).to.eql(2); // dispatch expect(app.root.parents[0].arr[0].child.message).to.eql('hello'); expect(app.root.parents[0].arr[1].child.message).to.eql('hello'); app.root.parents[0].updateOdds(); expect(app.root.parents[0].arr[0].child.message).to.eql('hello'); expect(app.root.parents[0].arr[1].child.message).to.eql('hello_1'); // pop app.root.parents[0].pop(); expect(app.root.parents[0].arr.length).to.eql(1); expect(app.root.parents[0].arr[0].child.message).to.eql('hello'); // assign app.root.parents[0].push(); app.root.parents[0].assign(); expect(app.root.parents[0].arr.length).to.eql(2); expect(app.root.parents[0].arr[0].value).to.eql(5); expect(app.root.parents[0].arr[1].value).to.eql(0); }); }); });