├── .npmignore ├── .gitignore ├── renovate.json ├── test ├── types │ ├── tsconfig.json │ └── index.ts ├── action-creator.ts ├── mapper.ts └── helpers.ts ├── src ├── index.ts ├── types │ ├── fsa.ts │ └── vuex.ts ├── utils.ts ├── helpers.ts ├── mapper.ts └── action-creator.ts ├── .eslintrc.json ├── jest.config.js ├── tsconfig.json ├── README.md ├── package.json ├── LICENSE └── .circleci └── config.yml /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | coverage/ 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "include": ["./*.ts"] 4 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { combineAction, action, combineMutation, mutation } from "./helpers"; 2 | export { actionCreator, actionCreatorFactory } from "./action-creator"; 3 | export { mapActions, mapMutations, createNamespacedHelpers } from "./mapper"; 4 | -------------------------------------------------------------------------------- /src/types/fsa.ts: -------------------------------------------------------------------------------- 1 | export type FluxType = string; 2 | /** 3 | * Action conformed to FSA 4 | */ 5 | export interface FSA { 6 | readonly type: FluxType; 7 | payload: Payload; 8 | error?: boolean; 9 | meta?: any; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * makeType with prefix 3 | * @param type 4 | * @param prefix 5 | */ 6 | export function makeType(type: string, prefix?: string): string { 7 | if (prefix) { 8 | return `${prefix}/${type}`; 9 | } 10 | return type; 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "extends": [ 5 | "plugin:@typescript-eslint/recommended", 6 | "prettier/@typescript-eslint", 7 | "prettier" 8 | ], 9 | "rules": { 10 | "@typescript-eslint/no-namespace": "off", 11 | "@typescript-eslint/no-explicit-any": "off" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: "http://localhost", 3 | moduleNameMapper: { 4 | "^~(.*)$": "/src/$1" 5 | }, 6 | moduleFileExtensions: ["js", "ts"], 7 | transform: { 8 | ".*\\.ts$": "/node_modules/ts-jest" 9 | }, 10 | testMatch: ["**/test/**/*.ts", "!**/test/types/*.ts"], 11 | collectCoverage: true, 12 | collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts"] 13 | }; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es2015", "dom"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "strict": true, 12 | "declaration": true, 13 | "removeComments": true, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "allowSyntheticDefaultImports": true, 16 | "baseUrl": ".", 17 | "outDir": "lib" 18 | }, 19 | "include": ["./src/**/*.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /src/types/vuex.ts: -------------------------------------------------------------------------------- 1 | import { ActionContext, Store } from "vuex"; 2 | 3 | /** 4 | * Enhanced type definition for Vuex ActionHandler 5 | */ 6 | export type ActionHandler = ( 7 | this: Store, 8 | injectee: ActionContext, 9 | payload: P 10 | ) => any; 11 | 12 | /** 13 | * Enhanced type definition for Vuex ActionObject 14 | */ 15 | export interface ActionObject { 16 | root?: boolean; 17 | handler: ActionHandler; 18 | } 19 | 20 | /** 21 | * Enhanced type definition for Vuex Action 22 | */ 23 | export type Action = ActionHandler | ActionObject; 24 | 25 | /** 26 | * Enhanced type definition for Vuex Mutation 27 | */ 28 | export type Mutation = (this: Store, state: S, payload: P) => void; 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | vuex-typescript-fsa 2 | --- 3 | 4 | The helper function for inferring a combination of action/mutation and handler 5 | 6 | **This project is under development. Some features have bugs and some APIs might be changed near future.** 7 | 8 | ## Installation 9 | 10 | ``` 11 | npm install vuex-typescript-fsa 12 | ``` 13 | 14 | ## Demo 15 | 16 | ![demo](https://github.com/sue71/vuex-typescript-fsa/blob/images/demo-01.gif) 17 | 18 | ## Usage 19 | 20 | ```js 21 | const Increment = actionCreator("Increment"); 22 | 23 | const store = new Store<{ count: number }>({ 24 | state: { 25 | count: 0 26 | }, 27 | actions: combineAction( 28 | action(Increment, function(context, action) { 29 | context.commit(action); 30 | }) 31 | ), 32 | mutations: combineMutation( 33 | mutation(Increment, function(state, action) { 34 | state.count = action.payload; 35 | }) 36 | ) 37 | }); 38 | 39 | store.dispatch(Increment(10)); 40 | ``` 41 | 42 | ## License 43 | 44 | MIT 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuex-typescript-fsa", 3 | "version": "0.4.1", 4 | "description": "The helper which makes vuex more type-safety", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib" 8 | ], 9 | "dependencies": { 10 | "vuex": "^3.0.1" 11 | }, 12 | "devDependencies": { 13 | "@types/jest": "25.1.4", 14 | "@typescript-eslint/eslint-plugin": "2.25.0", 15 | "@typescript-eslint/parser": "2.25.0", 16 | "codecov": "3.6.5", 17 | "js-yaml": "3.13.1", 18 | "eslint": "6.8.0", 19 | "eslint-config-prettier": "6.10.1", 20 | "eslint-plugin-prettier": "3.1.2", 21 | "jest": "25.2.3", 22 | "prettier": "2.0.2", 23 | "ts-jest": "25.2.1", 24 | "typescript": "3.8.3", 25 | "vue": "2.6.11" 26 | }, 27 | "scripts": { 28 | "build": "tsc", 29 | "lint": "eslint src/**/*.ts", 30 | "test": "jest", 31 | "test:types": "tsc -p ./test/types/tsconfig.json", 32 | "codecov": "codecov" 33 | }, 34 | "keywords": [ 35 | "vuex", 36 | "TypeScript" 37 | ], 38 | "author": "sue71", 39 | "license": "MIT" 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { ActionTree, MutationTree } from "vuex"; 2 | import { ActionCreator, PayloadType } from "./action-creator"; 3 | import { Action, Mutation } from "./types/vuex"; 4 | import { FSA } from "./types/fsa"; 5 | 6 | /** 7 | * Create action handler with type annotation 8 | */ 9 | export function action>( 10 | actionCreator: A, 11 | action: Action>> 12 | ): ActionTree { 13 | return { 14 | [actionCreator.type]: action 15 | }; 16 | } 17 | 18 | /** 19 | * Create mutation handler with type annotation 20 | */ 21 | export function mutation>( 22 | actionCreator: A, 23 | mutation: Mutation>> 24 | ): MutationTree { 25 | return { 26 | [actionCreator.type]: mutation 27 | }; 28 | } 29 | 30 | /** 31 | * Combine action handlers as a ActionTree 32 | */ 33 | export function combineAction( 34 | ...actions: ActionTree[] 35 | ): ActionTree { 36 | return actions.reduce((res, v) => Object.assign(res, v), {}); 37 | } 38 | 39 | /** 40 | * Combine mutation handlers as a ActionTree 41 | */ 42 | export function combineMutation( 43 | ...mutations: MutationTree[] 44 | ): MutationTree { 45 | return mutations.reduce((res, v) => Object.assign(res, v), {}); 46 | } 47 | -------------------------------------------------------------------------------- /test/types/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import { actionCreatorFactory, actionCreator } from "../../src/action-creator"; 3 | import { action } from "../../src/helpers"; 4 | import { 5 | mapActions, 6 | mapMutations, 7 | createNamespacedHelpers 8 | } from "../../src/mapper"; 9 | 10 | // test: actionCreator 11 | const WithPayload = actionCreator("payload"); 12 | const NoPayload = actionCreator("payload"); 13 | 14 | WithPayload(["payload"]); 15 | WithPayload(["payload"], { 16 | error: false, 17 | meta: "", 18 | namespace: "" 19 | }); 20 | NoPayload(void 0); 21 | NoPayload(void 0, { 22 | error: false, 23 | meta: "", 24 | namespace: "" 25 | }); 26 | 27 | // test: actionCreatorFactory 28 | const namespacedActionCreator = actionCreatorFactory({ 29 | namespace: "ns", 30 | prefix: "prefix" 31 | }); 32 | const Namespaced = namespacedActionCreator("payload"); 33 | 34 | Namespaced(["payload"]); 35 | 36 | // test: action 37 | action(Namespaced, (_, action) => { 38 | action.payload.length; 39 | }); 40 | 41 | action(WithPayload, (_, action) => { 42 | action.payload.length; 43 | }); 44 | 45 | action(NoPayload, (_, action) => { 46 | action.payload; 47 | }); 48 | 49 | const namespacedHelper = createNamespacedHelpers("ns"); 50 | 51 | // test: mapper 52 | Vue.extend({ 53 | methods: { 54 | ...mapActions({ action: WithPayload }), 55 | ...mapMutations({ mutation: WithPayload }), 56 | ...namespacedHelper.mapActions({ namespacedAction: WithPayload }), 57 | callAction() { 58 | this.action(["test"]); 59 | this.mutation(["test"]); 60 | } 61 | } 62 | }); 63 | -------------------------------------------------------------------------------- /src/mapper.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreator, PayloadType } from "./action-creator"; 2 | 3 | export type Mappable = Record>; 4 | 5 | export function mapActions( 6 | map: T, 7 | namespace?: string 8 | ): { 9 | [K in keyof T]: (payload: PayloadType) => Promise | object 10 | } { 11 | return Object.keys(map) 12 | .map(v => ({ key: v, action: map[v] })) 13 | .reduce((acc, item) => { 14 | const action = item.action; 15 | return { 16 | ...acc, 17 | [item.key]: function(this: any, payload: any) { 18 | return this.$store.dispatch( 19 | action(payload, { 20 | namespace: namespace || action.namespace 21 | }) 22 | ); 23 | } 24 | }; 25 | }, {}) as any; 26 | } 27 | 28 | export function mapMutations( 29 | map: T, 30 | namespace?: string 31 | ): { [K in keyof T]: (payload: PayloadType) => void } { 32 | return Object.keys(map) 33 | .map(v => ({ key: v, action: map[v] })) 34 | .reduce((acc, item) => { 35 | const action = item.action; 36 | return { 37 | ...acc, 38 | [item.key]: function(this: any, payload: any) { 39 | return this.$store.commit( 40 | action(payload, { 41 | namespace: namespace || action.namespace 42 | }) 43 | ); 44 | } 45 | }; 46 | }, {}) as any; 47 | } 48 | 49 | export const createNamespacedHelpers = (namespace: string) => ({ 50 | mapMutations: (map: T) => mapMutations(map, namespace), 51 | mapActions: (map: T) => mapActions(map, namespace) 52 | }); 53 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | # Shared reference for a environment 4 | executors: 5 | default: 6 | docker: 7 | - image: circleci/node:10.19 8 | environment: 9 | # CODECOV_TOKEN: xxx // setup via circleci 10 | working_directory: ~/repo 11 | 12 | jobs: 13 | # Job for build 14 | build: 15 | executor: default 16 | steps: 17 | - checkout 18 | - restore_cache: 19 | keys: 20 | - npm-cache-{{ arch }}-{{ .Branch }}-{{ checksum "package-lock.json" }} 21 | - npm-cache-{{ arch }}-{{ .Branch }} 22 | - npm-cache 23 | - run: 24 | name: install npm dependencies 25 | command: npm install 26 | - run: 27 | name: build 28 | command: npm run build 29 | - save_cache: 30 | key: npm-cache-{{ arch }}-{{ .Branch }}-{{ checksum "package-lock.json" }} 31 | paths: 32 | - node_modules 33 | - persist_to_workspace: 34 | root: ~/repo 35 | paths: 36 | - . 37 | 38 | # Job for test 39 | test: 40 | executor: default 41 | steps: 42 | - checkout 43 | - attach_workspace: 44 | at: ~/repo 45 | - run: 46 | name: unit test 47 | command: npm t 48 | - run: 49 | name: test types 50 | command: npm run test:types 51 | - run: 52 | name: send coverage 53 | command: npm run codecov 54 | 55 | workflows: 56 | per_commit: 57 | jobs: 58 | - build: 59 | name: build 60 | filters: 61 | branches: 62 | only: /.*/ 63 | - test: 64 | name: test 65 | requires: 66 | - build 67 | filters: 68 | branches: 69 | only: /.*/ 70 | -------------------------------------------------------------------------------- /test/action-creator.ts: -------------------------------------------------------------------------------- 1 | import { actionCreator, actionCreatorFactory } from "../src"; 2 | 3 | describe("#actionCreator", () => { 4 | test("make action creator", () => { 5 | const creator = actionCreatorFactory({ 6 | prefix: "prefix", 7 | namespace: "namespace" 8 | }); 9 | const createFSA = creator("TYPE"); 10 | expect(createFSA.namespace).toEqual("namespace"); 11 | expect(createFSA.type).toEqual("prefix/TYPE"); 12 | }); 13 | test("make action creator with namespace", () => { 14 | const creator = actionCreatorFactory("namespace"); 15 | const createFSA = creator("TYPE"); 16 | expect(createFSA.namespace).toEqual("namespace"); 17 | expect(createFSA.type).toEqual("TYPE"); 18 | }); 19 | test("make fsa", () => { 20 | const createFSA = actionCreator("TYPE", { 21 | prefix: "prefix", 22 | namespace: "namespace" 23 | }); 24 | expect(createFSA("test")).toEqual({ 25 | type: "prefix/TYPE", 26 | payload: "test", 27 | error: undefined, 28 | meta: undefined 29 | }); 30 | }); 31 | test("make fsa with options", () => { 32 | const createFSA = actionCreator("TYPE"); 33 | expect(createFSA.namespace).toEqual(undefined); 34 | expect(createFSA.type).toEqual("TYPE"); 35 | expect( 36 | createFSA("test", { 37 | namespace: "namespace", 38 | error: false, 39 | meta: "meta" 40 | }) 41 | ).toEqual({ 42 | type: "namespace/TYPE", 43 | payload: "test", 44 | error: false, 45 | meta: "meta" 46 | }); 47 | }); 48 | test("make namespaced fsa", () => { 49 | const createFSA = actionCreator("TYPE", { 50 | namespace: "namespace" 51 | }); 52 | expect(createFSA.namespaced("test")).toEqual({ 53 | type: "namespace/TYPE", 54 | payload: "test", 55 | error: undefined, 56 | meta: undefined 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/action-creator.ts: -------------------------------------------------------------------------------- 1 | import { FSA, FluxType } from "./types/fsa"; 2 | import { makeType } from "./utils"; 3 | 4 | /** 5 | * Definition for annotating type parameter 6 | */ 7 | export type PayloadType = T extends ActionCreator ? R : never; 8 | export type ReturnType = T extends (...args: any[]) => infer R ? R : never; 9 | 10 | /** 11 | * Factory function for Action 12 | * ActionCreator also have action type 13 | * 14 | * @example 15 | * export const Login = actionCreator("LOGIN"); 16 | * Login(["foo", "bar"]) 17 | */ 18 | export interface ActionCreator { 19 | type: FluxType; 20 | namespace: string; 21 | (payload: Payload, options?: FSAOptions): FSA; 22 | namespaced(payload: Payload, options?: FSAOptions): FSA; 23 | } 24 | 25 | export interface FSAOptions { 26 | namespace?: string; 27 | error?: boolean; 28 | meta?: any; 29 | } 30 | 31 | export interface ActionCreatorOptions { 32 | prefix?: string; 33 | namespace?: string; 34 | } 35 | 36 | /** 37 | * Factory function for create ActionCreator 38 | * @param type 39 | */ 40 | export function actionCreator( 41 | type: FluxType, 42 | creatorOptions: ActionCreatorOptions = {} 43 | ): ActionCreator { 44 | // make full type 45 | const fullType = makeType(type, creatorOptions.prefix); 46 | const create = (payload: Payload, options: FSAOptions = {}): FSA => { 47 | return { 48 | type: options.namespace ? makeType(type, options.namespace) : fullType, 49 | payload, 50 | error: options.error, 51 | meta: options.meta 52 | }; 53 | }; 54 | return Object.assign(create, { 55 | namespaced: (payload: Payload, options: FSAOptions = {}) => 56 | create(payload, { ...options, namespace: creatorOptions.namespace }), 57 | type: fullType, 58 | namespace: creatorOptions.namespace 59 | }) as ActionCreator; 60 | } 61 | 62 | /** 63 | * Factory function for create ActionCreator 64 | * @param type 65 | */ 66 | export function actionCreatorFactory( 67 | options: ActionCreatorOptions | string 68 | ): typeof actionCreator { 69 | if (typeof options === "string") { 70 | return (type: FluxType) => actionCreator(type, { namespace: options }); 71 | } 72 | return (type: FluxType) => actionCreator(type, options); 73 | } 74 | -------------------------------------------------------------------------------- /test/mapper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | actionCreator, 3 | mapActions, 4 | createNamespacedHelpers, 5 | mapMutations 6 | } from "../src"; 7 | import Vue from "vue"; 8 | import Vuex, { Store, Module } from "vuex"; 9 | 10 | describe("mapper", () => { 11 | const Action = actionCreator("action"); 12 | const NamespacedAction = actionCreator("action", { 13 | namespace: "b" 14 | }); 15 | 16 | const root: Module<{ value: string }, any> = { 17 | modules: { 18 | a: { 19 | namespaced: false, 20 | actions: { 21 | action: jest.fn() as any 22 | }, 23 | mutations: { 24 | action: jest.fn() as any 25 | } 26 | }, 27 | b: { 28 | namespaced: true, 29 | actions: { 30 | action: jest.fn() as any 31 | }, 32 | mutations: { 33 | action: jest.fn() as any 34 | } 35 | } 36 | } 37 | }; 38 | 39 | Vue.use(Vuex); 40 | 41 | const namespacedHelper = createNamespacedHelpers("b"); 42 | const store = new Store(root); 43 | 44 | const view = new Vue({ 45 | store, 46 | methods: { 47 | ...mapActions({ 48 | action: Action 49 | }), 50 | ...namespacedHelper.mapActions({ 51 | namespacedAction: Action 52 | }), 53 | ...mapActions({ 54 | fixedNamespacedAction: NamespacedAction 55 | }), 56 | ...mapMutations({ 57 | mutation: Action 58 | }), 59 | ...namespacedHelper.mapMutations({ 60 | namespacedMutation: Action 61 | }), 62 | ...mapMutations({ 63 | fixedNamespacedMutation: NamespacedAction 64 | }) 65 | } 66 | }); 67 | 68 | describe("simple module", () => { 69 | test("map action", () => { 70 | expect(view.action).toBeDefined(); 71 | }); 72 | test("map mutation", () => { 73 | expect(view.mutation).toBeDefined(); 74 | }); 75 | test("dispatch action", () => { 76 | view.action(); 77 | expect(root.modules!.a!.actions!["action"]).toHaveBeenCalled(); 78 | }); 79 | test("dispatch mutation", () => { 80 | view.mutation(); 81 | expect(root.modules!.a!.mutations!["action"]).toHaveBeenCalled(); 82 | }); 83 | }); 84 | 85 | describe("namespaced module", () => { 86 | test("map action", () => { 87 | expect(view.namespacedAction).toBeDefined(); 88 | }); 89 | test("map mutation", () => { 90 | expect(view.namespacedMutation).toBeDefined(); 91 | }); 92 | test("disptch action", () => { 93 | view.namespacedAction(); 94 | expect(root.modules!.b!.actions!["action"]).toHaveBeenCalled(); 95 | }); 96 | test("dispatch action with fixed namespace", () => { 97 | view.fixedNamespacedAction(); 98 | expect(root.modules!.b!.actions!["action"]).toHaveBeenCalled(); 99 | }); 100 | test("dispatch mutation", () => { 101 | view.namespacedMutation(); 102 | expect(root.modules!.b!.mutations!["action"]).toHaveBeenCalled(); 103 | }); 104 | test("dispatch mutation with fixed namespace", () => { 105 | view.fixedNamespacedAction(); 106 | expect(root.modules!.b!.mutations!["action"]).toHaveBeenCalled(); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/helpers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | action, 3 | combineAction, 4 | combineMutation, 5 | mutation, 6 | actionCreator 7 | } from "../src"; 8 | import Vue from "vue"; 9 | import Vuex, { Store } from "vuex"; 10 | import { ActionObject } from "../src/types/vuex"; 11 | 12 | describe("helpers", () => { 13 | const Action = actionCreator("ACTION"); 14 | const Action2 = actionCreator("ACTION2"); 15 | 16 | describe("#action", () => { 17 | test("type is ACTION", () => { 18 | const tree = action(Action, () => {}); 19 | expect(Object.keys(tree)[0]).toEqual("ACTION"); 20 | }); 21 | 22 | test("return action handler", () => { 23 | const tree = action(Action, { 24 | root: true, 25 | handler: function() {} 26 | }); 27 | expect((tree["ACTION"] as ActionObject).root).toBeTruthy(); 28 | expect( 29 | (tree["ACTION"] as ActionObject).handler 30 | ).toBeDefined(); 31 | }); 32 | }); 33 | 34 | describe("#combineAction", () => { 35 | test("return ActionTree", () => { 36 | const tree = action(Action, () => {}); 37 | const tree2 = action(Action2, () => {}); 38 | const tree3 = combineAction(tree, tree2); 39 | const keys = Object.keys(tree3); 40 | expect(keys).toEqual(["ACTION", "ACTION2"]); 41 | }); 42 | }); 43 | 44 | describe("#combineMutation", () => { 45 | test("return MutationTree", () => { 46 | const tree = action(Action, () => {}); 47 | const tree2 = action(Action2, () => {}); 48 | const tree3 = combineAction(tree, tree2); 49 | const keys = Object.keys(tree3); 50 | expect(keys).toEqual(["ACTION", "ACTION2"]); 51 | }); 52 | }); 53 | 54 | describe("#vuex", () => { 55 | Vue.use(Vuex); 56 | interface Counter { 57 | value: number; 58 | } 59 | const Add = actionCreator("ADD"); 60 | 61 | test("bind this for combineAction", () => { 62 | const store = new Store({ 63 | state: { 64 | value: 0 65 | }, 66 | actions: combineAction( 67 | action(Add, function() { 68 | expect(this.state.value).toEqual(0); 69 | }) 70 | ) 71 | }); 72 | store.dispatch(Add(1)); 73 | }); 74 | 75 | test("refer payload inside action handler", () => { 76 | const store = new Store({ 77 | state: { 78 | value: 0 79 | }, 80 | actions: combineAction( 81 | action(Add, function(_, action) { 82 | expect(action.payload).toEqual(1); 83 | }) 84 | ) 85 | }); 86 | store.dispatch(Add(1)); 87 | }); 88 | 89 | test("call async action", next => { 90 | const store = new Store({ 91 | state: { 92 | value: 0 93 | }, 94 | actions: combineAction( 95 | action(Add, async function(_, action) { 96 | setTimeout(() => { 97 | expect(this.state.value).toEqual(0); 98 | expect(action.payload).toEqual(1); 99 | next(); 100 | }, 1000); 101 | }) 102 | ) 103 | }); 104 | store.dispatch(Add(1)); 105 | }); 106 | 107 | test("update state in combineMutation", () => { 108 | const store = new Store({ 109 | state: { 110 | value: 0 111 | }, 112 | mutations: combineMutation( 113 | mutation(Add, function(state, action) { 114 | state.value = action.payload; 115 | }) 116 | ) 117 | }); 118 | store.commit(Add(1)); 119 | expect(store.state.value).toEqual(1); 120 | }); 121 | 122 | test("dispatch namspaced action", () => { 123 | const store = new Store({ 124 | modules: { 125 | local: { 126 | namespaced: true, 127 | actions: combineAction( 128 | action(Action, function(context, action) { 129 | context.commit(Action(action.payload)); 130 | }) 131 | ), 132 | mutations: combineMutation( 133 | mutation(Action, function(_, action) { 134 | expect(action.payload).toEqual(["foo"]); 135 | }) 136 | ) 137 | } 138 | }, 139 | actions: combineAction( 140 | action(Action, function() { 141 | fail(); 142 | }) 143 | ), 144 | mutations: combineMutation( 145 | mutation(Action, function() { 146 | fail(); 147 | }) 148 | ) 149 | }); 150 | store.dispatch(Action(["foo"], { namespace: "local" })); 151 | }); 152 | 153 | test("dispatch non-namespaced action", () => { 154 | const store = new Store({ 155 | modules: { 156 | local: { 157 | namespaced: true, 158 | actions: combineAction( 159 | action(Action, function() { 160 | fail(); 161 | }) 162 | ), 163 | mutations: combineMutation( 164 | mutation(Action, function() { 165 | fail(); 166 | }) 167 | ) 168 | } 169 | }, 170 | actions: combineAction( 171 | action(Action, function(context, action) { 172 | context.commit(Action(action.payload)); 173 | }) 174 | ), 175 | mutations: combineMutation( 176 | mutation(Action, function(_, action) { 177 | expect(action.payload).toEqual(["foo"]); 178 | }) 179 | ) 180 | }); 181 | store.dispatch(Action(["foo"])); 182 | }); 183 | 184 | test("dispatch action with root options", () => { 185 | const store = new Store({ 186 | modules: { 187 | local: { 188 | namespaced: true, 189 | actions: combineAction( 190 | action(Add, { 191 | root: true, 192 | handler(context, action) { 193 | expect(action.payload).toEqual(1); 194 | context.commit(action, { root: true }); 195 | } 196 | }) 197 | ) 198 | } 199 | }, 200 | mutations: combineMutation( 201 | mutation(Add, function(state, action) { 202 | state.value += action.payload; 203 | }) 204 | ), 205 | state: { 206 | value: 0 207 | } 208 | }); 209 | store.dispatch(Add(1)); 210 | expect(store.state.value).toEqual(1); 211 | }); 212 | }); 213 | }); 214 | --------------------------------------------------------------------------------