import { IAction, Overmind } from 'overmind' import simple from 'simple-mock' import { describe, expect, it, restore } from 'test' import { Block, build, settings } from './index.js' import { Setup } from './types.js' const foo = { name: 'foo', state: { foo: { hop: 'hop', }, }, actions: { foobar: { foo: () => {}, }, }, } type Action = IAction const doBar: Action = () => {} const bar = { name: 'bar', state: { bar: 'barman', }, actions: { doBar, foobar: { bar: () => {}, }, }, effects: { api: {}, }, settings: { bong: 'bong', }, } const foobaz = { name: 'foobaz', init() {}, state: { foo: { baz: 'baz', }, }, } type BongSettings = { bong: string } const bongApp: Block<{}, BongSettings> = { name: 'bongApp', settings: { bong: 'bongApp settings', }, } const bongUser = { name: 'bongUser', settings: { bong: 'bongUser settings', }, } describe('build', () => { afterEach(restore) it('should deep merge state', () => { const config = build(foo).using(bar).using(foobaz).config() expect(config.state).toEqual({ foo: { hop: 'hop', baz: 'baz' }, bar: 'barman', }) }) it('should deep merge actions', () => { const config = build(foo).using(bar).config() expect(Object.keys(config.actions.foobar).sort()).toEqual(['bar', 'foo']) }) it('should deep copy array', () => { const blob = { name: 'blob', state: { blob: { list: ['1', '2'], }, }, actions: { blob: { push: (ctx: any, value: string) => { ctx.state.blob.list.push(value) }, }, }, } const app1 = build(blob).app() const app2 = build(blob).app() app1.actions.blob.push('3') expect(app1.state.blob.list).toEqual(['1', '2', '3']) expect(app2.state.blob.list).toEqual(['1', '2']) }) it('should produce app', () => { const app = build(foo).using(bar).using(foobaz).app() expect(app instanceof Overmind).toBe(true) }) it('should throw `missing config() or app()`', () => { const config: any = build(foo).using(bar).using(foobaz) expect(config.state.message).toEqual( `Please run 'config()' or 'app()' to finish build.` ) }) it('should merge from last to first', () => { const config = build({ name: 'foo', state: { foo: 'foo' } }) .using({ name: 'bar', state: { foo: 'baz' }, }) .app() expect(config.state).toEqual({ foo: 'foo' }) }) it('should produce state type', () => { const config = build(foo).using(bar).using(foobaz).app() const x = config.state.foo.baz // Should not pass for TS // @ts-ignore const s = config.settings expect(x).toEqual('baz') expect(s).toBe(undefined) }) it('should throw on incompatible types', () => { expect(() => build({ name: 'app', state: { b: 'x' } }) .using({ name: 'foo', state: { b: { c: 'c' } } }) .app() ).toThrow( `Cannot merge: incompatible types at path 'state.b' (block 'app' has 'string' instead of 'object').` ) }) it('should throw on incompatible types (non-objects)', () => { expect(() => build({ name: 'app', state: { b: 4 } }) .using({ name: 'foo', state: { b: 'c' } }) .app() ).toThrow( `Cannot merge: incompatible types at path 'state.b' (block 'app' has 'number' instead of 'string').` ) }) it('should run init with all settings', () => { let test: any = 'init not run' const bong: Block<{}, BongSettings> = { name: 'bong', setup(_config, settings) { test = settings }, } build(bongApp).using(bongUser).using(bong).app() expect(test).toEqual({ bongApp: 'bongApp settings', bongUser: 'bongUser settings', }) }) it('should pass settings in using order', () => { let test: string[] = [] const bong: Block<{}, BongSettings> = { name: 'bong', setup(_config, settings) { test = Object.keys(settings) }, } build(bongApp).using(bongUser).using(bong).app() expect(test).toEqual(['bongUser', 'bongApp']) }) it('should throw on redefined actions', () => { const a = { name: 'a', actions: { doThis() {}, }, } const b = { name: 'b', actions: { doThis() {}, }, } expect(() => build(a).using(b).app()).toThrow( `Cannot redefine 'actions.doThis'.` ) }) it('should throw on action with object', () => { const a = { name: 'a', actions: { foo: { doThis() {}, }, }, } const b = { name: 'b', actions: { foo() {}, }, } expect(() => build(a).using(b).app()).toThrow( `Cannot redefine 'actions.foo'.` ) }) it('should throw on invalid type', () => { const a = { name: 'a', actions: { foo: { doThis: [], }, }, } const b = { name: 'b', actions: {}, } expect(() => build(a).using(b).app()).toThrow( `Value at 'actions.foo.doThis' is not a function.` ) }) it('should run setup without settings', () => { let test = '' const a = { name: 'a', setup() { test = 'setup' }, } build(a).config() expect(test).toBe('setup') }) it('should throw on redefined effects', () => { const a = { name: 'a', effects: { api: { blah() {} }, }, } const b = { name: 'b', effects: { api: { blah() {} }, }, } expect(() => build(a).using(b).app()).toThrow( `Cannot redefine 'effects.api.blah'.` ) }) it('should add dependencies', () => { const d = { name: 'd', state: { d: 'd', }, } const c = { name: 'c', state: { c: 'c', }, } const b = { name: 'b', state: { b: 'b', }, dependencies: [c], } const a = { name: 'a', state: { a: 'a', }, dependencies: [b, c, d], } expect(build(a).config().state).toEqual({ a: 'a', b: 'b', c: 'c', d: 'd' }) }) it('should throw on missing name', () => { simple.mock(console, 'log').callFn(() => {}) expect(() => build({ state: {} } as any).config()).toThrow(``) }) it('should parse initializers', async () => { const test: string[] = [] const app = build({ name: 'foo', onInitialize: () => test.push('foo'), }) .using({ name: 'bar', onInitialize: () => test.push('bar'), }) .app() await app.initialized expect(test).toEqual(['bar', 'foo']) }) it('should halt on false return', async () => { const test: string[] = [] const app = build({ name: 'foo', onInitialize: () => { test.push('foo') }, }) .using({ name: 'bar', onInitialize: () => { test.push('bar') return false }, }) .app() await app.initialized expect(test).toEqual(['bar']) }) it('should have valid Setup type', () => { // This should not break on test compilation. // TODO: It would be better to have proper type tests. interface MySettings { my?: { foo: string bar: number } } interface MyConfig { state: { test: { foo: string bar: number } } } const setup: Setup = (config, settings) => { // Test unwrap config.state.test = settings['foo'] } const my = { name: 'my', setup } // Use of MySettings const config = { name: 'foo', settings: settings({ my: { foo: 'fox', bar: 3, }, }), state: { test: { foo: 'foo', bar: 2 }, }, } expect(build(config).using(my).app().state.test).toEqual({ foo: 'fox', bar: 3, }) }) })