[[🦉nuxt2-vuetify2-playwright-sandbox]]に[[Testing Library]]を入れてみる。 <div class="link-card"> <div class="link-card-header"> <img src="https://testing-library.com/img/octopus-32x32.png" class="link-card-site-icon"/> <span class="link-card-site-name">testing-library.com</span> </div> <div class="link-card-body"> <div class="link-card-content"> <div> <p class="link-card-title">Intro | Testing Library</p> </div> <div class="link-card-description"> Vue Testing Library builds on top of DOM Testing Library by adding APIs for </div> </div> <img src="https://testing-library.com/img/octopus-128x128.png" class="link-card-image" /> </div> <a href="https://testing-library.com/docs/vue-testing-library/intro/"></a> </div> ## インストール ```console npm install --save-dev @testing-library/vue@5 ``` ## [[Jest]]のインストール [[Testing Library]]にテストメソッドやアサーションメソッドは含まれないため、[[Jest]]をインストールする。 ```console npm i -D jest babel-jest @babel/core @babel/preset-env @babel/preset-typescript @types/jest npm install --save-dev @jest/globals ``` `babel.config.js`を作成。 ```js module.exports = { presets: [ ['@babel/preset-env', {targets: {node: 'current'}}], '@babel/preset-typescript' ], }; ``` ## [[vue-jest]]のインストール ```console npm i -D @vue/vue2-jest@29 ``` > [!note] > [公式ドキュメント](https://github.com/vuejs/vue-jest#installation) には`@29`の案内はされていないが、[[babel-jest]]のバージョンが29だと[[dependencies]]のエラーとなるため29を指定してみたら動いた。 ## テストファイル作成 `pages/buttons.vue.spec.ts`を作成。 ```ts import { render, screen, fireEvent } from "@testing-library/vue"; import { test, expect } from "@jest/globals"; import buttons from "./buttons.vue"; test("increments value on click", async () => { render(buttons); expect(screen.queryByText("押すと無効になる")).toBeTruthy(); const button = screen.getByText("押すと無効になる"); await fireEvent.click(button); }); ``` ## テスト実行 ```console npx jest vue.spec.ts ``` ## NGと向き合う このままでは動かなかったのでエラーと向き合って修正していく。 ### その1 以下のエラーになる。 ```error Details: C:\Users\syoum\git\github.com\tadashi-aikawa\nuxt2-vuetify2-playwright-sandbox\pages\buttons.vue:1 ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){<script setup lang="ts"> ^ SyntaxError: Unexpected token '<' 1 | import { render, screen, fireEvent } from "@testing-library/vue"; 2 | import { test, expect } from "@jest/globals"; > 3 | import buttons from "./buttons.vue"; | ^ 4 | 5 | test("increments value on click", async () => { 6 | render(buttons); at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1495:14) at Object.require (pages/buttons.vue.spec.ts:3:1) ``` [[vue-jest]]をインストールしていなかったから。 ### その2 以下のエラーになる。 ```error ● Validation Error: Module vue-jest in the transform option was not found. <rootDir> is: C:\Users\syoum\git\github.com\tadashi-aikawa\nuxt2-vuetify2-playwright-sandbox Configuration Documentation: https://jestjs.io/docs/configuration ``` `vue-jest`ではダメ。 ```diff transform: { - "^.+\\.vuequot;: "vue-jest", + "^.+\\.vuequot;: "@vue/vue2-jest", }, ``` ### その3 ```error Details: C:\Users\syoum\git\github.com\tadashi-aikawa\nuxt2-vuetify2-playwright-sandbox\pages\buttons.vue.spec.ts:1 ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){import { render, screen, fireEvent } from "@testing-library/vue"; ^^^^^^ SyntaxError: Cannot use import statement outside a module at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1495:14) ``` `jest.config.js`が不十分。 ```js /** @type {import('jest').Config} */ const config = { transform: { "^.+\\.(js|ts)quot;: "babel-jest", "^.+\\.vuequot;: "@vue/vue2-jest", }, }; module.exports = config; ``` ### その4 ```error Cannot find module '~/components/card-wrapper.vue' from 'pages/buttons.vue' Require stack: pages/buttons.vue pages/buttons.vue.spec.ts 1 | <script setup lang="ts"> > 2 | import CardWrapper from "~/components/card-wrapper.vue"; | ^ 3 | import { ref } from "vue"; 4 | 5 | const clicked = ref(false); at Resolver._throwModNotFoundError (node_modules/jest-resolve/build/resolver.js:427:11) at Object.<anonymous> (pages/buttons.vue:2:1) at Object.require (pages/buttons.vue.spec.ts:3:1) ``` `jest.config.js`に[[moduleNameMapper]]の設定を追加。 ```js /** @type {import('jest').Config} */ const config = { transform: { "^.+\\.(js|ts)quot;: "babel-jest", "^.+\\.vuequot;: "@vue/vue2-jest", }, moduleNameMapper: { "^~/(.*)quot;: "<rootDir>/$1", "^@/(.*)quot;: "<rootDir>/$1", }, }; module.exports = config; ``` ### その5 ```error ● increments value on click The error below may be caused by using the wrong test environment, see https://jestjs.io/docs/configuration#testenvironment-string. Consider using the "jsdom" test environment. ReferenceError: document is not defined 4 | 5 | test("increments value on click", async () => { > 6 | render(buttons); | ^ 7 | 8 | expect(screen.queryByText("押すと無効になる")).toBeTruthy(); 9 | at render (node_modules/@testing-library/vue/dist/render.js:27:13) at Object.<anonymous> (pages/buttons.vue.spec.ts:6:9) ``` [[Jest]]のv28では[[jest-environment-jsdom]]を明示的にインストールしなければいけない。 https://zenn.dev/keita_hino/articles/488d31e8c4a240 https://jestjs.io/docs/28.x/upgrading-to-jest28#jsdom ```console npm i -D jest-environment-jsdom ``` `jest.config.js`の`testEnvironment`にも`jsdom`を指定する必要あり。 ```js /** @type {import('jest').Config} */ const config = { testEnvironment: "jsdom", transform: { "^.+\\.(js|ts)quot;: "babel-jest", "^.+\\.vuequot;: "@vue/vue2-jest", }, moduleNameMapper: { "^~/(.*)quot;: "<rootDir>/$1", "^@/(.*)quot;: "<rootDir>/$1", }, }; module.exports = config; ``` ### その6 ```error console.error [Vue warn]: Unknown custom element: <v-card> - did you register the component correctly? For recursive components, make sure to provide the "name" option. found in ---> <CardWrapper> <Buttons> <Root> 4 | 5 | test("increments value on click", async () => { > 6 | render(buttons); | ^ 7 | 8 | expect(screen.queryByText("押すと無効になる")).toBeTruthy(); 9 | ``` `setup-jest.js`を作成。 ```js import Vue from "vue"; import Vuetify from "vuetify"; Vue.use(Vuetify); export default new Vuetify(); ``` `jest.config.js`で`setup-jest.js`をセットアップファイルとして指定。 ```js /** @type {import('jest').Config} */ const config = { testEnvironment: "jsdom", transform: { "^.+\\.(js|ts)quot;: "babel-jest", "^.+\\.vuequot;: "@vue/vue2-jest", }, moduleNameMapper: { "^~/(.*)quot;: "<rootDir>/$1", "^@/(.*)quot;: "<rootDir>/$1", }, setupFiles: ["<rootDir>/setup-jest.js"], }; module.exports = config; ``` テスト実行ファイルで`~/setup-jest`からVuetifyのインスタンスをインポートし、renderに指定する。 ```ts import { render, screen, fireEvent } from "@testing-library/vue"; import { test, expect } from "@jest/globals"; import buttons from "./buttons.vue"; import vuetify from "~/setup-jest"; test("increments value on click", async () => { render(buttons, { vuetify }); expect(screen.queryByText("押すと無効になる")).toBeTruthy(); const button = screen.getByText("押すと無効になる"); await fireEvent.click(button); }); ``` ## テストケースをリアルにする ボタンのクリック前後でボタンがenabledかどうかを調べたい。ただ、今の環境ではそのようなアサーションメソッドが存在しない。[[jest-dom]]をインストールする。 ```console npm install --save-dev @testing-library/jest-dom ``` テストコードを少し修正する。 ```ts import { render, screen, fireEvent } from "@testing-library/vue"; import "@testing-library/jest-dom"; import { test } from "@jest/globals"; import buttons from "./buttons.vue"; import vuetify from "~/setup-jest"; test("increments value on click", async () => { render(buttons, { vuetify }); const button = screen.getByRole("button", { name: "押すと無効になる" }); expect(button).toBeEnabled(); await fireEvent.click(button); expect(button).toBeDisabled(); }); ``` これは以下の処理を行っている。 1. 押すと無効になるボタンを取得 2. 1が有効であることを確認 3. 1をクリック 4. 1が無効であることを確認 実行してテスト通過すればOK。 ## [[package.json]]にコマンド追加 `npm test`で実行できるよう[[package.json]]に加筆する。 ```json "scripts": { "test": "jest vue.spec.ts" }, ``` ## 対応コミット <div class="link-card"> <div class="link-card-header"> <img src="https://github.githubassets.com/favicons/favicon.svg" class="link-card-site-icon"/> <span class="link-card-site-name">GitHub</span> </div> <div class="link-card-body"> <div class="link-card-content"> <div> <p class="link-card-title">Testing Libraryを実験的に追加 · tadashi-aikawa/nuxt2-vuetify2-playwright-sandbox@d95d5bb</p> </div> <div class="link-card-description"> Contribute to tadashi-aikawa/nuxt2-vuetify2-playwright-sandbox development by creating an account on... </div> </div> <img src="https://opengraph.githubassets.com/aa9db87eb4e59f0c8e0d46aa386d1e0d8a5e845121f0e7972ace34926b47c504/tadashi-aikawa/nuxt2-vuetify2-playwright-sandbox/commit/d95d5bbbcc21adf2b8dabf39cfe944afe7d11718" class="link-card-image" /> </div> <a href="https://github.com/tadashi-aikawa/nuxt2-vuetify2-playwright-sandbox/commit/d95d5bbbcc21adf2b8dabf39cfe944afe7d11718"></a> </div> ## 通信について ブラウザのオブジェクト(`window`とか)に依存しない実装なら[[E2Eテスト]]と同程度に動く。ただやはり、ユニットテスト向きな気がする。 [[E2Eテスト]]と領域が被ると、[[Page Object Model]]を重複して作成する羽目になりそう。ただ、`screen`は`page`とIF似てそうなのでなんとかならないか...。[[Locator]]ではなくHTMLElementを返却しているからおそらく厳しそう。 ということもあり、少なくともテスト範囲は被らない方がよさそうだ。