浏览代码

Merge branch 'dev' into 4.0

# Conflicts:
#	.gitignore
#	examples/chat/app.js
#	examples/classic/shopping-cart/app.js
#	examples/counter/app.js
#	examples/todomvc/app.js
#	package.json
#	rollup.config.js
#	rollup.main.config.js
#	scripts/build-main.js
#	scripts/release.sh
#	src/index.cjs.js
#	src/store.js
#	test/unit/helpers.spec.js
#	test/unit/hot-reload.spec.js
#	test/unit/modules.spec.js
#	test/unit/setup.js
#	test/unit/store.spec.js
#	types/tsconfig.json
#	yarn.lock
Kia Ishii 5 年之前
父节点
当前提交
af6971ef78

+ 1 - 2
.babelrc

@@ -1,4 +1,3 @@
 {
-  "presets": ["env"],
-  "plugins": ["transform-object-rest-spread"]
+  "presets": ["@babel/preset-env"]
 }

+ 0 - 0
.eslintrc → .eslintrc.json


+ 4 - 1
.gitignore

@@ -2,10 +2,13 @@
 !/dist/logger.d.ts
 /docs/.vuepress/dist
 /examples/**/build.js
-/node_modules
+/coverage
+/docs/.vuepress/dist
+/examples/**/build.js
 /test/e2e/reports
 /test/e2e/screenshots
 /types/typings
 /types/test/*.js
 *.log
 .DS_Store
+node_modules

+ 17 - 4
docs/api/README.md

@@ -185,7 +185,7 @@ const store = new Vuex.Store({ ...options })
   })
   ```
 
-  By default, new handler is added to the end of the chain, so it will be executed after other handlers that were added before. This can be overriden by adding `prepend: true` to `options`, which will add the handler to the beginning of the chain.
+  By default, new handler is added to the end of the chain, so it will be executed after other handlers that were added before. This can be overridden by adding `prepend: true` to `options`, which will add the handler to the beginning of the chain.
 
   ``` js
   store.subscribe(handler, { prepend: true })
@@ -210,10 +210,10 @@ const store = new Vuex.Store({ ...options })
   })
   ```
 
-  By default, new handler is added to the end of the chain, so it will be executed after other handlers that were added before. This can be overriden by adding `prepend: true` to `options`, which will add the handler to the beginning of the chain.
+  By default, new handler is added to the end of the chain, so it will be executed after other handlers that were added before. This can be overridden by adding `prepend: true` to `options`, which will add the handler to the beginning of the chain.
 
   ``` js
-  store.subscribe(handler, { prepend: true })
+  store.subscribeAction(handler, { prepend: true })
   ```
 
   To stop subscribing, call the returned unsubscribe function.
@@ -233,7 +233,20 @@ const store = new Vuex.Store({ ...options })
   })
   ```
 
-  Most commonly used in plugins. [Details](../guide/plugins.md)
+  > New in 3.4.0
+
+  Since 3.4.0, `subscribeAction` can also specify an `error` handler to catch an error thrown when an action is dispatched. The function will receive an `error` object as the third argument.
+
+  ``` js
+  store.subscribeAction({
+    error: (action, state, error) => {
+      console.log(`error action ${action.type}`)
+      console.error(error)
+    }
+  })
+  ```
+
+  The `subscribeAction` method is most commonly used in plugins. [Details](../guide/plugins.md)
 
 ### registerModule
 

+ 13 - 13
docs/fr/guide/modules.md

@@ -8,14 +8,14 @@ Pour y remédier, Vuex nous permet de diviser notre store en **modules**. Chaque
 
 ``` js
 const moduleA = {
-  state: { ... },
+  state: () => ({ ... }),
   mutations: { ... },
   actions: { ... },
   getters: { ... }
 }
 
 const moduleB = {
-  state: { ... },
+  state: () => ({ ... }),
   mutations: { ... },
   actions: { ... }
 }
@@ -37,7 +37,9 @@ Dans les mutations et accesseurs d'un module, le premier argument reçu sera **l
 
 ``` js
 const moduleA = {
-  state: { count: 0 },
+  state: () => ({
+    count: 0
+  }),
   mutations: {
     increment (state) {
       // `state` est l'état du module local
@@ -87,14 +89,14 @@ Par défaut, les actions, mutations et accesseurs à l'intérieur d'un module so
 
 Si vous souhaitez que votre module soit autosuffisant et réutilisable, vous pouvez le ranger sous un espace de nom avec `namespaced: true`. Quand le module est enregistré, tous ses accesseurs, actions et mutations seront automatiquement basés sur l'espace de nom du module dans lesquels ils sont rangés. Par exemple :
 
-```js
+``` js
 const store = new Vuex.Store({
   modules: {
     account: {
       namespaced: true,
 
       // propriétés du module
-      state: { ... }, // l'état du module est déjà imbriqué et n'est pas affecté par l'option `namespace`
+      state: () => ({ ... }), // l'état du module est déjà imbriqué et n'est pas affecté par l'option `namespace`
       getters: {
         isAdmin () { ... } // -> getters['account/isAdmin']
       },
@@ -109,7 +111,7 @@ const store = new Vuex.Store({
       modules: {
         // hérite de l'espace de nom du module parent
         myPage: {
-          state: { ... },
+          state: () => ({ ... }),
           getters: {
             profile () { ... } // -> getters['account/profile']
           }
@@ -119,7 +121,7 @@ const store = new Vuex.Store({
         posts: {
           namespaced: true,
 
-          state: { ... },
+          state: () => ({ ... }),
           getters: {
             popular () { ... } // -> getters['account/posts/popular']
           }
@@ -262,7 +264,7 @@ export default {
 
 Vous devez faire attention au nom d'espace imprévisible pour vos modules quand vous créez un [plugin](./plugins.md) qui fournit les modules et laisser les utilisateurs les ajouter au store de Vuex. Vos modules seront également sous espace de nom si l'utilisateur du plugin l'ajoute sous un module sous espace de nom. Pour vous adapter à la situation, vous devez recevoir la valeur de l'espace de nom via vos options de plugin :
 
-```js
+``` js
 // passer la valeur d'espace de nom via une option du plugin
 // et retourner une fonction de plugin Vuex
 export function createPlugin (options = {}) {
@@ -311,11 +313,9 @@ C'est exactement le même problème qu'avec `data` dans un composant Vue. Ainsi
 
 ``` js
 const MyReusableModule = {
-  state () {
-    return {
-      foo: 'bar'
-    }
-  },
+  state: () => ({
+    foo: 'bar'
+  }),
   // mutations, actions, accesseurs...
 }
 ```

+ 11 - 11
docs/guide/modules.md

@@ -8,14 +8,14 @@ To help with that, Vuex allows us to divide our store into **modules**. Each mod
 
 ``` js
 const moduleA = {
-  state: { ... },
+  state: () => ({ ... }),
   mutations: { ... },
   actions: { ... },
   getters: { ... }
 }
 
 const moduleB = {
-  state: { ... },
+  state: () => ({ ... }),
   mutations: { ... },
   actions: { ... }
 }
@@ -37,7 +37,9 @@ Inside a module's mutations and getters, the first argument received will be **t
 
 ``` js
 const moduleA = {
-  state: { count: 0 },
+  state: () => ({
+    count: 0
+  }),
   mutations: {
     increment (state) {
       // `state` is the local module state
@@ -94,7 +96,7 @@ const store = new Vuex.Store({
       namespaced: true,
 
       // module assets
-      state: { ... }, // module state is already nested and not affected by namespace option
+      state: () => ({ ... }), // module state is already nested and not affected by namespace option
       getters: {
         isAdmin () { ... } // -> getters['account/isAdmin']
       },
@@ -109,7 +111,7 @@ const store = new Vuex.Store({
       modules: {
         // inherits the namespace from parent module
         myPage: {
-          state: { ... },
+          state: () => ({ ... }),
           getters: {
             profile () { ... } // -> getters['account/profile']
           }
@@ -119,7 +121,7 @@ const store = new Vuex.Store({
         posts: {
           namespaced: true,
 
-          state: { ... },
+          state: () => ({ ... }),
           getters: {
             popular () { ... } // -> getters['account/posts/popular']
           }
@@ -322,11 +324,9 @@ This is actually the exact same problem with `data` inside Vue components. So th
 
 ``` js
 const MyReusableModule = {
-  state () {
-    return {
-      foo: 'bar'
-    }
-  },
+  state: () => ({
+    foo: 'bar'
+  }),
   // mutations, actions, getters...
 }
 ```

+ 14 - 2
docs/ja/api/README.md

@@ -175,7 +175,7 @@ const store = new Vuex.Store({ ...options })
 
 ### subscribe
 
-- `subscribe(handler: Function): Function`
+- `subscribe(handler: Function, options?: Object): Function`
 
   ストアへのミューテーションを購読します。`handler` は、全てのミューテーションの後に呼ばれ、引数として、ミューテーション ディスクリプタとミューテーション後の状態を受け取ります。
 
@@ -186,13 +186,19 @@ const store = new Vuex.Store({ ...options })
   })
   ```
 
+  デフォルトでは、新しい `handler` はチェーンの最後に登録されます。つまり、先に追加された他の `handler` が呼び出された後に実行されます。`prepend: true` を `options` に設定することで、`handler` をチェーンの最初に登録することができます。
+
+  ``` js
+  store.subscribe(handler, { prepend: true })
+  ```
+
   購読を停止するには、返された unsubscribe 関数呼び出します。
 
   プラグインの中でもっともよく利用されます。[詳細](../guide/plugins.md)
 
 ### subscribeAction
 
-- `subscribeAction(handler: Function)`
+- `subscribeAction(handler: Function, options?: Object): Function`
 
   > 2.5.0 で新規追加
 
@@ -205,6 +211,12 @@ const store = new Vuex.Store({ ...options })
   })
   ```
 
+  デフォルトでは、新しい `handler` はチェーンの最後に登録されます。つまり、先に追加された他の `handler` が呼び出された後に実行されます。`prepend: true` を `options` に設定することで、`handler` をチェーンの最初に登録することができます。
+
+  ``` js
+  store.subscribeAction(handler, { prepend: true })
+  ```
+
  購読を停止するには、返された購読解除関数を呼びます。
 
   > 3.1.0 で新規追加

+ 11 - 11
docs/ja/guide/modules.md

@@ -8,14 +8,14 @@
 
 ``` js
 const moduleA = {
-  state: { ... },
+  state: () => ({ ... }),
   mutations: { ... },
   actions: { ... },
   getters: { ... }
 }
 
 const moduleB = {
-  state: { ... },
+  state: () => ({ ... }),
   mutations: { ... },
   actions: { ... }
 }
@@ -37,7 +37,9 @@ store.state.b // -> `moduleB` のステート
 
 ``` js
 const moduleA = {
-  state: { count: 0 },
+  state: () => ({
+    count: 0
+  }),
   mutations: {
     increment (state) {
       // `state` はモジュールのローカルステート
@@ -94,7 +96,7 @@ const store = new Vuex.Store({
       namespaced: true,
 
       // モジュールのアセット
-      state: { ... }, // モジュールステートはすでにネストされており、名前空間のオプションによって影響を受けません
+      state: () => ({ ... }), // モジュールステートはすでにネストされており、名前空間のオプションによって影響を受けません
       getters: {
         isAdmin () { ... } // -> getters['account/isAdmin']
       },
@@ -109,7 +111,7 @@ const store = new Vuex.Store({
       modules: {
         // 親モジュールから名前空間を継承する
         myPage: {
-          state: { ... },
+          state: () => ({ ... }),
           getters: {
             profile () { ... } // -> getters['account/profile']
           }
@@ -119,7 +121,7 @@ const store = new Vuex.Store({
         posts: {
           namespaced: true,
 
-          state: { ... },
+          state: () => ({ ... }),
           getters: {
             popular () { ... } // -> getters['account/posts/popular']
           }
@@ -318,11 +320,9 @@ store.registerModule(['nested', 'myModule'], {
 
 ``` js
 const MyReusableModule = {
-  state () {
-    return {
-      foo: 'bar'
-    }
-  },
+  state: () => ({
+    foo: 'bar'
+  }),
   // ミューテーション、アクション、ゲッター...
 }
 ```

+ 11 - 0
docs/ja/guide/plugins.md

@@ -109,16 +109,27 @@ const logger = createLogger({
     // `mutation` は `{ type, payload }` です
     return mutation.type !== "aBlacklistedMutation"
   },
+  actionFilter (action, state) {
+    // `filter` と同等ですが、アクション用です
+    // `action` は `{ type, payloed }` です
+    return action.type !== "aBlacklistedAction"
+  },
   transformer (state) {
     // ロギングの前に、状態を変換します
     // 例えば、特定のサブツリーのみを返します
     return state.subTree
   },
+  actionTransformer (action) {
+    // `mutationTransformer` と同等ですが、アクション用です
+    return action.type
+  },
   mutationTransformer (mutation) {
     // ミューテーションは、`{ type, payload }` の形式でログ出力されます
     // 任意の方法でそれをフォーマットできます
     return mutation.type
   },
+  logActions: true, // アクションログを出力します。
+  logMutations: true, // ミューテーションログを出力します。
   logger: console, // `console` API の実装, デフォルトは `console`
 })
 ```

+ 11 - 11
docs/kr/guide/modules.md

@@ -8,14 +8,14 @@
 
 ``` js
 const moduleA = {
-  state: { ... },
+  state: () => ({ ... }),
   mutations: { ... },
   actions: { ... },
   getters: { ... }
 }
 
 const moduleB = {
-  state: { ... },
+  state: () => ({ ... }),
   mutations: { ... },
   actions: { ... }
 }
@@ -37,7 +37,9 @@ store.state.b // -> moduleB'의 상태
 
 ``` js
 const moduleA = {
-  state: { count: 0 },
+  state: () => ({
+    count: 0
+  }),
   mutations: {
     increment (state) {
       // state는 지역 모듈 상태 입니다
@@ -94,7 +96,7 @@ const store = new Vuex.Store({
       namespaced: true,
 
       // 모듈 자산
-      state: { ... }, // 모듈 상태는 이미 중첩되어 있고, 네임스페이스 옵션의 영향을 받지 않음
+      state: () => ({ ... }), // 모듈 상태는 이미 중첩되어 있고, 네임스페이스 옵션의 영향을 받지 않음
       getters: {
         isAdmin () { ... } // -> getters['account/isAdmin']
       },
@@ -109,7 +111,7 @@ const store = new Vuex.Store({
       modules: {
         // 부모 모듈로부터 네임스페이스를 상속받음
         myPage: {
-          state: { ... },
+          state: () => ({ ... }),
           getters: {
             profile () { ... } // -> getters['account/profile']
           }
@@ -119,7 +121,7 @@ const store = new Vuex.Store({
         posts: {
           namespaced: true,
 
-          state: { ... },
+          state: () => ({ ... }),
           getters: {
             popular () { ... } // -> getters['account/posts/popular']
           }
@@ -311,11 +313,9 @@ Server Side Rendered 앱에서 상태를 유지하는 것처럼 새 모듈을 
 
 ``` js
 const MyReusableModule = {
-  state () {
-    return {
-      foo: 'bar'
-    }
-  },
+  state: () => ({
+    foo: 'bar'
+  }),
   // 변이, 액션, getters...
 }
 ```

+ 11 - 11
docs/ptbr/guide/modules.md

@@ -8,14 +8,14 @@ Para ajudar com isso, o Vuex nos permite dividir nosso _store_ em **módulos**.
 
 ``` js
 const moduleA = {
-  state: { ... },
+  state: () => ({ ... }),
   mutations: { ... },
   actions: { ... },
   getters: { ... }
 }
 
 const moduleB = {
-  state: { ... },
+  state: () => ({ ... }),
   mutations: { ... },
   actions: { ... }
 }
@@ -37,7 +37,9 @@ Dentro das mutações e _getters_ de um módulo, o 1º argumento recebido será
 
 ``` js
 const moduleA = {
-  state: { count: 0 },
+  state: () => ({
+    count: 0
+  }),
   mutations: {
     increment (state) {
       // `state` é o estado local do módulo
@@ -94,7 +96,7 @@ const store = new Vuex.Store({
       namespaced: true,
 
       // module assets
-      state: { ... }, // o estado do módulo já está aninhado e não é afetado pela opção de namespace
+      state: () => ({ ... }), // o estado do módulo já está aninhado e não é afetado pela opção de namespace
       getters: {
         isAdmin () { ... } // -> getters['account/isAdmin']
       },
@@ -109,7 +111,7 @@ const store = new Vuex.Store({
       modules: {
         // herda o namespace do modulo pai
         myPage: {
-          state: { ... },
+          state: () => ({ ... }),
           getters: {
             profile () { ... } // -> getters['account/profile']
           }
@@ -119,7 +121,7 @@ const store = new Vuex.Store({
         posts: {
           namespaced: true,
 
-          state: { ... },
+          state: () => ({ ... }),
           getters: {
             popular () { ... } // -> getters['account/posts/popular']
           }
@@ -317,11 +319,9 @@ Este é exatamente o mesmo problema com `data` dentro dos componentes Vue. Entã
 
 ``` js
 const MyReusableModule = {
-  state () {
-    return {
-      foo: 'bar'
-    }
-  },
+  state: () => ({
+    foo: 'bar'
+  }),
   // mutações, ações, getters...
 }
 ```

+ 14 - 2
docs/ru/api/README.md

@@ -174,7 +174,7 @@ const store = new Vuex.Store({ ...options });
 
 ### subscribe
 
-* `subscribe(handler: Function): Function`
+* `subscribe(handler: Function, options?: Object): Function`
 
 Отслеживание вызова мутаций хранилища. Обработчик `handler` вызывается после каждой мутации и получает в качестве параметров дескриптор мутации и состояние после мутации:
 
@@ -185,13 +185,19 @@ store.subscribe((mutation, state) => {
 });
 ```
 
+По умолчанию, новый обработчик добавляется в конец цепочки, поэтому он будет выполняться после других обработчиков, добавленных раньше. Это поведение можно переопределить добавив `prepend: true` в `options`, что позволит добавлять обработчик в начало цепочки.
+
+```js
+store.subscribe(handler, { prepend: true })
+```
+
 Для прекращения отслеживания, необходимо вызвать возвращаемую методом функцию.
 
 Чаще всего используется в плагинах. [Подробнее](../guide/plugins.md)
 
 ### subscribeAction
 
-* `subscribeAction(handler: Function): Function`
+* `subscribeAction(handler: Function, options?: Object): Function`
 
 > Добавлено в версии 2.5.0
 
@@ -204,6 +210,12 @@ store.subscribeAction((action, state) => {
 });
 ```
 
+По умолчанию, новый обработчик добавляется в конец цепочки, поэтому он будет выполняться после других обработчиков, добавленных раньше. Это поведение можно переопределить добавив `prepend: true` в `options`, что позволит добавлять обработчик в начало цепочки.
+
+```js
+store.subscribeAction(handler, { prepend: true })
+```
+
 Для прекращения отслеживания, необходимо вызвать возвращаемую методом функцию.
 
 > Добавлено в версии 3.1.0

+ 11 - 11
docs/ru/guide/modules.md

@@ -8,14 +8,14 @@
 
 ```js
 const moduleA = {
-  state: { ... },
+  state: () => ({ ... }),
   mutations: { ... },
   actions: { ... },
   getters: { ... }
 }
 
 const moduleB = {
-  state: { ... },
+  state: () => ({ ... }),
   mutations: { ... },
   actions: { ... }
 }
@@ -37,7 +37,9 @@ store.state.b // -> состояние модуля `moduleB`
 
 ```js
 const moduleA = {
-  state: { count: 0 },
+  state: () => ({
+    count: 0
+  }),
   mutations: {
     increment(state) {
       // `state` указывает на локальное состояние модуля
@@ -94,7 +96,7 @@ const store = new Vuex.Store({
       namespaced: true,
 
       // содержимое модуля
-      state: { ... }, // состояние модуля автоматически вложено и не зависит от опции пространства имён
+      state: () => ({ ... }), // состояние модуля автоматически вложено и не зависит от опции пространства имён
       getters: {
         isAdmin () { ... } // -> getters['account/isAdmin']
       },
@@ -109,7 +111,7 @@ const store = new Vuex.Store({
       modules: {
         // наследует пространство имён из родительского модуля
         myPage: {
-          state: { ... },
+          state: () => ({ ... }),
           getters: {
             profile () { ... } // -> getters['account/profile']
           }
@@ -119,7 +121,7 @@ const store = new Vuex.Store({
         posts: {
           namespaced: true,
 
-          state: { ... },
+          state: () => ({ ... }),
           getters: {
             popular () { ... } // -> getters['account/posts/popular']
           }
@@ -319,11 +321,9 @@ store.registerModule(['nested', 'myModule'], {
 
 ```js
 const MyReusableModule = {
-  state() {
-    return {
-      foo: 'bar'
-    };
-  }
+  state: () => ({
+    foo: 'bar'
+  })
   // мутации, действия, геттеры...
 };
 ```

+ 11 - 0
docs/ru/guide/plugins.md

@@ -107,6 +107,11 @@ const logger = createLogger({
     // `mutation` — это объект `{ type, payload }`
     return mutation.type !== 'aBlacklistedMutation';
   },
+  actionFilter (action, state) {
+    // аналогично `filter`, но для действий
+    // `action` будет объектом `{ type, payload }`
+    return action.type !== 'aBlacklistedAction'
+  },
   transformer(state) {
     // обработать состояние перед логированием
     // например, позволяет рассматривать только конкретное поддерево
@@ -117,6 +122,12 @@ const logger = createLogger({
     // но это можно изменить
     return mutation.type;
   },
+  actionTransformer (action) {
+    // аналогично `mutationTransformer`, но для действий
+    return action.type
+  },
+  logActions: true, // логирование действий
+  logMutations: true, // логирование мутаций
   logger: console // реализация API `console`, по умолчанию `console`
 });
 ```

+ 11 - 11
docs/zh/guide/modules.md

@@ -8,14 +8,14 @@
 
 ``` js
 const moduleA = {
-  state: { ... },
+  state: () => ({ ... }),
   mutations: { ... },
   actions: { ... },
   getters: { ... }
 }
 
 const moduleB = {
-  state: { ... },
+  state: () => ({ ... }),
   mutations: { ... },
   actions: { ... }
 }
@@ -37,7 +37,9 @@ store.state.b // -> moduleB 的状态
 
 ``` js
 const moduleA = {
-  state: { count: 0 },
+  state: () => ({
+    count: 0
+  }),
   mutations: {
     increment (state) {
       // 这里的 `state` 对象是模块的局部状态
@@ -94,7 +96,7 @@ const store = new Vuex.Store({
       namespaced: true,
 
       // 模块内容(module assets)
-      state: { ... }, // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
+      state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
       getters: {
         isAdmin () { ... } // -> getters['account/isAdmin']
       },
@@ -109,7 +111,7 @@ const store = new Vuex.Store({
       modules: {
         // 继承父模块的命名空间
         myPage: {
-          state: { ... },
+          state: () => ({ ... }),
           getters: {
             profile () { ... } // -> getters['account/profile']
           }
@@ -119,7 +121,7 @@ const store = new Vuex.Store({
         posts: {
           namespaced: true,
 
-          state: { ... },
+          state: () => ({ ... }),
           getters: {
             popular () { ... } // -> getters['account/posts/popular']
           }
@@ -321,11 +323,9 @@ store.registerModule(['nested', 'myModule'], {
 
 ``` js
 const MyReusableModule = {
-  state () {
-    return {
-      foo: 'bar'
-    }
-  },
+  state: () => ({
+    foo: 'bar'
+  }),
   // mutation, action 和 getter 等等...
 }
 ```

+ 0 - 3
examples/classic/shopping-cart/app.js

@@ -1,11 +1,8 @@
-import 'babel-polyfill'
 import { createApp } from 'vue'
 import App from './components/App.vue'
 import store from './store'
 import { currency } from './currency'
 
-// Vue.filter('currency', currency)
-
 const app = createApp(App)
 
 app.use(store)

+ 2 - 2
examples/classic/shopping-cart/store/modules/cart.js

@@ -2,10 +2,10 @@ import shop from '../../api/shop'
 
 // initial state
 // shape: [{ id, quantity }]
-const state = {
+const state = () => ({
   items: [],
   checkoutStatus: null
-}
+})
 
 // getters
 const getters = {

+ 2 - 2
examples/classic/shopping-cart/store/modules/products.js

@@ -1,9 +1,9 @@
 import shop from '../../api/shop'
 
 // initial state
-const state = {
+const state = () => ({
   all: []
-}
+})
 
 // getters
 const getters = {}

+ 24 - 0
jest.config.js

@@ -0,0 +1,24 @@
+module.exports = {
+  rootDir: __dirname,
+  globals: {
+    __DEV__: true
+  },
+  moduleNameMapper: {
+    '^@/(.*)$': '<rootDir>/src/$1',
+    '^test/(.*)$': '<rootDir>/test/$1'
+  },
+  testMatch: ['<rootDir>/test/unit/**/*.spec.js'],
+  testPathIgnorePatterns: ['/node_modules/'],
+  setupFilesAfterEnv: [
+    './test/setup.js'
+  ],
+  "transform": {
+    "^.+\\.js$": "<rootDir>/node_modules/babel-jest"
+  },
+  coverageDirectory: 'coverage',
+  coverageReporters: ['json', 'lcov', 'text-summary', 'clover'],
+  collectCoverageFrom: [
+    'src/**/*.js',
+    '!src/index.cjs.js'
+  ]
+}

