TypeScript で実装した Nuxt.js/Store をテストする

TypeScript で実装した Nuxt.js/Store をテストしたい

## 成果物
出来上がりはこちらに
https://github.com/hirose504/nuxt-typescript-vuex-module-decorators-test

## 準備

### create nuxt-app

```bash
$ yarn create nuxt-app nuxt-typescript-vuex-module-decorators-test
yarn create v1.21.1
create-nuxt-app v2.14.0
✨  Generating Nuxt.js project in nuxt-typescript-vuex-module-decorators-test
? Project name nuxt-typescript-vuex-module-decorators-test
? Project description My posh Nuxt.js project
? Author name hirose504
? Choose the package manager Yarn
? Choose UI framework None
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose linting tools ESLint, Prettier
? Choose test framework Jest
? Choose rendering mode Universal (SSR)
? Choose development tools jsconfig.json (Recommended for VS Code)

🎉  Successfully created project nuxt-typescript-vuex-module-decorators-test

  To get started:

	cd nuxt-typescript-vuex-module-decorators-test
	yarn dev

  To build & start for production:

	cd nuxt-typescript-vuex-module-decorators-test
	yarn build
	yarn start

  To test:

	cd nuxt-typescript-vuex-module-decorators-test
	yarn test

✨  Done in 182.69s.
```

### typescript setup
https://typescript.nuxtjs.org/ja/guide/setup.html
https://typescript.nuxtjs.org/ja/guide/runtime.html
https://typescript.nuxtjs.org/ja/guide/lint.html

#### add vuex-module-decorators
https://championswimmer.in/vuex-module-decorators/

```bash
$ yarn add vuex-module-decorators
```

```diff
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,6 +9,7 @@
       "esnext.asynciterable",
       "dom"
     ],
+    "experimentalDecorators": true,
     "esModuleInterop": true,
     "allowJs": true,
     "sourceMap": true,
```

#### add ts-jest
https://github.com/kulshekhar/ts-jest

```bash
$ yarn add --dev ts-jest @types/jest
```

```diff
--- a/jest.config.js
+++ b/jest.config.js
@@ -4,14 +4,16 @@ module.exports = {
     '^~/(.*)$': '<rootDir>/$1',
     '^vue$': 'vue/dist/vue.common.js'
   },
-  moduleFileExtensions: ['js', 'vue', 'json'],
+  moduleFileExtensions: ['ts', 'js', 'vue', 'json'],
   transform: {
+    '^.+\\.ts$': 'ts-jest',
     '^.+\\.js$': 'babel-jest',
     '.*\\.(vue)$': 'vue-jest'
   },
   collectCoverage: true,
   collectCoverageFrom: [
     '<rootDir>/components/**/*.vue',
-    '<rootDir>/pages/**/*.vue'
+    '<rootDir>/pages/**/*.vue',
+    '<rootDir>/store/**/*.ts'
   ]
 }
```

```diff
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -26,7 +26,8 @@
     },
     "types": [
       "@types/node",
-      "@nuxt/types"
+      "@nuxt/types",
+      "@types/jest"
     ]
   },
   "exclude": [
```

## 実装
https://ja.nuxtjs.org/guide/vuex-store/ をベースに TypeScript で書く

### types
```ts:~/types/index.d.ts
export type Todo = {
  text: string
  done: boolean
}
```

### store

```ts:~/store/todos.ts
import { Module, VuexModule, Mutation } from 'vuex-module-decorators'
import { Todo } from '~/types'

@Module({ name: 'todos', stateFactory: true, namespaced: true })
export default class TodosModule extends VuexModule {
  list: Todo[] = []

  @Mutation
  add(text: string) {
    this.list.push({
      text,
      done: false
    })
  }

  @Mutation
  remove(todo: Todo) {
    this.list.splice(this.list.indexOf(todo), 1)
  }

  @Mutation
  toggle(todo: Todo) {
    todo.done = !todo.done
  }
}
```

