UNPKG

@cycle/state

Version:

Wraps your Cycle.js main function with reducer-driven state management

466 lines (399 loc) 12.2 kB
// tslint:disable-next-line import 'mocha'; import * as assert from 'assert'; import xs, {Stream} from 'xstream'; import delay from 'xstream/extra/delay'; import isolate from '@cycle/isolate'; import {withState, StateSource, Reducer, makeCollection} from '../src/index'; describe('makeCollection', function() { it('should return an isolatable List component', done => { type ItemState = { key: string; val: number | null; }; const expected = [ [{key: 'a', val: 3}], [{key: 'a', val: 3}, {key: 'b', val: null}], [{key: 'a', val: 3}, {key: 'b', val: 10}], [{key: 'a', val: 3}, {key: 'b', val: 10}, {key: 'c', val: 27}], [{key: 'a', val: 3}, {key: 'b', val: 10}], ]; function Child(sources: {state: StateSource<ItemState>}) { const defaultReducer$ = xs.of((prev: any) => { if (typeof prev.val === 'number') { return prev; } else { return {key: prev.key, val: 10}; } }); const deleteReducer$ = xs .of((prev: any) => (prev.key === 'c' ? void 0 : prev)) .compose(delay(50)); return { state: xs.merge(defaultReducer$, deleteReducer$) as Stream< Reducer<any> >, }; } const List = makeCollection<ItemState>({ item: Child, itemKey: s => s.key, itemScope: key => key, collectSinks: instances => ({ state: instances.pickMerge('state'), }), }); type MainState = { list: Array<ItemState>; }; function Main(sources: {state: StateSource<MainState>}) { sources.state.stream.addListener({ next(x) { assert.deepEqual(x.list, expected.shift()); }, error(e) { done(e.message); }, complete() { done('complete should not be called'); }, }); const childSinks = isolate(List, 'list')(sources); const childReducer$ = childSinks.state; const initReducer$ = xs.of(function initReducer(prevState: any): any { return {list: [{key: 'a', val: 3}]}; }); const addReducer$ = xs.merge( xs .of(function addB(prev: MainState): MainState { return {list: prev.list.concat({key: 'b', val: null})}; }) .compose(delay(100)), xs .of(function addC(prev: MainState): MainState { return {list: prev.list.concat({key: 'c', val: 27})}; }) .compose(delay(200)) ); const parentReducer$ = xs.merge(initReducer$, addReducer$); const reducer$ = xs.merge(parentReducer$, childReducer$); return { state: reducer$ as Stream<Reducer<any>>, }; } const wrapped = withState(Main); wrapped({}); setTimeout(() => { assert.strictEqual(expected.length, 0); done(); }, 300); }); it('should work with a custom itemKey', done => { const expected = [ [{id: 'a', val: 3}], [{id: 'a', val: 3}, {id: 'b', val: null}], [{id: 'a', val: 3}, {id: 'b', val: 10}], [{id: 'a', val: 3}, {id: 'b', val: 10}, {id: 'c', val: 27}], [{id: 'a', val: 3}, {id: 'b', val: 10}], ]; type ItemState = { id: string; val: number | undefined; }; function Child(sources: {state: StateSource<ItemState>}) { const defaultReducer$ = xs.of((prev: ItemState) => { if (typeof prev.val === 'number') { return prev; } else { return {id: prev.id, val: 10}; } }); const deleteReducer$ = xs .of((prev: ItemState) => (prev.id === 'c' ? void 0 : prev)) .compose(delay(50)); return { state: xs.merge(defaultReducer$, deleteReducer$), }; } const List = makeCollection<ItemState>({ item: Child, itemKey: s => s.id, collectSinks: instances => ({ state: instances.pickMerge('state'), }), }); function Main(sources: {state: StateSource<any>}) { sources.state.stream.addListener({ next(x) { assert.deepEqual(x.list, expected.shift()); }, error(e) { done(e.message); }, complete() { done('complete should not be called'); }, }); const childSinks = isolate(List, 'list')(sources); const childReducer$ = childSinks.state; const initReducer$ = xs.of(function initReducer(prevState: any): any { return {list: [{id: 'a', val: 3}]}; }); const addReducer$ = xs.merge( xs .of(function addB(prev: any) { return {list: prev.list.concat({id: 'b', val: null})}; }) .compose(delay(100)), xs .of(function addC(prev: any) { return {list: prev.list.concat({id: 'c', val: 27})}; }) .compose(delay(200)) ); const parentReducer$ = xs.merge(initReducer$, addReducer$); const reducer$ = xs.merge(parentReducer$, childReducer$) as Stream< Reducer<any> >; return { state: reducer$, }; } const wrapped = withState(Main); wrapped({}); setTimeout(() => { assert.strictEqual(expected.length, 0); done(); }, 300); }); it('should support itemFactory instead of static item', done => { type ItemState = { type: string; name: string | null; }; const expected: Array<Array<ItemState>> = [ [{type: 'a', name: null}], [{type: 'a', name: 'Apple'}], [{type: 'a', name: 'Apple'}, {type: 'b', name: null}], [{type: 'a', name: 'Apple'}, {type: 'b', name: 'Banana'}], ]; function ChildApple(sources: {state: StateSource<ItemState>}) { return { state: xs.of((prev: ItemState) => { if (typeof prev.name === 'string') { return prev; } else { return {type: prev.type, name: 'Apple'}; } }), }; } function ChildBanana(sources: {state: StateSource<ItemState>}) { return { state: xs.of((prev: ItemState) => { if (typeof prev.name === 'string') { return prev; } else { return {type: prev.type, name: 'Banana'}; } }), }; } const List = makeCollection<ItemState>({ itemFactory: s => (s.type === 'a' ? ChildApple : ChildBanana), itemKey: s => s.type, collectSinks: instances => ({ state: instances.pickMerge('state'), }), }); function Main(sources: {state: StateSource<{list: Array<ItemState>}>}) { sources.state.stream.addListener({ next(x) { assert.deepEqual(x.list, expected.shift()); }, error(e) { done(e.message); }, complete() { done('complete should not be called'); }, }); const childSinks = isolate(List, 'list')(sources); const childReducer$ = childSinks.state; const initReducer$ = xs.of(function initReducer(prevState: any): any { return {list: [{type: 'a', name: null}]}; }); const addReducer$ = xs .of(function addB(prev: any) { return {list: prev.list.concat({type: 'b', name: null})}; }) .compose(delay(100)); const parentReducer$ = xs.merge(initReducer$, addReducer$); const reducer$ = xs.merge(parentReducer$, childReducer$) as Stream< Reducer<any> >; return { state: reducer$, }; } const wrapped = withState(Main); wrapped({}); setTimeout(() => { assert.strictEqual(expected.length, 0); done(); }, 300); }); it('should correctly accumulate over time even without itemKey', done => { const expected = [ [{val: 3}], [{val: 4}], [{val: 5}], [{val: 6}], [{val: 6}, {val: null}], [{val: 6}, {val: 10}], [{val: 6}, {val: 11}], [{val: 6}, {val: 12}], [{val: 6}, {val: 13}], ]; function Child(sources: {state: StateSource<any>}) { const defaultReducer$ = xs.of((prev: any) => { if (typeof prev.val === 'number') { return prev; } else { return {val: 10}; } }); const incrementReducer$ = xs .of( (prev: any) => ({val: prev.val + 1}), (prev: any) => ({val: prev.val + 1}), (prev: any) => ({val: prev.val + 1}) ) .compose(delay(50)); return { state: xs.merge(defaultReducer$, incrementReducer$), }; } const List = makeCollection({ item: Child, collectSinks: instances => ({ state: instances.pickMerge('state'), }), }); function Main(sources: {state: StateSource<any>}) { sources.state.stream.addListener({ next(x) { assert.deepEqual(x.list, expected.shift()); }, error(e) { done(e.message); }, complete() { done('complete should not be called'); }, }); const childSinks = isolate(List, 'list')(sources); const childReducer$ = childSinks.state; const initReducer$ = xs.of(function initReducer(prevState: any): any { return {list: [{val: 3}]}; }); const addReducer$ = xs .of(function addSecond(prev: any) { return {list: prev.list.concat({val: null})}; }) .compose(delay(100)); const parentReducer$ = xs.merge(initReducer$, addReducer$); const reducer$ = xs.merge(parentReducer$, childReducer$) as Stream< Reducer<any> >; return { state: reducer$, }; } const wrapped = withState(Main); wrapped({}); setTimeout(() => { assert.strictEqual(expected.length, 0); done(); }, 200); }); it('should work also on an object, not just on arrays', done => { const expected = [{key: 'a', val: null}, {key: 'a', val: 10}]; function Child(sources: {state: StateSource<any>}) { const defaultReducer$ = xs.of((prev: any) => { if (typeof prev.val === 'number') { return prev; } else { return {key: prev.key, val: 10}; } }); return { state: defaultReducer$, }; } const Wrapper = makeCollection({ item: Child, collectSinks: instances => ({ state: instances.pickMerge('state'), }), }); function Main(sources: {state: StateSource<any>}) { sources.state.stream.addListener({ next(x) { assert.deepEqual(x.wrap, expected.shift()); }, error(e) { done(e.message); }, complete() { done('complete should not be called'); }, }); const wrapperSinks = isolate(Wrapper, 'wrap')(sources); const wrapperReducer$ = wrapperSinks.state; const initReducer$ = xs.of(function initReducer(prevState: any): any { return {wrap: {key: 'a', val: null}}; }); const reducer$ = xs.merge(initReducer$, wrapperReducer$) as Stream< Reducer<any> >; return { state: reducer$, }; } const wrapped = withState(Main); wrapped({}); setTimeout(() => { assert.strictEqual(expected.length, 0); done(); }, 60); }); it('should not throw if pickMerge() is called with name that item does not use', done => { function Child(sources: {state: StateSource<any>}) { return { state: xs.of({}), }; } const List = makeCollection<{key: string}>({ item: Child, itemKey: s => s.key, itemScope: key => key, collectSinks: instances => ({ HTTP: instances.pickMerge('HTTP'), }), }); function Main(sources: {state: StateSource<any>}) { const childSinks = isolate(List, 'list')(sources); const initReducer$ = xs.of(function initReducer(prevState: any): any { return {list: [{key: 'a', val: 3}]}; }); childSinks.HTTP.subscribe({}); return { state: initReducer$, }; } const wrapped = withState(Main); wrapped({}); done(); }); });