diff --git a/TODO.md b/TODO.md index 4bfc456..ceb0c57 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,8 @@ TODO for `1.3.0` === - - [ ] add documentation for all new reactor options + - [x] add documentation for all new reactor options + - [x] add tests for all new reactor options - [ ] link the nuclear-js package from the hot reloadable example - [ ] link `0.3.0` of `nuclear-js-react-addons` in hot reloadable example - [ ] add `nuclear-js-react-addons` link in example and documentation diff --git a/docs/src/docs/07-api.md b/docs/src/docs/07-api.md index 9c3bdc8..bde2210 100644 --- a/docs/src/docs/07-api.md +++ b/docs/src/docs/07-api.md @@ -31,11 +31,13 @@ If `config.debug` is true then all of the options below will be enabled. `logDirtyStores` (default=`false`) console.logs what stores have changed after each dispatched action. -`throwOnUndefinedDispatch` (default=`false`) if true, throws an Error if a store ever returns undefined. +`throwOnUndefinedActionType` (default=`false`) if true, throws an Error when dispatch is called with an undefined action type. + +`throwOnUndefinedStoreReturnValue` (default=`false`) if true, throws an Error if a store handler or `getInitialState()` ever returns `undefined`. `throwOnNonImmutableStore` (default=`false`) if true, throws an Error if a store returns a non-immutable value. Javascript primitive such as `String`, `Boolean` and `Number` count as immutable. -`throwOnDispatchInDispatch` (default=`true`) if true, throws an Error if a dispatch occurs in a change observer. +`throwOnDispatchInDispatch` (default=`false`) if true, throws an Error if a dispatch occurs in a change observer. **Example** diff --git a/src/reactor/fns.js b/src/reactor/fns.js index 609f090..7f47527 100644 --- a/src/reactor/fns.js +++ b/src/reactor/fns.js @@ -34,6 +34,9 @@ export function registerStores(reactorState, stores) { const initialState = store.getInitialState() + if (initialState === undefined && getOption(reactorState, 'throwOnUndefinedStoreReturnValue')) { + throw new Error('Store getInitialState() must return a value, did you forget a return statement') + } if (getOption(reactorState, 'throwOnNonImmutableStore') && !isImmutableValue(initialState)) { throw new Error('Store getInitialState() must return an immutable value, did you forget to call toImmutable') } @@ -58,14 +61,7 @@ export function registerStores(reactorState, stores) { export function replaceStores(reactorState, stores) { return reactorState.withMutations((reactorState) => { each(stores, (store, id) => { - const initialState = store.getInitialState() - - if (getOption(reactorState, 'throwOnNonImmutableStore') && !isImmutableValue(initialState)) { - throw new Error('Store getInitialState() must return an immutable value, did you forget to call toImmutable') - } - - reactorState - .update('stores', stores => stores.set(id, store)) + reactorState.update('stores', stores => stores.set(id, store)) }) }) } @@ -77,6 +73,10 @@ export function replaceStores(reactorState, stores) { * @return {ReactorState} */ export function dispatch(reactorState, actionType, payload) { + if (actionType === undefined && getOption(reactorState, 'throwOnUndefinedActionType')) { + throw new Error('`dispatch` cannot be called with an `undefined` action type.'); + } + const currState = reactorState.get('state') let dirtyStores = reactorState.get('dirtyStores') @@ -96,7 +96,7 @@ export function dispatch(reactorState, actionType, payload) { throw e } - if (getOption(reactorState, 'throwOnUndefinedDispatch') && newState === undefined) { + if (newState === undefined && getOption(reactorState, 'throwOnUndefinedStoreReturnValue')) { const errorMsg = 'Store handler must return a value, did you forget a return statement' logging.dispatchError(reactorState, errorMsg) throw new Error(errorMsg) @@ -297,7 +297,7 @@ export function reset(reactorState) { storeMap.forEach((store, id) => { const storeState = prevState.get(id) const resetStoreState = store.handleReset(storeState) - if (getOption(reactorState, 'throwOnUndefinedDispatch') && resetStoreState === undefined) { + if (resetStoreState === undefined && getOption(reactorState, 'throwOnUndefinedStoreReturnValue')) { throw new Error('Store handleReset() must return a value, did you forget a return statement') } if (getOption(reactorState, 'throwOnNonImmutableStore') && !isImmutableValue(resetStoreState)) { diff --git a/src/reactor/records.js b/src/reactor/records.js index 3888aef..a59314f 100644 --- a/src/reactor/records.js +++ b/src/reactor/records.js @@ -7,9 +7,11 @@ export const PROD_OPTIONS = Map({ logAppState: false, // logs what stores changed after a dispatch logDirtyStores: false, + // if true, throws an error when dispatching an `undefined` actionType + throwOnUndefinedActionType: false, // if true, throws an error if a store returns undefined - throwOnUndefinedDispatch: false, - // if true, throws an error if a store returns undefined + throwOnUndefinedStoreReturnValue: false, + // if true, throws an error if a store.getInitialState() returns a non immutable value throwOnNonImmutableStore: false, // if true, throws when dispatching in dispatch throwOnDispatchInDispatch: false, @@ -22,9 +24,11 @@ export const DEBUG_OPTIONS = Map({ logAppState: true, // logs what stores changed after a dispatch logDirtyStores: true, + // if true, throws an error when dispatching an `undefined` actionType + throwOnUndefinedActionType: true, // if true, throws an error if a store returns undefined - throwOnUndefinedDispatch: true, - // if true, throws an error if a store returns undefined + throwOnUndefinedStoreReturnValue: true, + // if true, throws an error if a store.getInitialState() returns a non immutable value throwOnNonImmutableStore: true, // if true, throws when dispatching in dispatch throwOnDispatchInDispatch: true, diff --git a/tests/reactor-tests.js b/tests/reactor-tests.js index d346fea..bf095db 100644 --- a/tests/reactor-tests.js +++ b/tests/reactor-tests.js @@ -33,7 +33,8 @@ describe('Reactor', () => { expect(getOption(reactor.reactorState, 'logDispatches')).toBe(true) expect(getOption(reactor.reactorState, 'logAppState')).toBe(false) expect(getOption(reactor.reactorState, 'logDirtyStores')).toBe(false) - expect(getOption(reactor.reactorState, 'throwOnUndefinedDispatch')).toBe(false) + expect(getOption(reactor.reactorState, 'throwOnUndefinedActionType')).toBe(false) + expect(getOption(reactor.reactorState, 'throwOnUndefinedStoreReturnValue')).toBe(false) expect(getOption(reactor.reactorState, 'throwOnNonImmutableStore')).toBe(false) expect(getOption(reactor.reactorState, 'throwOnDispatchInDispatch')).toBe(false) }) @@ -48,12 +49,253 @@ describe('Reactor', () => { expect(getOption(reactor.reactorState, 'logDispatches')).toBe(false) expect(getOption(reactor.reactorState, 'logAppState')).toBe(true) expect(getOption(reactor.reactorState, 'logDirtyStores')).toBe(true) - expect(getOption(reactor.reactorState, 'throwOnUndefinedDispatch')).toBe(true) + expect(getOption(reactor.reactorState, 'throwOnUndefinedActionType')).toBe(true) + expect(getOption(reactor.reactorState, 'throwOnUndefinedStoreReturnValue')).toBe(true) expect(getOption(reactor.reactorState, 'throwOnNonImmutableStore')).toBe(true) expect(getOption(reactor.reactorState, 'throwOnDispatchInDispatch')).toBe(false) }) }) + describe('options', () => { + describe('throwOnUndefinedActionType', () => { + it('should NOT throw when `false`', () => { + var reactor = new Reactor({ + options: { + throwOnUndefinedActionType: false, + }, + }) + + expect(() => { + reactor.dispatch(undefined) + }).not.toThrow() + }) + + it('should throw when `true`', () => { + var reactor = new Reactor({ + options: { + throwOnUndefinedActionType: true, + }, + }) + + expect(() => { + reactor.dispatch(undefined) + }).toThrow() + }) + }) + + describe('throwOnUndefinedStoreReturnValue', () => { + it('should NOT throw during `registerStores`, `dispatch` or `reset` when `false`', () => { + var reactor = new Reactor({ + options: { + throwOnUndefinedStoreReturnValue: false, + }, + }) + + expect(() => { + reactor.registerStores({ + store: Store({ + getInitialState() { + return undefined + }, + initialize() { + this.on('action', () => undefined) + }, + }) + }) + reactor.dispatch('action') + reactor.reset() + }).not.toThrow() + }) + + it('should throw during `registerStores` when `true`', () => { + var reactor = new Reactor({ + options: { + throwOnUndefinedStoreReturnValue: true, + }, + }) + + expect(() => { + reactor.registerStores({ + store: Store({ + getInitialState() { + return undefined + }, + initialize() { + this.on('action', () => undefined) + }, + }) + }) + }).toThrow() + }) + + it('should throw during `dispatch` when `true`', () => { + var reactor = new Reactor({ + options: { + throwOnUndefinedStoreReturnValue: true, + }, + }) + + expect(() => { + reactor.registerStores({ + store: Store({ + getInitialState() { + return undefined + }, + initialize() { + this.on('action', () => undefined) + }, + }) + }) + }).toThrow() + }) + + it('should throw during `reset` when `true`', () => { + var reactor = new Reactor({ + options: { + throwOnUndefinedStoreReturnValue: true, + }, + }) + + expect(() => { + reactor.registerStores({ + store: Store({ + getInitialState() { + return 1 + }, + handleReset() { + return undefined + } + }) + }) + reactor.reset() + }).toThrow() + }) + }) + + describe('throwOnNonImmutableStore', () => { + it('should NOT throw during `registerStores` or `reset` when `false`', () => { + var reactor = new Reactor({ + options: { + throwOnNonImmutableStore: false, + }, + }) + + expect(() => { + reactor.registerStores({ + store: Store({ + getInitialState() { + return { foo: 'bar' } + }, + handleReset() { + return { foo: 'baz' } + }, + }) + }) + reactor.reset() + }).not.toThrow() + }) + + it('should throw during `registerStores` when `true`', () => { + var reactor = new Reactor({ + options: { + throwOnNonImmutableStore: true, + }, + }) + + expect(() => { + reactor.registerStores({ + store: Store({ + getInitialState() { + return { foo: 'bar' } + }, + }) + }) + }).toThrow() + }) + + it('should throw during `reset` when `true`', () => { + var reactor = new Reactor({ + options: { + throwOnNonImmutableStore: true, + }, + }) + + expect(() => { + reactor.registerStores({ + store: Store({ + getInitialState() { + return 123 + }, + handleReset() { + return { foo: 'baz' } + }, + }) + }) + reactor.reset() + }).toThrow() + }) + }) + + describe('throwOnDispatchInDispatch', () => { + it('should NOT throw when `false`', () => { + var reactor = new Reactor({ + options: { + throwOnDispatchInDispatch: false, + }, + }) + + expect(() => { + reactor.registerStores({ + count: Store({ + getInitialState() { + return 1 + }, + initialize() { + this.on('increment', curr => curr + 1) + } + }) + }) + + reactor.observe(['count'], (val) => { + if (val % 2 === 0) { + reactor.dispatch('increment') + } + }) + reactor.dispatch('increment') + expect(reactor.evaluate(['count'])).toBe(3) + }).not.toThrow() + }) + + it('should throw when `true`', () => { + var reactor = new Reactor({ + options: { + throwOnDispatchInDispatch: true, + }, + }) + + expect(() => { + reactor.registerStores({ + count: Store({ + getInitialState() { + return 1 + }, + initialize() { + this.on('increment', curr => curr + 1) + } + }) + }) + + reactor.observe(['count'], (val) => { + if (val % 2 === 0) { + reactor.dispatch('increment') + } + }) + reactor.dispatch('increment') + }).toThrow() + }) + }) + }) + describe('Reactor with no initial state', () => { var checkoutActions var reactor