```ts:~/store/index.ts
import { Store } from 'vuex'
import { initialiseStores } from '~/utils/store-accessor'
const initializer = (store: Store<any>) => initialiseStores(store)
export const plugins = [initializer]
export * from '~/utils/store-accessor'
```

```ts:~/utils/store-accessor.ts
/* eslint-disable import/no-mutable-exports */
import { Store } from 'vuex'
import { getModule } from 'vuex-module-decorators'
import TodosModule from '~/store/todos'

let todosStore: TodosModule

function initialiseStores(store: Store<any>): void {
  todosStore = getModule(TodosModule, store)
}

export { initialiseStores, todosStore }
```

### pages

```vue:~/pages/todos.vue
<template>
  <ul>
    <li v-for="todo in todos" :key="todo.index">
      <input type="checkbox" :checked="todo.done" @change="toggle(todo)" />>
      <span :class="{ done: todo.done }">{{ todo.text }}</span>
    </li>
    <li>
      <input placeholder="What needs to be done?" @keyup.enter="addTodo" />
    </li>
  </ul>
</template>

<script>
import { mapMutations } from 'vuex'

export default {
  computed: {
    todos() {
      return this.$store.state.todos.list
    }
  },
  methods: {
    addTodo(e) {
      this.$store.commit('todos/add', e.target.value)
      e.target.value = ''
    },
    ...mapMutations({
      toggle: 'todos/toggle'
    })
  }
}
</script>

<style>
.done {
  text-decoration: line-through;
}
</style>
```

## テスト

```ts:~/test/store/todos.test.ts
import { createStore } from '~/.nuxt/store'
import { initialiseStores, todosStore } from '~/utils/store-accessor'

describe('TodosModule', () => {
  beforeEach(() => {
    initialiseStores(createStore())
  })

  test('init', () => {
    expect(todosStore.list).toEqual([])
  })

  test('add', () => {
    todosStore.add('test')
    expect(todosStore.list).toEqual([{ text: 'test', done: false }])
  })

  test('remove', () => {
    todosStore.add('aaaa')
    todosStore.add('bbbb')
    todosStore.add('cccc')
    todosStore.remove(todosStore.list[1])
    expect(todosStore.list).toEqual([
      { text: 'aaaa', done: false },
      { text: 'cccc', done: false }
    ])
  })

  test('toggle', () => {
    todosStore.add('aaaa')
    todosStore.add('bbbb')
    todosStore.add('cccc')
    todosStore.toggle(todosStore.list[1])
    expect(todosStore.list).toEqual([
      { text: 'aaaa', done: false },
      { text: 'bbbb', done: true },
      { text: 'cccc', done: false }
    ])
  })
})
```

### 実行

```bash
$ yarn test                                                                                                           master
yarn run v1.21.1
$ jest
 PASS  test/store/todos.test.ts
 PASS  test/Logo.spec.js
------------|----------|----------|----------|----------|-------------------|
File        |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
------------|----------|----------|----------|----------|-------------------|
All files   |    70.83 |        0 |       60 |    66.67 |                   |
 components |      100 |      100 |      100 |      100 |                   |
  Logo.vue  |      100 |      100 |      100 |      100 |                   |
 pages      |        0 |        0 |        0 |        0 |                   |
  index.vue |        0 |      100 |        0 |        0 |              1,28 |
  todos.vue |        0 |        0 |        0 |        0 |     1,14,19,24,25 |
 store      |      100 |      100 |      100 |      100 |                   |
  index.ts  |      100 |      100 |      100 |      100 |                   |
  todos.ts  |      100 |      100 |      100 |      100 |                   |
------------|----------|----------|----------|----------|-------------------|

Test Suites: 2 passed, 2 total
Tests:       5 passed, 5 total
Snapshots:   0 total
Time:        5.098s
Ran all test suites.
✨  Done in 8.17s.
```