mst-effect
Version:
Designed to be used with MobX-State-Tree to create asynchronous actions using RxJS.
191 lines (153 loc) • 5.44 kB
text/typescript
import { timer, Observable } from 'rxjs'
import { map, startWith, endWith, switchMap } from 'rxjs/operators'
import { types as mstTypes } from 'mobx-state-tree'
import { types, effect, action, destroy, NOOP, ValidEffectActions } from '../src'
describe('effect', () => {
describe(`action`, () => {
it(`should execute the action immediately`, () => {
const spy = jest.fn()
const Model = types.model().actions((self) => ({
log: effect<string>(self, (payload$) =>
payload$.pipe(map((message) => action(spy, message))),
),
}))
const model = Model.create()
model.log('hi!')
expect(spy.mock.calls).toEqual([['hi!']])
})
it(`should accept 'NOOP' as valid action`, () => {
const spy = jest.fn()
const Model = types.model().actions((self) => ({
run: effect<ValidEffectActions>(self, (payload$) => payload$),
}))
const model = Model.create()
model.run(NOOP)
model.run(action(spy))
expect(spy.mock.calls).toEqual([[]])
})
it(`should accept an array of actions`, () => {
const spy = jest.fn()
const Model = types.model().actions((self) => ({
log: effect<number>(self, (payload$) =>
payload$.pipe(
map((times) => Array.from({ length: times }).map((_, index) => action(spy, index))),
),
),
}))
const model = Model.create()
model.log(3)
expect(spy.mock.calls).toEqual([[0], [1], [2]])
})
it(`should ignore 'NOOP' inside action array`, () => {
const spy = jest.fn()
const Model = types.model().actions((self) => ({
log: effect<string>(self, (payload$) =>
payload$.pipe(map((message) => [NOOP, action(spy, message), NOOP])),
),
}))
const model = Model.create()
model.log('hi!')
expect(spy.mock.calls).toEqual([['hi!']])
})
it(`should able to modify model inside action`, () => {
jest.useFakeTimers()
const Model = types.model({ value: types.string }).actions((self) => ({
setValue: effect<string>(self, (payload$) => {
function updateValue(value: string) {
self.value = value
}
return payload$.pipe(
switchMap((newValue) => timer(1000).pipe(map(() => action(updateValue, newValue)))),
)
}),
}))
const model = Model.create({ value: '' })
model.setValue('123')
jest.advanceTimersByTime(1000)
expect(model.value).toBe('123')
jest.useRealTimers()
})
it(`should warning if action invalid`, () => {
const spy = jest.spyOn(console, 'warn').mockImplementation()
const Model = types.model().actions((self) => ({
emit: effect<any>(self, (payload$) => payload$),
}))
const model = Model.create()
const invalidActions = [
'string',
123,
true,
false,
undefined,
null,
Symbol('invalid action'),
{},
function invalidAction() {},
]
invalidActions.forEach((invalidAction) => model.emit(invalidAction))
expect(spy.mock.calls.length).toBe(invalidActions.length)
expect(spy.mock.calls).toMatchSnapshot()
spy.mockRestore()
})
it(`should warning if 'types' is not imported from 'mst-effect'`, () => {
const spy = jest.spyOn(console, 'warn').mockImplementation()
const Model = mstTypes.model().actions((self) => ({
emit: effect(self, (payload$) => payload$.pipe(map(() => NOOP))),
}))
Model.create()
expect(spy.mock.calls).toMatchSnapshot()
spy.mockRestore()
})
})
describe(`subscription`, () => {
it(`should subscribe the Observable when create`, () => {
const spy = jest.fn()
const Model = types.model().actions((self) => ({
log: effect<never>(self, (payload$) => payload$.pipe(startWith(action(spy, 'start')))),
}))
Model.create()
expect(spy.mock.calls).toEqual([['start']])
})
it(`should complete the Observable when destroy`, () => {
const spy = jest.fn()
const Model = types.model().actions((self) => ({
log: effect<never>(self, (payload$) => payload$.pipe(endWith(action(spy, 'end')))),
}))
const model = Model.create()
destroy(model)
expect(spy.mock.calls).toEqual([['end']])
})
it(`should unsubscribe the Observable when destroy`, () => {
const spy = jest.fn()
const Model = types.model().actions((self) => ({
log: effect<never>(self, () => new Observable(() => spy)),
}))
const model = Model.create()
destroy(model)
expect(spy).toBeCalledTimes(1)
})
})
it(`should keep alive after error occur`, () => {
const spy = jest.fn()
const spyError = jest.spyOn(console, 'error').mockImplementation()
const Model = types.model().actions((self) => ({
throwErr: effect<boolean>(self, (payload$) =>
payload$.pipe(
map((throwErr) => {
if (throwErr) {
throw new Error('')
} else {
return action(spy)
}
}),
),
),
}))
const model = Model.create()
model.throwErr(true)
expect(spy.mock.calls).toEqual([])
expect(spyError.mock.calls.length).toBe(1)
model.throwErr(false)
expect(spy.mock.calls).toEqual([[]])
})
})