CypressでVuetifyを扱う場合のテクニック

仕事でNuxtとVuetifyを使ってWEBアプリのフロントエンドを実装しています。リリースごとに毎回手動で動作確認を行っていて、毎回面倒なので、Cypress（E2Eテスト）を導入して正常系だけでも動作確認を自動化し省力化を図りました。

NuxtとVuetifyを使用しているプロジェクトにCypressを導入するにあたって、地味にハマった箇所を共有したいと思います。

# 環境
使用する主要なライブラリとバージョンは下記になります。

| ライブラリ | バージョン |
|:-:|:-:|
| Nuxt | 2.15.8 |
| Vue | 2.7.8 |
| Vuetify | 2.6.8 |
| Cypress | 10.4.0 |

# テストプロジェクトの作成
`npx create-nuxt-app`コマンドを使ってテストプロジェクトを作成します。設定は基本的に適当に以下にしましたが、後程使いますので、`UI framework`では必ず`Vuetify.js`を選択して下さい。

```
% npx create-nuxt-app nuxt-cypress-app

create-nuxt-app v4.0.0
✨  Generating Nuxt.js project in nuxt-cypress-app
? Project name: nuxt-cypress-app
? Programming language: JavaScript
? Package manager: Yarn
? UI framework: Vuetify.js
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Linting tools: ESLint, Prettier
? Testing framework: Jest
? Rendering mode: Single Page App
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: jsconfig.json (Recommended for VS Code if you're not using typescript), Dependabot (For auto-updating dependencies, GitHub only)
? Continuous integration: GitHub Actions (GitHub only)
? What is your GitHub username? yamada
? Version control system: Git
```

cypressをインストールします。
```
$ cd nuxt-cypress-app
$ yarn add -D cypress
```

プロジェクト直下に`cypress`ディレクトリを作成し、以下のcypressの設定ファイル（cypress.config.js）とテストファイルを格納するディレクトリ（tests）を作成します。

```:cypressディレクトリ
.
├── cypress.config.js
└── tests
```

```:cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    specPattern: "**/tests/*.spec.js",
    supportFile: false,
    video: true,
    videoUploadOnPasses: false,
    defaultCommandTimeout: 10000
  }
})
```

簡単にE2Eテストを実行できるよう、プロジェクト直下の`package.json`の`scripts`に`"e2e": "cypress run --config-file=cypress/cypress.config.js --browser chrome"`を追加します。`npm run e2e`でE2Eテストを実行できるようになりました。

テスト対象のファイルを作成します。今回は名前や生年月日などを入力する画面と完了画面を作成します。
```:入力画面
<template>
  <v-container fluid>
    <v-text-field
      ref="name"
      :value="name"
      label=""
      maxlength="50"
      outlined
      data-cy="name"
      @change="(value) => (name = value)"
    >
      <template #prepend>Name</template>
    </v-text-field>

    <v-menu
      v-model="menu"
      :close-on-content-click="false"
      :nudge-right="40"
      transition="scale-transition"
      offset-y
      hide-details
      min-width="auto"
    >
      <template #activator="{ on, attrs }">
        <v-text-field
          id="birthday"
          ref="birthday"
          v-model="birthday"
          label=""
          outlined
          readonly
          clearable
          data-cy="birthday-text-field"
          v-bind="attrs"
          v-on="on"
          @click:clear="birthday = null"
        >
          <template #prepend>Birthday</template>
        </v-text-field>
      </template>
      <v-date-picker
        v-model="birthday"
        year-icon="mdi-calendar-edit"
        prev-icon="mdi-skip-previous"
        next-icon="mdi-skip-next"
        data-cy="birthday-date-picker"
        :day-format="(date) => new Date(date).getDate()"
        @input="menu = false"
      ></v-date-picker>
    </v-menu>

    <v-radio-group
      ref="sex"
      v-model="sex"
      row
      class="mt-0 mb-0"
    >
      <template #prepend>Sex</template>
      <v-radio
        id="male"
        value="male"
        label="male"
        data-cy="sex-male"
      ></v-radio>
      <v-radio
        id="female"
        value="female"
        label="female"
        data-cy="sex-female"
      ></v-radio>
    </v-radio-group>

    <v-row class="my-5">
      <v-col cols="10">
        <v-checkbox
          v-model="agreedWithEula"
          :label="`I agree`"
          color="primary"
          data-cy="agreed-with-eula"
        >
        </v-checkbox>
      </v-col>
    </v-row>

    <v-btn
      class="mx-0"
      cols="12"
      large
      block
      color="primary"
      nuxt
      data-cy="next-btn"
      @click="register"
    >
      register
    </v-btn>

  </v-container>
</template>

<script>
export default {
  name: 'RegisterIndex',
  data() {
    return {
      menu: false,
      name: '',
      birthday: '',
      sex: '',
      agreedWithEula: false
    }
  },
  methods: {
    register() {
      this.$router.push(
          `/register/complete/`
        )
    }
  }
}
</script>

<style scoped>
</style>

```

