import { Store, makeObserver, connect } from "../src/store"; import { nextTick, nextMicroTick, makeTestFixture, makeTestWEnv } from "./helpers"; import { Env, Component } from "../src/component"; describe("basic use", () => { test("commit a mutation", () => { const state = { n: 1 }; const mutations = { inc(state, delta) { state.n += delta; } }; const store = new Store({ state, mutations }); expect(store.state.n).toBe(1); store.commit("inc", 14); expect(store.state.n).toBe(15); }); test("dispatch an action", () => { const state = { n: 1 }; const mutations = { inc(state, delta) { state.n += delta; } }; const actions = { inc({ commit }, delta) { commit("inc", delta); } }; const store = new Store({ state, mutations, actions }); expect(store.state.n).toBe(1); store.dispatch("inc", 14); expect(store.state.n).toBe(15); }); test("modifying state outside of mutations trigger error", () => { const state = { n: 1 }; const actions = { inc({ state }) { state.n++; } }; const store = new Store({ state, mutations: {}, actions }); expect(() => store.dispatch("inc")).toThrow(); expect(() => (store.state.n = 15)).toThrow(); }); test("can dispatch an action in an action", () => { const state = { n: 1 }; const mutations = { inc(state, delta) { state.n += delta; } }; const actions = { inc({ commit }, delta) { commit("inc", delta); }, inc100({ dispatch }) { dispatch("inc", 100); } }; const store = new Store({ state, mutations, actions }); expect(store.state.n).toBe(1); store.dispatch("inc100"); expect(store.state.n).toBe(101); }); test("dispatch allow synchronizing between actions", async () => { const state = { n: 1 }; const mutations = { inc(state, delta) { state.n += delta; }, setN(state, n) { state.n = n; } }; const actions = { async dosomething({ commit, dispatch }) { await dispatch("setTo10"); commit("inc", 3); }, async setTo10({ commit }) { await Promise.resolve(); commit("setN", 10); } }; const store = new Store({ state, mutations, actions }); expect(store.state.n).toBe(1); store.dispatch("dosomething"); expect(store.state.n).toBe(1); await nextTick(); expect(store.state.n).toBe(13); }); test("env is given to actions", () => { expect.assertions(1); const someEnv = {}; const actions = { someaction({ env }) { expect(env).toBe(someEnv); } }; const store = new Store({ state: {}, actions, env: someEnv }); store.dispatch("someaction"); }); }); describe("observer", () => { test("properly observe objects", () => { const observer = makeObserver(); const obj: any = {}; observer.observe(obj); expect(obj.__rev__).toBe(0); expect(observer.__rev__).toBe(0); const ob2: any = { a: 1 }; observer.observe(ob2); expect(ob2.__rev__).toBe(0); ob2.a = 2; expect(observer.__rev__).toBe(1); expect(ob2.__rev__).toBe(1); ob2.b = 3; expect(observer.__rev__).toBe(1); expect(ob2.__rev__).toBe(1); observer.set(ob2, "b", 4); expect(observer.__rev__).toBe(2); expect(ob2.__rev__).toBe(2); }); test("various object property changes", () => { const observer = makeObserver(); const obj: any = { a: 1 }; observer.observe(obj); expect(obj.__rev__).toBe(0); obj.a = 2; expect(observer.__rev__).toBe(1); expect(obj.__rev__).toBe(1); // same value again obj.a = 2; expect(observer.__rev__).toBe(1); expect(obj.__rev__).toBe(1); obj.a = 3; expect(observer.__rev__).toBe(2); expect(obj.__rev__).toBe(2); }); test("properly observe arrays", () => { const observer = makeObserver(); const arr: any = []; observer.observe(arr); expect(arr.__rev__).toBe(0); expect(observer.__rev__).toBe(0); expect(arr.length).toBe(0); arr.push(1); expect(arr.__rev__).toBe(1); expect(observer.__rev__).toBe(1); expect(arr.length).toBe(1); }); test("properly observe arrays in object", () => { const observer = makeObserver(); const state: any = { arr: [] }; observer.observe(state); expect(state.arr.__rev__).toBe(0); expect(observer.__rev__).toBe(0); expect(state.arr.length).toBe(0); state.arr.push(1); expect(state.arr.__rev__).toBe(1); expect(observer.__rev__).toBe(1); expect(state.arr.length).toBe(1); }); test("reobserve new object values", () => { const observer = makeObserver(); const obj: any = { a: 1 }; observer.observe(obj); expect(obj.__rev__).toBe(0); obj.a = { b: 2 }; expect(observer.__rev__).toBe(1); expect(obj.__rev__).toBe(1); expect(obj.a.__rev__).toBe(0); obj.a.b = 3; expect(observer.__rev__).toBe(2); expect(obj.__rev__).toBe(1); expect(obj.a.__rev__).toBe(1); }); test("properly handle already observed state", () => { const observer = makeObserver(); const obj1: any = { a: 1 }; const obj2: any = { b: 1 }; observer.observe(obj1); observer.observe(obj2); expect(obj1.__rev__).toBe(0); expect(obj2.__rev__).toBe(0); obj1.a = 2; obj2.b = 3; expect(obj1.__rev__).toBe(1); expect(obj2.__rev__).toBe(1); obj2.b = obj1; expect(obj1.__rev__).toBe(1); expect(obj2.__rev__).toBe(2); }); test("accept cycles in observed state", () => { const observer = makeObserver(); const obj1: any = {}; const obj2: any = { b: obj1, key: 1 }; obj1.a = obj2; observer.observe(obj1); expect(obj1.__rev__).toBe(0); expect(obj2.__rev__).toBe(0); obj2.key = 3; expect(obj1.__rev__).toBe(0); expect(obj2.__rev__).toBe(1); }); }); describe("advanced state properties", () => { test("state in the store is reference equal after mutation", async () => { const state = {}; const mutations = { donothing() {} }; const store = new Store({ state, mutations }); expect(store.state).toBe(state); store.commit("donothing"); expect(store.state).toBe(state); }); test("can use array properties in mutations", () => { expect.assertions(3); const state = { a: [1, 2, 3] }; const mutations = { m(state) { expect(state.a.length).toBe(3); const l = state.a.push(53); expect(l).toBe(4); } }; const store = new Store({ state, mutations }); store.commit("m"); expect(store.state.a).toEqual([1, 2, 3, 53]); }); test("can use object assign in store", async () => { const mutations = { dosomething(state) { Object.assign(state.westmalle, { a: 3, b: 4 }); } }; const store = new Store({ state: { westmalle: { a: 1, b: 2 } }, mutations }); store.commit("dosomething"); expect(store.state.westmalle).toEqual({ a: 3, b: 4 }); }); test("aku reactive store state 1", async () => { const mutations = { inc(state) { state.counter++; } }; const state = { counter: 0 }; const store = new Store({ state, mutations }); expect(store.state.counter).toBe(0); store.commit("inc", {}); expect(store.state.counter).toBe(1); }); }); describe("updates triggered by the store", () => { test("multiple commits trigger one update", async () => { let updateCounter = 0; const state = { n: 1 }; const mutations = { inc(state, delta) { state.n += delta; } }; const store = new Store({ state, mutations }); store.on("update", null, () => updateCounter++); store.commit("inc", 14); expect(updateCounter).toBe(0); store.commit("inc", 50); expect(updateCounter).toBe(0); await nextMicroTick(); expect(updateCounter).toBe(1); }); test("empty commits do not trigger updates", async () => { let updateCounter = 0; const state = { n: 1 }; const mutations = { inc(state, delta) { state.n += delta; }, noop() {}, noop2(state) { const val = state.n; state.n = val; } }; const store = new Store({ state, mutations }); store.on("update", null, () => updateCounter++); store.commit("noop"); await nextMicroTick(); expect(updateCounter).toBe(0); store.commit("inc", 50); await nextMicroTick(); expect(updateCounter).toBe(1); store.commit("noop2"); await nextMicroTick(); expect(updateCounter).toBe(1); }); }); describe("connecting a component to store", () => { let fixture: HTMLElement; let env: Env; beforeEach(() => { fixture = makeTestFixture(); env = makeTestWEnv(); }); afterEach(() => { fixture.remove(); }); class App extends Component { inlineTemplate = `
`; widgets = { Todo }; } class Todo extends Component { inlineTemplate = ``; } test("connecting a component works", async () => { const state = { todos: [] }; const mutations = { addTodo(state, msg) { state.todos.push({ msg }); } }; function mapStateToProps(s) { return { todos: s.todos }; } const TodoApp = connect(mapStateToProps)(App); const store = new Store({ state, mutations }); (env).store = store; const app = new TodoApp(env); await app.mount(fixture); expect(fixture.innerHTML).toMatchSnapshot(); store.commit("addTodo", "hello"); await nextTick(); expect(fixture.innerHTML).toMatchSnapshot(); }); test("connected child components with custom hooks", async () => { let steps: any = []; class Child extends Component { inlineTemplate = `
`; mounted() { steps.push("child:mounted"); } willUnmount() { steps.push("child:willUnmount"); } } const ConnectedChild = connect(s => s)(Child); class Parent extends Component { inlineTemplate = `
`; widgets = { ConnectedChild }; constructor(env: Env) { super(env); this.state = { child: true }; } } const store = new Store({ state: {} }); (env).store = store; const parent = new Parent(env); await parent.mount(fixture); expect(steps).toEqual(["child:mounted"]); await parent.updateState({ child: false }); expect(steps).toEqual(["child:mounted", "child:willUnmount"]); }); test("connect receives ownprops as second argument", async () => { const state = { todos: [{ id: 1, text: "jupiler" }] }; let nextId = 2; const mutations = { addTodo(state, text) { state.todos.push({ text, id: nextId++ }); } }; const store = new Store({ state, mutations }); class TodoItem extends Component { inlineTemplate = ``; } const ConnectedTodo = connect((state, props) => { const todo = state.todos.find(t => t.id === props.id); return todo; })(TodoItem); class TodoList extends Component { inlineTemplate = `
`; widgets = { ConnectedTodo }; } function mapStateToProps(state) { return { todos: state.todos }; } const ConnectedTodoList = connect(mapStateToProps)(TodoList); (env).store = store; const app = new ConnectedTodoList(env); await app.mount(fixture); expect(fixture.innerHTML).toBe("
jupiler
"); store.commit("addTodo", "hoegaarden"); await nextTick(); expect(fixture.innerHTML).toBe( "
jupilerhoegaarden
" ); }); test("connected component is updated when props are updated", async () => { class Beer extends Component { inlineTemplate = ``; } const ConnectedBeer = connect((state, props) => { return state.beers[props.id]; })(Beer); class App extends Component { inlineTemplate = `
`; widgets = { ConnectedBeer }; state = { beerId: 1 }; } const state = { beers: { 1: { name: "jupiler" }, 2: { name: "kwak" } } }; const store = new Store({ state }); (env).store = store; const app = new App(env); await app.mount(fixture); expect(fixture.innerHTML).toBe("
jupiler
"); await app.updateState({ beerId: 2 }); expect(fixture.innerHTML).toBe("
kwak
"); }); });