+ 23 - 23
package.json

@@ -21,13 +21,14 @@
     "build:main": "node scripts/build-main.js",
     "build:logger": "node scripts/build-logger.js",
     "lint": "eslint src test",
-    "test": "npm run lint && npm run test:unit && npm run test:ssr && npm run test:types && npm run test:e2e",
-    "test:unit": "jasmine JASMINE_CONFIG_PATH=test/unit/jasmine.json",
+    "test": "npm run lint && npm run test:types && npm run test:unit && npm run test:ssr && npm run test:e2e",
+    "test:unit": "jest --testPathIgnorePatterns test/e2e",
     "test:e2e": "node test/e2e/runner.js",
-    "test:ssr": "cross-env VUE_ENV=server jasmine JASMINE_CONFIG_PATH=test/unit/jasmine.json",
+    "test:ssr": "cross-env VUE_ENV=server jest --testPathIgnorePatterns test/e2e",
     "test:types": "tsc -p types/test",
+    "coverage": "jest --testPathIgnorePatterns test/e2e --coverage",
     "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
-    "release": "bash scripts/release.sh",
+    "release": "node scripts/release.js",
     "docs": "vuepress dev docs",
     "docs:build": "vuepress build docs"
   },
@@ -45,44 +46,43 @@
     "vue": "3.0.0-beta.4"
   },
   "devDependencies": {
+    "@babel/core": "^7.9.6",
+    "@babel/preset-env": "^7.9.6",
     "@rollup/plugin-buble": "^0.21.3",
     "@rollup/plugin-commonjs": "^11.1.0",
     "@rollup/plugin-node-resolve": "^7.1.3",
-    "@rollup/plugin-replace": "^2.3.1",
-    "@types/node": "^13.13.2",
-    "@vue/compiler-sfc": "3.0.0-beta.4",
-    "babel-core": "^6.22.1",
-    "babel-loader": "^7.1.2",
-    "babel-plugin-transform-object-rest-spread": "^6.23.0",
-    "babel-polyfill": "^6.22.0",
-    "babel-preset-env": "^1.5.1",
+    "@rollup/plugin-replace": "^2.3.2",
+    "@types/node": "^13.13.5",
+    "babel-jest": "^26.0.1",
+    "babel-loader": "^8.1.0",
     "brotli": "^1.3.2",
     "chalk": "^4.0.0",
-    "chromedriver": "^81.0.0",
+    "chromedriver": "^80.0.1",
     "conventional-changelog-cli": "^2.0.31",
     "cross-env": "^5.2.0",
     "cross-spawn": "^6.0.5",
     "css-loader": "^2.1.0",
-    "eslint": "^5.12.0",
-    "eslint-plugin-vue-libs": "^3.0.0",
+    "enquirer": "^2.3.5",
+    "eslint": "^6.8.0",
+    "eslint-plugin-vue-libs": "^4.0.0",
     "execa": "^4.0.0",
-    "express": "^4.14.1",
-    "jasmine": "2.8.0",
-    "jasmine-core": "2.8.0",
-    "jsdom": "^16.2.2",
+    "express": "^4.17.1",
+    "jest": "^26.0.1",
     "nightwatch": "^1.3.1",
     "nightwatch-helpers": "^1.2.0",
-    "rollup": "^2.7.2",
+    "regenerator-runtime": "^0.13.5",
+    "rollup": "^2.8.2",
     "rollup-plugin-terser": "^5.3.0",
+    "semver": "^7.3.2",
     "todomvc-app-css": "^2.1.0",
     "typescript": "^3.8.3",
-    "vue": "3.0.0-beta.4",
-    "vue-loader": "16.0.0-alpha.3",
+    "vue": "^2.5.22",
+    "vue-loader": "^15.2.1",
     "vue-template-compiler": "^2.5.22",
     "vuepress": "^0.14.1",
     "vuepress-theme-vue": "^1.1.0",
     "webpack": "^4.43.0",
     "webpack-dev-middleware": "^3.7.2",
-    "webpack-hot-middleware": "^2.19.1"
+    "webpack-hot-middleware": "^2.25.0"
   }
 }

