@cycle/state
Version:
Wraps your Cycle.js main function with reducer-driven state management
466 lines (399 loc) • 12.2 kB
text/typescript
// 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();
});
});