```:完了画面
<template>
  <v-container fluid>
    Your registration is complete.
  </v-container>
</template>

<script>
export default {
  name: 'RegisterComplete'
}
</script>

<style scoped>
</style>
```

# E2Eテストの実装
テスト対象のファイルができたら、実際にE2Eテストの実装します。今回は

1. 入力画面のそれぞれの項目に入力
1. 入力項目に入力できたら「Register」ボタンを押下
1. 完了画面に遷移し、「Your registration is complete.」のテキストがあることを検証

のシナリオで作成します。

cypressのテストディレクトリに`register.spec.js`という名前でテストファイルを作成します。

```:E2Eテスト
describe('test register page', () => {
  it('should register', () => {
    cy.visit('/register/')
    cy.get('[data-cy=name]').type('John')
    cy.get('[data-cy=birthday-text-field]').click()
    const datePicker = cy.get('[data-cy=birthday-date-picker]')
    for (let i = 0; i < 18; i++) {
      datePicker.get('[aria-label="Previous month"]').click()
    }
    cy.get('[data-cy=birthday-date-picker]').within(() => {
      cy.wait(1000)
      cy.get('div').contains('8').click()
    })

    cy.get('[data-cy=sex-female]').parent().click()
    cy.get('[data-cy=agreed-with-eula]').parent().click()
    cy.wait(1000)
    cy.get('[data-cy=next-btn]').click()
    cy.wait(1000)

    cy.contains(
      'Your registration is complete.'
    ).should('exist')
  })
})
```

`npm run e2e`でCypressを実際に実行すると、下記のようにE2Eテストが走って、動画も撮ることができます。

![Videotogif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76958/170a6216-cece-fd6f-599c-eebd72cd84a7.gif)

# CypressでVuetifyを扱ってハマった箇所
## `v-radio`や`v-checkbox`でチェックがつかない

`cy.get('[data-cy=agreed-with-eula]').click()`と単純にclickメソッドを呼ぶだけではチェックがつきませんでした。
`cy.get('[data-cy=agreed-with-eula]').parent().click()`のようにparentメソッドを呼ぶ必要がある（理由についても調べたがまだ分かっていない）。

## `v-date-picker`で特定の日付を選択したい
```
// date pickerのコンポーネントを取得
const datePicker = cy.get('[data-cy=birthday-date-picker]')
// プロジェクトで現在の日付から18ヶ月前を選択する必要があるので、「前月に戻る」アイコンを18回押下
for (let i = 0; i < 18; i++) {
  datePicker.get('[aria-label="Previous month"]').click()
}
cy.get('[data-cy=birthday-date-picker]').within(() => {
  // 後続で日付のDOMが取得できるよう、ブラウザのレンダリングを待つ（とりあえず1秒）
  cy.wait(1000)
  // 適当に8日をクリック
  cy.get('div').contains('8').click()
})
```

## 最後に
上記テストプロジェクトは[Github](https://github.com/satsuma0711/nuxt-cypress-app)にアップしましたので、分からないことがあればクローンして実際に試してみてください。