+ 11 - 6
rollup.config.js

@@ -1,3 +1,4 @@
+import buble from '@rollup/plugin-buble'
 import replace from '@rollup/plugin-replace'
 import resolve from '@rollup/plugin-node-resolve'
 import commonjs from '@rollup/plugin-commonjs'
@@ -5,11 +6,10 @@ import { terser } from 'rollup-plugin-terser'
 import pkg from './package.json'
 
 const banner = `/*!
- /**
-  * vuex v${pkg.version}
-  * (c) ${new Date().getFullYear()} Evan You
-  * @license MIT
-  */`
+ * vuex v${pkg.version}
+ * (c) ${new Date().getFullYear()} Evan You
+ * @license MIT
+ */`
 
 export function createEntries(configs) {
   return configs.map((c) => createEntry(c))
@@ -40,11 +40,16 @@ function createEntry(config) {
   }
 
   c.plugins.push(replace({
-    __DEV__: config.format === 'es' && !config.browser
+    __VERSION__: pkg.version,
+    __DEV__: config.format !== 'iife' && !config.browser
       ? `(process.env.NODE_ENV !== 'production')`
       : config.env !== 'production'
   }))
 
+  if (config.transpile !== false) {
+    c.plugins.push(buble())
+  }
+
   c.plugins.push(resolve())
   c.plugins.push(commonjs())
 

+ 113 - 0
scripts/release.js

@@ -0,0 +1,113 @@
+const fs = require('fs')
+const path = require('path')
+const chalk = require('chalk')
+const semver = require('semver')
+const { prompt } = require('enquirer')
+const execa = require('execa')
+const currentVersion = require('../package.json').version
+
+const versionIncrements = [
+  'patch',
+  'minor',
+  'major'
+]
+
+const tags = [
+  'latest',
+  'next'
+]
+
+const inc = (i) => semver.inc(currentVersion, i)
+const bin = (name) => path.resolve(__dirname, `../node_modules/.bin/${name}`)
+const run = (bin, args, opts = {}) => execa(bin, args, { stdio: 'inherit', ...opts })
+const step = (msg) => console.log(chalk.cyan(msg))
+
+async function main() {
+  let targetVersion
+
+  const { release } = await prompt({
+    type: 'select',
+    name: 'release',
+    message: 'Select release type',
+    choices: versionIncrements.map(i => `${i} (${inc(i)})`).concat(['custom'])
+  })
+
+  if (release === 'custom') {
+    targetVersion = (await prompt({
+      type: 'input',
+      name: 'version',
+      message: 'Input custom version',
+      initial: currentVersion
+    })).version
+  } else {
+    targetVersion = release.match(/\((.*)\)/)[1]
+  }
+
+  if (!semver.valid(targetVersion)) {
+    throw new Error(`Invalid target version: ${targetVersion}`)
+  }
+
+  const { tag } = await prompt({
+    type: 'select',
+    name: 'tag',
+    message: 'Select tag type',
+    choices: tags
+  })
+
+  console.log(tag)
+
+  const { yes } = await prompt({
+    type: 'confirm',
+    name: 'yes',
+    message: `Releasing v${targetVersion} with the "${tag}" tag. Confirm?`
+  })
+
+  if (!yes) {
+    return
+  }
+
+  // Run tests before release.
+  step('\nRunning tests...')
+  await run('yarn', ['test'])
+
+  // Update the package version.
+  step('\nUpdating the package version...')
+  updatePackage(targetVersion)
+
+  // Build the package.
+  step('\nBuilding the package...')
+  await run('yarn', ['build'])
+
+  // Generate the changelog.
+  step('\nGenerating the changelog...')
+  await run('yarn', ['changelog'])
+
+  // Commit changes to the Git.
+  step('\nCommitting changes...')
+  await run('git', ['add', '-A'])
+  await run('git', ['commit', '-m', `release: v${targetVersion}`])
+
+  // Publish the package.
+  step('\nPublishing the package...')
+  await run ('yarn', [
+    'publish', '--tag', tag, '--new-version', targetVersion, '--no-commit-hooks',
+    '--no-git-tag-version'
+  ])
+
+  // Push to GitHub.
+  step('\nPushing to GitHub...')
+  await run('git', ['tag', `v${targetVersion}`])
+  await run('git', ['push', 'origin', `refs/tags/v${targetVersion}`])
+  await run('git', ['push'])
+}
+
+function updatePackage(version) {
+  const pkgPath = path.resolve(path.resolve(__dirname, '..'), 'package.json')
+  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
+
+  pkg.version = version
+
+  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
+}
+
+main().catch((err) => console.error(err))

+ 0 - 34
scripts/release.sh

@@ -1,34 +0,0 @@
-set -e
-echo "Enter release version: "
-read VERSION
-
-read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -r
-echo    # (optional) move to a new line
-if [[ $REPLY =~ ^[Yy]$ ]]
-then
-  echo "Releasing $VERSION ..."
-
-  # run tests
-  npm test 2>/dev/null
-
-  # build
-  VERSION=$VERSION npm run build
-
-  # generate the version so that the changelog can be generated too
-  yarn version --no-git-tag-version --no-commit-hooks --new-version $VERSION
-
-  # changelog
-  yarn changelog
-  echo "Please check the git history and the changelog and press enter"
-  read OKAY
-
-  # commit
-  git add -A
-  git commit -m "realese: v$VERSION"
-  git tag "v$VERSION"
-
-  # publish
-  git push origin refs/tags/v$VERSION
-  git push
-  npm publish --tag next
-fi

+ 26 - 12
src/store.js

@@ -148,18 +148,32 @@ export class Store {
       ? Promise.all(entry.map(handler => handler(payload)))
       : entry[0](payload)
 
-    return result.then(res => {
-      try {
-        this._actionSubscribers
-          .filter(sub => sub.after)
-          .forEach(sub => sub.after(action, this.state))
-      } catch (e) {
-        if (__DEV__) {
-          console.warn(`[vuex] error in after action subscribers: `)
-          console.error(e)
+    return new Promise((resolve, reject) => {
+      result.then(res => {
+        try {
+          this._actionSubscribers
+            .filter(sub => sub.after)
+            .forEach(sub => sub.after(action, this.state))
+        } catch (e) {
+          if (__DEV__) {
+            console.warn(`[vuex] error in after action subscribers: `)
+            console.error(e)
+          }
         }
-      }
-      return res
+        resolve(res)
+      }, error => {
+        try {
+          this._actionSubscribers
+            .filter(sub => sub.error)
+            .forEach(sub => sub.error(action, this.state, error))
+        } catch (e) {
+          if (__DEV__) {
+            console.warn(`[vuex] error in error action subscribers: `)
+            console.error(e)
+          }
+        }
+        reject(error)
+      })
     })
   }
 
@@ -217,7 +231,7 @@ export class Store {
   hasModule (path) {
     if (typeof path === 'string') path = [path]
 
-    if (process.env.NODE_ENV !== 'production') {
+    if (__DEV__) {
       assert(Array.isArray(path), `module path must be a string or an Array.`)
     }
 

+ 5 - 0
test/.eslintrc.json

@@ -0,0 +1,5 @@
+{
+  "env": {
+    "jest": true
+  }
+}

+ 5 - 0
test/setup.js

@@ -0,0 +1,5 @@
+import 'regenerator-runtime/runtime'
+import Vue from 'vue'
+import Vuex from '@/index'
+
+Vue.use(Vuex)

+ 0 - 5
test/unit/.eslintrc

@@ -1,5 +0,0 @@
-{
-  "env": {
-    "jasmine": true
-  }
-}

+ 17 - 17
test/unit/helpers.spec.js

@@ -91,7 +91,7 @@ describe('Helpers', () => {
   })
 
   it('mapState (with undefined states)', () => {
-    spyOn(console, 'error')
+    jest.spyOn(console, 'error').mockImplementation()
     const store = new Vuex.Store({
       modules: {
         foo: {
@@ -215,7 +215,7 @@ describe('Helpers', () => {
   })
 
   it('mapMutations (with undefined mutations)', () => {
-    spyOn(console, 'error')
+    jest.spyOn(console, 'error').mockImplementation()
     const store = new Vuex.Store({
       modules: {
         foo: {
@@ -378,7 +378,7 @@ describe('Helpers', () => {
   })
 
   it('mapGetters (with undefined getters)', () => {
-    spyOn(console, 'error')
+    jest.spyOn(console, 'error').mockImplementation()
     const store = new Vuex.Store({
       modules: {
         foo: {
@@ -404,8 +404,8 @@ describe('Helpers', () => {
   })
 
   it('mapActions (array)', () => {
-    const a = jasmine.createSpy()
-    const b = jasmine.createSpy()
+    const a = jest.fn()
+    const b = jest.fn()
     const store = new Vuex.Store({
       actions: {
         a,
@@ -423,8 +423,8 @@ describe('Helpers', () => {
   })
 
   it('mapActions (object)', () => {
-    const a = jasmine.createSpy()
-    const b = jasmine.createSpy()
+    const a = jest.fn()
+    const b = jest.fn()
     const store = new Vuex.Store({
       actions: {
         a,
@@ -445,7 +445,7 @@ describe('Helpers', () => {
   })
 
   it('mapActions (function)', () => {
-    const a = jasmine.createSpy()
+    const a = jest.fn()
     const store = new Vuex.Store({
       actions: { a }
     })
@@ -457,12 +457,12 @@ describe('Helpers', () => {
       })
     })
     vm.foo('foo')
-    expect(a.calls.argsFor(0)[1]).toBe('foobar')
+    expect(a.mock.calls[0][1]).toBe('foobar')
   })
 
   it('mapActions (with namespace)', () => {
-    const a = jasmine.createSpy()
-    const b = jasmine.createSpy()
+    const a = jest.fn()
+    const b = jest.fn()
     const store = new Vuex.Store({
       modules: {
         foo: {
@@ -488,7 +488,7 @@ describe('Helpers', () => {
   })
 
   it('mapActions (function with namespace)', () => {
-    const a = jasmine.createSpy()
+    const a = jest.fn()
     const store = new Vuex.Store({
       modules: {
         foo: {
@@ -505,12 +505,12 @@ describe('Helpers', () => {
       })
     })
     vm.foo('foo')
-    expect(a.calls.argsFor(0)[1]).toBe('foobar')
+    expect(a.mock.calls[0][1]).toBe('foobar')
   })
 
   it('mapActions (with undefined actions)', () => {
-    spyOn(console, 'error')
-    const a = jasmine.createSpy()
+    jest.spyOn(console, 'error').mockImplementation()
+    const a = jest.fn()
     const store = new Vuex.Store({
       modules: {
         foo: {
@@ -530,8 +530,8 @@ describe('Helpers', () => {
   })
 
   it('createNamespacedHelpers', () => {
-    const actionA = jasmine.createSpy()
-    const actionB = jasmine.createSpy()
+    const actionA = jest.fn()
+    const actionB = jest.fn()
     const store = new Vuex.Store({
       modules: {
         foo: {

+ 11 - 11
test/unit/hot-reload.spec.js

@@ -252,7 +252,7 @@ describe('Hot Reload', () => {
       }
     })
 
-    const spy = jasmine.createSpy()
+    const spy = jest.fn()
 
     const vm = mount(store, {
       computed: {
@@ -294,7 +294,7 @@ describe('Hot Reload', () => {
   it('provide warning if a new module is given', () => {
     const store = new Vuex.Store({})
 
-    spyOn(console, 'warn')
+    jest.spyOn(console, 'warn').mockImplementation()
 
     store.hotUpdate({
       modules: {
@@ -314,10 +314,10 @@ describe('Hot Reload', () => {
 
   it('update namespace', () => {
     // prevent to print notification of unknown action/mutation
-    spyOn(console, 'error')
+    jest.spyOn(console, 'error').mockImplementation()
 
-    const actionSpy = jasmine.createSpy()
-    const mutationSpy = jasmine.createSpy()
+    const actionSpy = jest.fn()
+    const mutationSpy = jest.fn()
 
     const store = new Vuex.Store({
       modules: {
@@ -334,9 +334,9 @@ describe('Hot Reload', () => {
     expect(store.state.a.value).toBe(1)
     expect(store.getters['a/foo']).toBe(1)
     store.dispatch('a/foo')
-    expect(actionSpy.calls.count()).toBe(1)
+    expect(actionSpy).toHaveBeenCalledTimes(1)
     store.commit('a/foo')
-    expect(actionSpy.calls.count()).toBe(1)
+    expect(actionSpy).toHaveBeenCalledTimes(1)
 
     store.hotUpdate({
       modules: {
@@ -352,18 +352,18 @@ describe('Hot Reload', () => {
 
     // should not be called
     store.dispatch('a/foo')
-    expect(actionSpy.calls.count()).toBe(1)
+    expect(actionSpy).toHaveBeenCalledTimes(1)
 
     // should be called
     store.dispatch('foo')
-    expect(actionSpy.calls.count()).toBe(2)
+    expect(actionSpy).toHaveBeenCalledTimes(2)
 
     // should not be called
     store.commit('a/foo')
-    expect(mutationSpy.calls.count()).toBe(1)
+    expect(mutationSpy).toHaveBeenCalledTimes(1)
 
     // should be called
     store.commit('foo')
-    expect(mutationSpy.calls.count()).toBe(2)
+    expect(mutationSpy).toHaveBeenCalledTimes(2)
   })
 })

+ 0 - 10
test/unit/jasmine.json

@@ -1,10 +0,0 @@
-{
-  "spec_dir": "test/unit",
-  "spec_files": [
-    "**/*.spec.js"
-  ],
-  "helpers": [
-    "../../node_modules/babel-register/lib/node.js",
-    "setup.js"
-  ]
-}

+ 1 - 1
test/unit/module/module-collection.spec.js

@@ -1,4 +1,4 @@
-import ModuleCollection from '../../../src/module/module-collection'
+import ModuleCollection from '@/module/module-collection'
 
 describe('ModuleCollection', () => {
   it('get', () => {

+ 1 - 1
test/unit/module/module.spec.js

@@ -1,4 +1,4 @@
-import Module from '../../../src/module/module'
+import Module from '@/module/module'
 
 describe('Module', () => {
   it('get state', () => {

+ 66 - 26
test/unit/modules.spec.js

@@ -62,8 +62,8 @@ describe('Modules', () => {
           }
         }
       })
-      const actionSpy = jasmine.createSpy()
-      const mutationSpy = jasmine.createSpy()
+      const actionSpy = jest.fn()
+      const mutationSpy = jest.fn()
       store.registerModule(['a', 'b'], {
         state: { value: 1 },
         getters: { foo: state => state.value },
@@ -94,8 +94,8 @@ describe('Modules', () => {
     it('dynamic module registration preserving hydration', () => {
       const store = new Vuex.Store({})
       store.replaceState({ a: { foo: 'state' }})
-      const actionSpy = jasmine.createSpy()
-      const mutationSpy = jasmine.createSpy()
+      const actionSpy = jest.fn()
+      const mutationSpy = jest.fn()
       store.registerModule('a', {
         namespaced: true,
         getters: { foo: state => state.foo },
@@ -116,7 +116,7 @@ describe('Modules', () => {
 
   // #524
   it('should not fire an unrelated watcher', done => {
-    const spy = jasmine.createSpy()
+    const spy = jest.fn()
     const store = new Vuex.Store({
       modules: {
         a: {
@@ -349,8 +349,8 @@ describe('Modules', () => {
     })
 
     it('module: namespace', () => {
-      const actionSpy = jasmine.createSpy()
-      const mutationSpy = jasmine.createSpy()
+      const actionSpy = jest.fn()
+      const mutationSpy = jest.fn()
 
       const store = new Vuex.Store({
         modules: {
@@ -385,8 +385,8 @@ describe('Modules', () => {
       const actionSpys = []
       const mutationSpys = []
       const createModule = (name, namespaced, children) => {
-        const actionSpy = jasmine.createSpy()
-        const mutationSpy = jasmine.createSpy()
+        const actionSpy = jest.fn()
+        const mutationSpy = jest.fn()
 
         actionSpys.push(actionSpy)
         mutationSpys.push(mutationSpy)
@@ -435,7 +435,7 @@ describe('Modules', () => {
         store.dispatch(type)
       })
       actionSpys.forEach(spy => {
-        expect(spy.calls.count()).toBe(1)
+        expect(spy).toHaveBeenCalledTimes(1)
       })
 
       // mutations
@@ -443,7 +443,7 @@ describe('Modules', () => {
         store.commit(type)
       })
       mutationSpys.forEach(spy => {
-        expect(spy.calls.count()).toBe(1)
+        expect(spy).toHaveBeenCalledTimes(1)
       })
     })
 
@@ -472,10 +472,10 @@ describe('Modules', () => {
     })
 
     it('module: action context is namespaced in namespaced module', done => {
-      const rootActionSpy = jasmine.createSpy()
-      const rootMutationSpy = jasmine.createSpy()
-      const moduleActionSpy = jasmine.createSpy()
-      const moduleMutationSpy = jasmine.createSpy()
+      const rootActionSpy = jest.fn()
+      const rootMutationSpy = jest.fn()
+      const moduleActionSpy = jest.fn()
+      const moduleMutationSpy = jest.fn()
 
       const store = new Vuex.Store({
         state: { value: 'root' },
@@ -494,14 +494,14 @@ describe('Modules', () => {
                 expect(rootGetters.foo).toBe('root')
 
                 dispatch('foo')
-                expect(moduleActionSpy.calls.count()).toBe(1)
+                expect(moduleActionSpy).toHaveBeenCalledTimes(1)
                 dispatch('foo', null, { root: true })
-                expect(rootActionSpy.calls.count()).toBe(1)
+                expect(rootActionSpy).toHaveBeenCalledTimes(1)
 
                 commit('foo')
-                expect(moduleMutationSpy.calls.count()).toBe(1)
+                expect(moduleMutationSpy).toHaveBeenCalledTimes(1)
                 commit('foo', null, { root: true })
-                expect(rootMutationSpy.calls.count()).toBe(1)
+                expect(rootMutationSpy).toHaveBeenCalledTimes(1)
 
                 done()
               }
@@ -515,8 +515,8 @@ describe('Modules', () => {
     })
 
     it('module: use other module that has same namespace', done => {
-      const actionSpy = jasmine.createSpy()
-      const mutationSpy = jasmine.createSpy()
+      const actionSpy = jest.fn()
+      const mutationSpy = jest.fn()
 
       const store = new Vuex.Store({
         modules: {
@@ -558,7 +558,7 @@ describe('Modules', () => {
     })
 
     it('module: warn when module overrides state', () => {
-      spyOn(console, 'warn')
+      jest.spyOn(console, 'warn').mockImplementation()
       const store = new Vuex.Store({
         modules: {
           foo: {
@@ -662,9 +662,9 @@ describe('Modules', () => {
 
     it('plugins', function () {
       let initState
-      const actionSpy = jasmine.createSpy()
+      const actionSpy = jest.fn()
       const mutations = []
-      const subscribeActionSpy = jasmine.createSpy()
+      const subscribeActionSpy = jest.fn()
       const store = new Vuex.Store({
         state: {
           a: 1
@@ -702,8 +702,8 @@ describe('Modules', () => {
     })
 
     it('action before/after subscribers', (done) => {
-      const beforeSpy = jasmine.createSpy()
-      const afterSpy = jasmine.createSpy()
+      const beforeSpy = jest.fn()
+      const afterSpy = jest.fn()
       const store = new Vuex.Store({
         actions: {
           [TEST]: () => Promise.resolve()
@@ -733,6 +733,46 @@ describe('Modules', () => {
     })
   })
 
+  it('action error subscribers', (done) => {
+    const beforeSpy = jest.fn()
+    const afterSpy = jest.fn()
+    const errorSpy = jest.fn()
+    const error = new Error()
+    const store = new Vuex.Store({
+      actions: {
+        [TEST]: () => Promise.reject(error)
+      },
+      plugins: [
+        store => {
+          store.subscribeAction({
+            before: beforeSpy,
+            after: afterSpy,
+            error: errorSpy
+          })
+        }
+      ]
+    })
+    store.dispatch(TEST, 2).catch(() => {
+      expect(beforeSpy).toHaveBeenCalledWith(
+        { type: TEST, payload: 2 },
+        store.state
+      )
+      expect(afterSpy).not.toHaveBeenCalled()
+      Vue.nextTick(() => {
+        expect(afterSpy).not.toHaveBeenCalledWith(
+          { type: TEST, payload: 2 },
+          store.state
+        )
+        expect(errorSpy).toHaveBeenCalledWith(
+          { type: TEST, payload: 2 },
+          store.state,
+          error
+        )
+        done()
+      })
+    })
+  })
+
   it('asserts a mutation should be a function', () => {
     expect(() => {
       new Vuex.Store({

+ 0 - 10
test/unit/setup.js

@@ -1,10 +0,0 @@
-import 'babel-polyfill'
-import { JSDOM } from 'jsdom'
-
-const dom = new JSDOM('<html><body></body></html>')
-
-global.document = dom.window.document
-global.window = dom.window
-global.navigator = dom.window.navigator
-
-global.__DEV__ = true

+ 16 - 29
test/unit/store.spec.js

@@ -1,6 +1,6 @@
 import { nextTick } from 'vue'
 import { mount } from './support/helpers'
-import Vuex from '../../src/index'
+import Vuex from '@/src/index'
 
 const TEST = 'TEST'
 const isSSR = process.env.VUE_ENV === 'server'
@@ -172,11 +172,11 @@ describe('Store', () => {
         }
       }
     })
-    const spy = jasmine.createSpy()
+    const spy = jest.fn()
     store._devtoolHook = {
       emit: spy
     }
-    const thenSpy = jasmine.createSpy()
+    const thenSpy = jest.fn()
     store.dispatch(TEST)
       .then(thenSpy)
       .catch(err => {
@@ -247,7 +247,7 @@ describe('Store', () => {
   })
 
   it('should warn silent option depreciation', () => {
-    spyOn(console, 'warn')
+    jest.spyOn(console, 'warn').mockImplementation()
 
     const store = new Vuex.Store({
       mutations: {
@@ -285,7 +285,7 @@ describe('Store', () => {
   })
 
   it('should not call root state function twice', () => {
-    const spy = jasmine.createSpy().and.returnValue(1)
+    const spy = jest.fn().mockReturnValue(1)
     new Vuex.Store({
       state: spy
     })
@@ -293,8 +293,8 @@ describe('Store', () => {
   })
 
   it('subscribe: should handle subscriptions / unsubscriptions', () => {
-    const subscribeSpy = jasmine.createSpy()
-    const secondSubscribeSpy = jasmine.createSpy()
+    const subscribeSpy = jest.fn()
+    const secondSubscribeSpy = jest.fn()
     const testPayload = 2
     const store = new Vuex.Store({
       state: {},
@@ -314,12 +314,12 @@ describe('Store', () => {
       store.state
     )
     expect(secondSubscribeSpy).toHaveBeenCalled()
-    expect(subscribeSpy.calls.count()).toBe(1)
-    expect(secondSubscribeSpy.calls.count()).toBe(2)
+    expect(subscribeSpy).toHaveBeenCalledTimes(1)
+    expect(secondSubscribeSpy).toHaveBeenCalledTimes(2)
   })
 
   it('subscribe: should handle subscriptions with synchronous unsubscriptions', () => {
-    const subscribeSpy = jasmine.createSpy()
+    const subscribeSpy = jest.fn()
     const testPayload = 2
     const store = new Vuex.Store({
       state: {},
@@ -336,11 +336,11 @@ describe('Store', () => {
       { type: TEST, payload: testPayload },
       store.state
     )
-    expect(subscribeSpy.calls.count()).toBe(1)
+    expect(subscribeSpy).toHaveBeenCalledTimes(1)
   })
 
   it('subscribeAction: should handle subscriptions with synchronous unsubscriptions', () => {
-    const subscribeSpy = jasmine.createSpy()
+    const subscribeSpy = jest.fn()
     const testPayload = 2
     const store = new Vuex.Store({
       state: {},
@@ -357,24 +357,11 @@ describe('Store', () => {
       { type: TEST, payload: testPayload },
       store.state
     )
-    expect(subscribeSpy.calls.count()).toBe(1)
+    expect(subscribeSpy).toHaveBeenCalledTimes(1)
   })
 
   // store.watch should only be asserted in non-SSR environment
   if (!isSSR) {
-    // TODO: This is working but can't make the test to pass because it throws;
-    // [Vue warn]: Unhandled error during execution of watcher callback
-    //
-    // it('strict mode: warn mutations outside of handlers', () => {
-    //   const store = new Vuex.Store({
-    //     state: {
-    //       a: 1
-    //     },
-    //     strict: true
-    //   })
-    //   expect(() => { store.state.a++ }).toThrow()
-    // })
-
     it('watch: with resetting vm', done => {
       const store = new Vuex.Store({
         state: {
@@ -385,7 +372,7 @@ describe('Store', () => {
         }
       })
 
-      const spy = jasmine.createSpy()
+      const spy = jest.fn()
       store.watch(state => state.count, spy)
 
       // reset store vm
@@ -418,8 +405,8 @@ describe('Store', () => {
       const getter = function getter (state, getters) {
         return state.count
       }
-      const spy = spyOn({ getter }, 'getter').and.callThrough()
-      const spyCb = jasmine.createSpy()
+      const spy = jest.spyOn({ getter }, 'getter')
+      const spyCb = jest.fn()
 
       store.watch(spy, spyCb)
 

+ 1 - 1
test/unit/util.spec.js

@@ -1,4 +1,4 @@
-import { find, deepCopy, forEachValue, isObject, isPromise, assert } from '../../src/util'
+import { find, deepCopy, forEachValue, isObject, isPromise, assert } from '@/util'
 
 describe('util', () => {
   it('find', () => {

+ 2 - 0
types/index.d.ts

@@ -81,10 +81,12 @@ export interface SubscribeOptions {
 }
 
 export type ActionSubscriber<P, S> = (action: P, state: S) => any;
+export type ActionErrorSubscriber<P, S> = (action: P, state: S, error: Error) => any;
 
 export interface ActionSubscribersObject<P, S> {
   before?: ActionSubscriber<P, S>;
   after?: ActionSubscriber<P, S>;
+  error?: ActionErrorSubscriber<P, S>;
 }
 
 export type SubscribeActionOptions<P, S> = ActionSubscriber<P, S> | ActionSubscribersObject<P, S>;

+ 56 - 0
types/test/index.ts

@@ -65,11 +65,67 @@ namespace StoreInstance {
     }
   });
 
+  store.subscribeAction({
+    before(action, state) {
+      action.type;
+      action.payload;
+      state.value;
+    },
+    error(action, state, error) {
+      action.type;
+      action.payload;
+      state.value;
+      error;
+    }
+  });
+
+  store.subscribeAction({
+    before(action, state) {
+      action.type;
+      action.payload;
+      state.value;
+    },
+    after(action, state) {
+      action.type;
+      action.payload;
+      state.value;
+    },
+    error(action, state, error) {
+      action.type;
+      action.payload;
+      state.value;
+      error;
+    }
+  });
+
+  store.subscribeAction({
+    after(action, state) {
+      action.type;
+      action.payload;
+      state.value;
+    }
+  });
+
   store.subscribeAction({
     after(action, state) {
       action.type;
       action.payload;
       state.value;
+    },
+    error(action, state, error) {
+      action.type;
+      action.payload;
+      state.value;
+      error;
+    }
+  });
+
+  store.subscribeAction({
+    error(action, state, error) {
+      action.type;
+      action.payload;
+      state.value;
+      error;
     }
   });