state-decorator 6.6.3 → 6.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +270 -84
- package/index.d.ts +5 -0
- package/index.es.js.map +1 -1
- package/index.js.map +1 -1
- package/package.json +1 -1
- package/test.d.ts +56 -1
- package/test.es.js +1 -1
- package/test.es.js.map +1 -1
- package/test.js +1 -1
- package/test.js.map +1 -1
package/README.md
CHANGED
|
@@ -76,19 +76,19 @@ type Props = {
|
|
|
76
76
|
// Initial state & actions
|
|
77
77
|
|
|
78
78
|
export const getInitialState = (): State => ({
|
|
79
|
-
|
|
79
|
+
selectedUserId: null,
|
|
80
80
|
user: null,
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
export const userAppActions: StoreActions<State, Actions, Props> = {
|
|
84
84
|
// synchronous action (simple form)
|
|
85
|
-
selectUser: ({ state, args: [id] }) => ({ ...state,
|
|
85
|
+
selectUser: ({ state, args: [id] }) => ({ ...state, selectedUserId: id }),
|
|
86
86
|
|
|
87
87
|
// a synchronous action with side effects
|
|
88
88
|
selectAndLoadUser: {
|
|
89
89
|
// same as simple form, using aliases (use short context alias 's' instead of 'state')
|
|
90
90
|
// effects contain only immutable state changes
|
|
91
|
-
effects: ({ s, args: [id] }) => ({ ...s,
|
|
91
|
+
effects: ({ s, args: [id] }) => ({ ...s, selectedUserId: id }),
|
|
92
92
|
// side effects (ie. other effects than state changes), using "actions" alias
|
|
93
93
|
sideEffects: ({ a }) => {
|
|
94
94
|
a.loadUser();
|
|
@@ -812,42 +812,138 @@ function Container(props: Props) {
|
|
|
812
812
|
# Unit testing
|
|
813
813
|
|
|
814
814
|
- The state decorator provides a testing framework for stores.
|
|
815
|
-
- The base principle is that a mock store is immutable
|
|
816
|
-
-
|
|
817
|
-
-
|
|
818
|
-
|
|
819
|
-
|
|
815
|
+
- The base principle is that a mock store is immutable:
|
|
816
|
+
- a new store is created each time state or props are set.
|
|
817
|
+
- it allows to share mock store across tests.
|
|
818
|
+
- The API is chainable: functions are returning a mock store, excepting test functions.
|
|
819
|
+
- On a mock store, we can test the store internals after initialiation, after inbound props have changed, after actions are called.
|
|
820
820
|
|
|
821
821
|
## Getting started
|
|
822
822
|
|
|
823
|
+
1. Create and setup a mock store (_createMockFromStore_)
|
|
824
|
+
2. Test store after initialization (_onInit_)
|
|
825
|
+
3. Test store after props changes (_onPropsChange_)
|
|
826
|
+
4. Test each action:
|
|
827
|
+
1. Get mock store action (_getAction_)
|
|
828
|
+
2. setup store state/prop (_setPartialState_, _setPartialProps_)
|
|
829
|
+
3. if action is asynchronous, you can mock promise (_promiseResolves_, _promiseRejects_)
|
|
830
|
+
4. call action with params (_call_)
|
|
831
|
+
5. await response to test state/actions called...
|
|
832
|
+
|
|
833
|
+
## Example
|
|
834
|
+
|
|
835
|
+
The store definition is available at [TodoStore.ts](https://github.com/GlobalSport/state-decorator/blob/develop/tests/fixtures/TodoStore.ts).
|
|
836
|
+
|
|
837
|
+
The test is available at [Todo.test.ts](https://github.com/GlobalSport/state-decorator/blob/develop/tests/Todo.test.ts).
|
|
838
|
+
|
|
823
839
|
```typescript
|
|
824
840
|
import { createMockFromStore } from 'state-decorator/test';
|
|
825
841
|
|
|
826
|
-
import
|
|
842
|
+
import todoStore from './fixtures/TodoStore';
|
|
827
843
|
|
|
828
844
|
describe('Todo', () => {
|
|
829
845
|
// create a mock store and setup initial state
|
|
830
|
-
// state / props can be set in full form or partial form
|
|
831
846
|
// this store can be shared accross tests because it is immutable
|
|
832
|
-
const store = createMockFromStore(todoStore
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
847
|
+
const store = createMockFromStore(todoStore)
|
|
848
|
+
// or
|
|
849
|
+
// const store = createMockStore(getInitialState, actions, props, options)
|
|
850
|
+
// initial state / props can be set in full form or partial form
|
|
851
|
+
.setPartialState({
|
|
852
|
+
idCount: 3,
|
|
853
|
+
todoIds: ['id', 'id2'],
|
|
854
|
+
todoMap: {
|
|
855
|
+
id: {
|
|
856
|
+
id: 'id',
|
|
857
|
+
completed: true,
|
|
858
|
+
title: 'todo',
|
|
859
|
+
},
|
|
860
|
+
id2: {
|
|
861
|
+
id: 'id2',
|
|
862
|
+
completed: false,
|
|
863
|
+
title: 'todo 2',
|
|
864
|
+
},
|
|
845
865
|
},
|
|
846
|
-
}
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
describe('Initialization', () => {
|
|
869
|
+
it('if not initial todo => load list', () => {
|
|
870
|
+
store
|
|
871
|
+
.setPartialState({
|
|
872
|
+
todoIds: [],
|
|
873
|
+
todoMap: {},
|
|
874
|
+
})
|
|
875
|
+
.onInit({ initialTodos: null })
|
|
876
|
+
.test(({ state, actions }) => {
|
|
877
|
+
// test state after initialization, ie:
|
|
878
|
+
// - options.onPropsChange with onMount: true
|
|
879
|
+
// - options.onMount
|
|
880
|
+
expect(state.todoIds).toEqual([]);
|
|
881
|
+
expect(state.todoMap).toEqual({});
|
|
882
|
+
|
|
883
|
+
// test derived state
|
|
884
|
+
expect(state.todos).toEqual([]);
|
|
885
|
+
|
|
886
|
+
// test side effects also
|
|
887
|
+
expect(actions.loadRemoteList).toHaveBeenCalled();
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it('initial todos => setup state and do not load list', () => {
|
|
892
|
+
store
|
|
893
|
+
.setPartialState({
|
|
894
|
+
todoIds: [],
|
|
895
|
+
todoMap: {},
|
|
896
|
+
})
|
|
897
|
+
.onInit({
|
|
898
|
+
initialTodos: [
|
|
899
|
+
{
|
|
900
|
+
id: 'item1',
|
|
901
|
+
title: 'Item 1',
|
|
902
|
+
completed: false,
|
|
903
|
+
},
|
|
904
|
+
],
|
|
905
|
+
})
|
|
906
|
+
.test(({ state, actions }) => {
|
|
907
|
+
// test state after initialization, ie:
|
|
908
|
+
// - options.onPropsChange with onMount: true
|
|
909
|
+
// - options.onMount
|
|
910
|
+
expect(state.todoIds).toEqual(['item1']);
|
|
911
|
+
expect(state.todoMap).toEqual({
|
|
912
|
+
item1: {
|
|
913
|
+
id: 'item1',
|
|
914
|
+
title: 'Item 1',
|
|
915
|
+
completed: false,
|
|
916
|
+
},
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
// test derived state
|
|
920
|
+
expect(state.todos).toEqual([
|
|
921
|
+
{
|
|
922
|
+
id: 'item1',
|
|
923
|
+
title: 'Item 1',
|
|
924
|
+
completed: false,
|
|
925
|
+
},
|
|
926
|
+
]);
|
|
927
|
+
|
|
928
|
+
// test side effects also
|
|
929
|
+
expect(actions.loadRemoteList).not.toHaveBeenCalled();
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it('onSetNewTitle', async () => {
|
|
935
|
+
// always return a promise whatever if the action is asynchronous or not
|
|
936
|
+
const { state, prevState } = await store.getAction('onSetNewTitle').call('new Title');
|
|
937
|
+
|
|
938
|
+
// previous state is available for comparison
|
|
939
|
+
expect(prevState.newTitle).toEqual('');
|
|
940
|
+
expect(state.newTitle).toEqual('new Title');
|
|
847
941
|
});
|
|
848
942
|
|
|
849
943
|
it('onCreate', async () => {
|
|
850
|
-
|
|
944
|
+
const {
|
|
945
|
+
state: { todoIds, todoMap, todos },
|
|
946
|
+
} = await store
|
|
851
947
|
// set partial state and return new store, source store is untouched
|
|
852
948
|
.setPartialState({
|
|
853
949
|
newTitle: 'new todo',
|
|
@@ -855,37 +951,69 @@ describe('Todo', () => {
|
|
|
855
951
|
// get a mock action that can be shared too !
|
|
856
952
|
.getAction('onCreate')
|
|
857
953
|
// Invokes action. It has no side effect on state of mock action
|
|
858
|
-
.call()
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
954
|
+
.call();
|
|
955
|
+
|
|
956
|
+
// assert state
|
|
957
|
+
expect(todoIds).toEqual(['id', 'id2', 'id3']);
|
|
958
|
+
expect(todoMap).toEqual({
|
|
959
|
+
id: {
|
|
960
|
+
id: 'id',
|
|
961
|
+
completed: true,
|
|
962
|
+
title: 'todo',
|
|
963
|
+
},
|
|
964
|
+
id2: {
|
|
965
|
+
id: 'id2',
|
|
966
|
+
completed: false,
|
|
967
|
+
title: 'todo 2',
|
|
968
|
+
},
|
|
969
|
+
id3: {
|
|
970
|
+
id: 'id3',
|
|
971
|
+
completed: false,
|
|
972
|
+
title: 'new todo',
|
|
973
|
+
},
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
// assert derived state
|
|
977
|
+
//
|
|
978
|
+
// export const todoOptions: StoreOptions<State, Actions, Props, DerivedState> = {
|
|
979
|
+
// derivedState: {
|
|
980
|
+
// todos: {
|
|
981
|
+
// getDeps: ({ s }) => [s.todoIds, s.todoMap],
|
|
982
|
+
// get: ({ s }) => s.todoIds.map((id) => s.todoMap[id]),
|
|
983
|
+
// },
|
|
984
|
+
// },
|
|
985
|
+
// };
|
|
986
|
+
|
|
987
|
+
expect(todos).toEqual([
|
|
988
|
+
{
|
|
989
|
+
id: 'id',
|
|
990
|
+
completed: true,
|
|
991
|
+
title: 'todo',
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
id: 'id2',
|
|
995
|
+
completed: false,
|
|
996
|
+
title: 'todo 2',
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
id: 'id3',
|
|
1000
|
+
completed: false,
|
|
1001
|
+
title: 'new todo',
|
|
1002
|
+
},
|
|
1003
|
+
]);
|
|
881
1004
|
});
|
|
882
1005
|
|
|
883
|
-
|
|
884
|
-
// this mock action is shared
|
|
1006
|
+
describe('onToggle', () => {
|
|
1007
|
+
// this mock action is shared in two tests but they can run in parallel
|
|
885
1008
|
const onToggle = store.getAction('onToggle');
|
|
886
1009
|
|
|
887
|
-
|
|
888
|
-
|
|
1010
|
+
it('toggle false => true', async () => {
|
|
1011
|
+
// arguments of call() depend on the Store action signature, use auto completion!
|
|
1012
|
+
const {
|
|
1013
|
+
state: { todoIds, todoMap },
|
|
1014
|
+
actions,
|
|
1015
|
+
} = await onToggle.call('id2');
|
|
1016
|
+
|
|
889
1017
|
expect(todoIds).toEqual(['id', 'id2']);
|
|
890
1018
|
expect(todoMap).toEqual({
|
|
891
1019
|
id: {
|
|
@@ -899,9 +1027,18 @@ describe('Todo', () => {
|
|
|
899
1027
|
title: 'todo 2',
|
|
900
1028
|
},
|
|
901
1029
|
});
|
|
1030
|
+
|
|
1031
|
+
// if there are some side effects calling store actions, you can test them
|
|
1032
|
+
// as mock actions are injected instead of real actions.
|
|
1033
|
+
expect(actions.updateRemoteList).toHaveBeenCalled();
|
|
902
1034
|
});
|
|
903
1035
|
|
|
904
|
-
|
|
1036
|
+
it('toggle true => false', async () => {
|
|
1037
|
+
const {
|
|
1038
|
+
state: { todoIds, todoMap },
|
|
1039
|
+
actions,
|
|
1040
|
+
} = await onToggle.call('id');
|
|
1041
|
+
|
|
905
1042
|
expect(todoIds).toEqual(['id', 'id2']);
|
|
906
1043
|
expect(todoMap).toEqual({
|
|
907
1044
|
id: {
|
|
@@ -915,53 +1052,103 @@ describe('Todo', () => {
|
|
|
915
1052
|
title: 'todo 2',
|
|
916
1053
|
},
|
|
917
1054
|
});
|
|
1055
|
+
|
|
1056
|
+
// if there are some side effects calling store actions, you can test them
|
|
1057
|
+
// as mock actions are injected instead of real actions.
|
|
1058
|
+
expect(actions.updateRemoteList).toHaveBeenCalled();
|
|
918
1059
|
});
|
|
919
1060
|
});
|
|
920
1061
|
|
|
921
1062
|
// Another store / action
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1063
|
+
describe('loadRemoteList', () => {
|
|
1064
|
+
// action is shared
|
|
1065
|
+
const action = store.getAction('loadRemoteList');
|
|
1066
|
+
|
|
1067
|
+
it('success', async () => {
|
|
1068
|
+
const { state } = await action
|
|
1069
|
+
// override default promise result
|
|
1070
|
+
.promiseResolves([
|
|
1071
|
+
{
|
|
1072
|
+
id: 'item1',
|
|
1073
|
+
title: 'item title',
|
|
1074
|
+
completed: true,
|
|
1075
|
+
},
|
|
1076
|
+
])
|
|
1077
|
+
.call();
|
|
1078
|
+
|
|
1079
|
+
expect(state.error).toBeFalsy();
|
|
1080
|
+
expect(state.todoIds).toEqual(['item1']);
|
|
1081
|
+
expect(state.todoMap).toEqual({
|
|
1082
|
+
item1: {
|
|
1083
|
+
id: 'item1',
|
|
1084
|
+
title: 'item title',
|
|
1085
|
+
completed: true,
|
|
1086
|
+
},
|
|
937
1087
|
});
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
it('error', async () => {
|
|
1091
|
+
const { state } = await action
|
|
1092
|
+
// override default promise result with error
|
|
1093
|
+
.promiseRejects(new Error('boom'))
|
|
1094
|
+
.call();
|
|
1095
|
+
|
|
1096
|
+
expect(state.error).toBeTruthy();
|
|
1097
|
+
});
|
|
938
1098
|
});
|
|
939
1099
|
|
|
940
|
-
//
|
|
1100
|
+
// Test effects and side effect when props are changing
|
|
1101
|
+
//
|
|
1102
|
+
// Here:
|
|
1103
|
+
// export const todoOptions: StoreOptions<State, Actions, Props, DerivedState> = {
|
|
1104
|
+
// onPropsChange: [
|
|
1105
|
+
// {
|
|
1106
|
+
// getDeps: (p) => [p.initialTodos],
|
|
1107
|
+
// effects: ({ s, p }) => ({ ...s, ...splitList(p.initialTodos) }),
|
|
1108
|
+
// onMount: true,
|
|
1109
|
+
// },
|
|
1110
|
+
// ],
|
|
1111
|
+
// };
|
|
941
1112
|
it('onPropsChange', () => {
|
|
942
1113
|
store
|
|
943
1114
|
// onPropsChange allows to test prop change, no side effect on original store
|
|
944
1115
|
// to create a new store with new props, use setProps or setPartialProps
|
|
945
1116
|
.onPropsChange({
|
|
946
|
-
|
|
1117
|
+
initialTodos: [
|
|
1118
|
+
{
|
|
1119
|
+
id: 'item1',
|
|
1120
|
+
title: 'Item 1',
|
|
1121
|
+
completed: false,
|
|
1122
|
+
},
|
|
1123
|
+
],
|
|
947
1124
|
})
|
|
948
|
-
.test((
|
|
949
|
-
// test
|
|
950
|
-
expect(
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1125
|
+
.test(({ state, actions }) => {
|
|
1126
|
+
// test state after prop has changed
|
|
1127
|
+
expect(state.todoIds).toEqual(['item1']);
|
|
1128
|
+
expect(state.todoMap).toEqual({
|
|
1129
|
+
item1: {
|
|
1130
|
+
id: 'item1',
|
|
1131
|
+
title: 'Item 1',
|
|
1132
|
+
completed: false,
|
|
1133
|
+
},
|
|
957
1134
|
});
|
|
1135
|
+
|
|
1136
|
+
// test derived state after prop has changed
|
|
1137
|
+
expect(state.todos).toEqual([
|
|
1138
|
+
{
|
|
1139
|
+
id: 'item1',
|
|
1140
|
+
title: 'Item 1',
|
|
1141
|
+
completed: false,
|
|
1142
|
+
},
|
|
1143
|
+
]);
|
|
1144
|
+
|
|
1145
|
+
// test side effects also
|
|
1146
|
+
expect(actions.loadRemoteList).not.toHaveBeenCalled();
|
|
958
1147
|
});
|
|
959
1148
|
});
|
|
960
1149
|
});
|
|
961
1150
|
```
|
|
962
1151
|
|
|
963
|
-
[](https://codesandbox.io/s/todo-forked-ml0z6?file=/src/TodoApp.tsx)
|
|
964
|
-
|
|
965
1152
|
## Error testing
|
|
966
1153
|
|
|
967
1154
|
### Managed error
|
|
@@ -1083,10 +1270,9 @@ TypeError: Cannot set property 'crash' of null
|
|
|
1083
1270
|
### MockStore
|
|
1084
1271
|
|
|
1085
1272
|
| Function | Arguments | Returns | Description |
|
|
1086
|
-
| --------------- | ------------------------------------ | ------------------- | ------------------------------------------------------------------------------------------- |
|
|
1273
|
+
| --------------- | ------------------------------------ | ------------------- | ------------------------------------------------------------------------------------------- | --- |
|
|
1087
1274
|
| getAction | actionName: keyof A | MockStoreAction | Get a mock action instance |
|
|
1088
|
-
|
|
|
1089
|
-
| onMount | props: P | MockResult | Test onMount. Does not change internal props. |
|
|
1275
|
+
| onInit | props?: Partial<P> | MockResult | Test store initialization (onPropsChanged marked onMount + onMount) | |
|
|
1090
1276
|
| onPropsChange | props: P, init?: boolean | MockResult | Test prop changes. Does not change internal props. Init allows to test onMount prop changes |
|
|
1091
1277
|
| setPartialProps | props: Partial<P> | new mock store | Create a new store with the merge of current props and partial props |
|
|
1092
1278
|
| setPartialState | state: Partial<S> | new mock store | Create a new store with the merge of current state and partial state |
|
package/index.d.ts
CHANGED
|
@@ -48,6 +48,11 @@ export declare type StoreApi<S, A extends DecoratedActions, P, DS = {}> = {
|
|
|
48
48
|
* A function that takes a list of action names or a tuple of [action name, promiseId] and returns <code>true</code> if any action is loading.
|
|
49
49
|
*/
|
|
50
50
|
isLoading: IsLoadingFunc<A>;
|
|
51
|
+
getConfig: () => {
|
|
52
|
+
getInitialState: (p: P) => S;
|
|
53
|
+
options: StoreOptions<S, A, P, DS>;
|
|
54
|
+
actions: StoreActions<S, A, P, DS>;
|
|
55
|
+
};
|
|
51
56
|
};
|
|
52
57
|
export declare type StateListenerContext<S, DS, A extends DecoratedActions> = S & DS & A & Pick<StoreApi<S, A, any, DS>, 'loading' | 'loadingMap' | 'isLoading' | 'abortAction'>;
|
|
53
58
|
export declare type StateListener<S, DS, A extends DecoratedActions> = (ctx: StateListenerContext<S, DS, A>) => void;
|