UNPKG

algebraic-effects

Version:

Algebraic effects in react using generators

234 lines (192 loc) 6.54 kB
import { createEffect, func, composeEffects, composeHandlers } from '../src'; import { sleep } from '../src/operations'; describe('createEffect', () => { const ConsoleEff = createEffect('ConsoleEff', { log: func(['...data']), }); const ApiEffect = createEffect('ApiEffect', { fetch: func(['url', '?request'], '*'), }); describe('Effect type', () => { it('should have type info', () => { expect(ApiEffect.name).toBe('ApiEffect'); }); it('should have fetch operation', () => { expect(ApiEffect.fetch).toBeInstanceOf(Function); expect(ApiEffect.fetch('/').name).toBe('fetch'); expect(ApiEffect.fetch('/').payload).toEqual(['/']); }); }); describe('Runner', () => { it('should have runner effect name', () => { const runner = ApiEffect.handler({ fetch: () => () => {}, }); expect(runner.effectName).toBe('ApiEffect'); }); it('should reject promise the op fails arg check', done => { function* action() { yield ApiEffect.fetch(); } ApiEffect.handler({ fetch: () => () => {} }) .run(action) .then(() => done('Shoundt have ben called')) .catch(e => { expect(e.message).toContain('ArgumentError'); done(); }); }); it('should reject promise for invalid program', done => { const notAGenerator = () => {}; ApiEffect.handler({ fetch: () => () => {} }) .run(notAGenerator) .then(() => done('shdnt be here')) .catch(e => { expect(e.message).toContain('Invalid generator'); done(); }); }); }); describe('createRunner#with & createRunner#concat', () => { const DummyEff = createEffect('DummyEff', { myFn: func() }); it('should compose handler with another handler using .with', done => { const action = function *() { yield DummyEff.myFn(); yield ConsoleEff.log(); yield 'Yo'; }; const myFn = jest.fn(); const logg = jest.fn(); const dummy = DummyEff.handler({ myFn: ({ resume }) => () => resume(myFn()) }); const konsole = ConsoleEff.handler({ log: ({ resume }) => d => resume(logg(d)) }); const run = dummy.with(konsole); expect(run.effectName).toBe('DummyEff.ConsoleEff'); run(action) .then(() => { expect(myFn).toBeCalledTimes(1); expect(logg).toBeCalledTimes(1); done(); }) .catch(done); }); it('should compose handler with another handler using .concat', done => { const action = function *() { yield DummyEff.myFn(); yield ConsoleEff.log(); yield 'Yo'; }; const myFn = jest.fn(); const logg = jest.fn(); const dummy = DummyEff.handler({ myFn: ({ resume }) => () => resume(myFn()) }); const konsole = ConsoleEff.handler({ log: ({ resume }) => d => resume(logg(d)) }); const run = dummy.concat(konsole); expect(run.effectName).toBe('DummyEff.ConsoleEff'); run(action) .then(() => { expect(myFn).toBeCalledTimes(1); expect(logg).toBeCalledTimes(1); done(); }) .catch(done); }); }); describe('createRunner#cancel', () => { const DummyEff = createEffect('DummyEff', { myFn: func() }); it('should cancel runner if .cancel is called', done => { const action = function *() { yield DummyEff.myFn(); yield sleep(500); yield DummyEff.myFn(); yield 'Yo'; }; const myFn = jest.fn(); const promise = DummyEff.handler({ myFn: ({ resume }) => () => resume(myFn()) }).run(action); setTimeout(() => { promise.cancel(); expect(myFn).toBeCalledTimes(1); done(); }, 100); promise .then(() => done('Shouldnt have reached here')) .catch(done); }); }); describe('composeEffects', () => { const action = function *() { const response = yield ApiEffect.fetch('/some-api'); yield ConsoleEff.log(response); yield response.data; }; it('should compose Api and IO effects', done => { const Effect = composeEffects(ApiEffect, ConsoleEff); const eff = Effect.handler({ log: ({ resume }) => ({ data }) => { expect(data).toBe('wow'); resume(); }, fetch: ({ resume }) => () => resume({ data: 'wow' }), }); eff(action) .then(() => done()) .catch(done); }); }); describe('composeHandlers', () => { const action = function *() { const response = yield ApiEffect.fetch('/some-api'); yield ConsoleEff.log(response + ' world'); yield response; }; it('should compose Api and IO effects', done => { const logg = jest.fn(); const api = ApiEffect.handler({ fetch: ({ resume }) => () => resume('Hello'), }); const konsole = ConsoleEff.handler({ log: ({ resume }) => d => resume(logg(d)), }); const eff = composeHandlers(api, konsole); eff(action) .then(data => { expect(data).toBe('Hello'); expect(logg).toBeCalledWith('Hello world'); done(); }) .catch(done); }); }); describe('example usage', () => { it('should resolve with the correct value for valid operation (fetch example)', done => { const api = ApiEffect.handler({ fetch: ({ resume }) => (url, req) => setTimeout(() => resume({ url, req, data: 'wow' }), 500), }); const action = function *() { const response = yield ApiEffect.fetch('/some-api'); yield response.data; }; api(action) .then(data => { expect(data).toBe('wow'); done(); }) .catch(done); }); it('should resolve with the correct value for valid operation (fetch example)', done => { const api = ApiEffect.handler({ fetch: ({ resume }) => (url, req) => setTimeout(() => resume({ url, req, data: 'wow' }), 500), }); const action = function *() { const response = yield ApiEffect.fetch('/some-api'); yield ConsoleEff.log('Wrong operation'); yield response.data; }; api(action) .then(() => done('Shouldve thrown error')) .catch(e => { expect(e.message).toContain('Invalid operation'); expect(e.message).toContain('"log"'); done(); }); }); }); });