WSL2 + Playwrightな環境にて、codegen機能によりReactアプリやDjango管理サイト向けのテストコードを自動生成してみた

前回の記事にて、WSL2上にPlaywrightをインストールして動作することを確認しました。
WSL2上にインストールしたPlaywrightを、Visual Studio Codeから動かしてみた - メモ的な思考的な

これにより環境ができたため、次はPlaywrightのcodegen機能でテストコードを自動生成してみることにしました。
Test Generator | Playwright

特に、codegen機能で生成されるテストコードが

  • ReactのようなSPAアプリ
  • Django管理サイトのような認証が必要なアプリ

にも対応しているのか気になったため、試してみたときのメモを残します。

 
目次

 

環境

Playwright環境は前回同様とします。

 
また、ReactやDjango管理サイトを持つアプリは、以前作成したものを流用し、WSL2上に構築します。
django-vite を使って、Django の template 上で React を動かしてみた - メモ的な思考的な

  • Django環境
  • React 環境
    • React 18.2.0
    • React Router 6.4.4
    • Vite 3.2.3
    • TypeScript 4.6.4

 
なお、ReactやDjangoは開発モードで起動しておきます。

# React
$ npm run dev

# Django
$ python manage.py runserver

 

自動生成するテストコード

SPAなReactアプリ向け

まずはSPAなReactアプリ向けのテストコードを生成してみます。

なお、現在の公式ドキュメントでは生成したテストコードの出力先についての記載が見当たりませんでした。

そこで、 npx playwright codegen --help によりヘルプを確認してみます。

$ npx playwright codegen --help
Usage: npx playwright codegen [options] [url]

open page and generate code for user actions

Options:
  -o, --output <file name>        saves the generated script to a file
  --target <language>             language to generate, one of javascript, playwright-test, python, python-async, python-pytest, csharp, csharp-mstest, csharp-nunit, java (default:
                                  "playwright-test")
  --save-trace <filename>         record a trace for the session and save it to a file
  -b, --browser <browserType>     browser to use, one of cr, chromium, ff, firefox, wk, webkit (default: "chromium")
  --block-service-workers         block service workers
  --channel <channel>             Chromium distribution channel, "chrome", "chrome-beta", "msedge-dev", etc
  --color-scheme <scheme>         emulate preferred color scheme, "light" or "dark"
  --device <deviceName>           emulate device, for example  "iPhone 11"
  --geolocation <coordinates>     specify geolocation coordinates, for example "37.819722,-122.478611"
  --ignore-https-errors           ignore https errors
  --load-storage <filename>       load context storage state from the file, previously saved with --save-storage
  --lang <language>               specify language / locale, for example "en-GB"
  --proxy-server <proxy>          specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"
  --proxy-bypass <bypass>         comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"
  --save-har <filename>           save HAR file with all network activity at the end
  --save-har-glob <glob pattern>  filter entries in the HAR by matching url against this glob pattern
  --save-storage <filename>       save context storage state at the end, for later use with --load-storage
  --timezone <time zone>          time zone to emulate, for example "Europe/Rome"
  --timeout <timeout>             timeout for Playwright actions in milliseconds, no timeout by default
  --user-agent <ua string>        specify user agent string
  --viewport-size <size>          specify browser viewport size in pixels, for example "1280, 720"
  -h, --help                      display help for command

Examples:

  $ codegen
  $ codegen --target=python
  $ codegen -b webkit https://example.com

 
オプションが色々と表示されましたが

-o, --output <file name>        saves the generated script to a file

と、 -o, --output を指定すれば良さそうです。

 
そこで以下を入力したところ、Chromiumが起動しました。

$ npx playwright codegen localhost:8000 -o localhost.spec.ts

 
今回は、React Routerによるルーティングの動作確認も兼ねて、

  1. すでにあるタスクをクリック
  2. 戻るボタンで一覧に戻る

という操作をテストコードに落としてみます。

 
まず、すでにあるタスクをクリックします。

 
続いて、戻るボタンをクリックします。

 
以上で操作が終わったたため、ブラウザを閉じます。

すると、 e2e ディレクトリの中に localhost.spec.ts ファイルが生成されていました。

import { test, expect } from '@playwright/test';

test('test', async ({ page }) => {
  await page.goto('http://localhost:8000/');
  await page.goto('http://localhost:8000/tasks');
  await page.getByRole('link', { name: '123' }).click();
  await page.getByRole('link', { name: 'Back' }).click();
});

 
次に、このままでテストコードとして使えるのか確認してみます。

自動生成されたテストコードをVS Codeにて実行したところパスしました。SPAなアプリでもPlaywrightは問題なく動くようです。

 

処理に時間がかかるReactアプリ

昔、Seleniumでテストコードを書いていた時に悩んだのが「時間のかかる処理が含まれる場合、テストコードでどのように待機させるか」でした。

Playwrightの場合はどうだろうと思い調べたところ、自動でいい感じに待機してくれそうでした。
Auto-waiting | Playwright

 
そこで、先程試していたReactアプリに対し、「時間がかかるような処理を追加することで、自動生成されるテストコードがどのように変化するか」を試してみます。

 
まずはReactアプリで処理に時間がかかるよう修正を加えます。

今回は、単純に5秒くらいsleepするコードを追加しました。

export const Index = () => {
  const [tasks, setTasks] = useState<Task[]>([])

  const fetchTasks = async() => {
    const apiUrl = 'http://localhost:8000/api/tasks/'
    const response: AxiosResponse<ApiGetResponse> = await axios.get(apiUrl)

    // ここを追加し、擬似的に遅くしてみた
    await new Promise(resolve => setTimeout(resolve, 5000))

    setTasks(response.data)
  }

 
この状態で React + Django を起動し、テストコードを生成してみます。

$ npx playwright codegen localhost:8000 -o localhost_sleep.spec.ts

 
ただ、生成されたテストコードは、sleepなしのものと同じ内容でした。

生成されたテストコードを実行してみたところ、sleepしているところでテストが待ちに入りました。

 
5秒ほど待つとテストの実行が再開し、テストがパスしました。

VS Code上でも、sleepしている箇所は5秒ほどかかっていることが分かります。

 
これにより、Playwrightでは処理に時間がかかる場合は自動で待機してくれそうでした。

 

Django管理サイト

続いて、認証が必要となるDjango管理サイト向けのテストコードを生成してみます。

なお、認証で使った情報については、 --save-storage オプションを使うことでローカル保存されるようです。
Preserve authenticated state | Test Generator | Playwright

 
まずは

  1. Django管理サイトでログイン
  2. Taskモデルの詳細を開く

という操作を元にしたテストコードを生成してみます。

なお、後でログイン情報を再利用できるよう、 --save-storage オプションで認証情報をファイルに保存してみます。

$ npx playwright codegen localhost:8000/admin/ -o localhost_django_admin.spec.ts --save-storage secret.json

 
操作後、以下のテストコードが生成されていました。

import { test, expect } from '@playwright/test';

test('test', async ({ page }) => {
  await page.goto('http://localhost:8000/admin/login/?next=/admin/');
  await page.getByLabel('ユーザー名:').click();
  await page.getByLabel('ユーザー名:').fill('admin');
  await page.getByLabel('ユーザー名:').press('Tab');
  await page.getByLabel('パスワード:').fill('pass');
  await page.getByRole('button', { name: 'ログイン' }).click();
  await page.getByRole('link', { name: 'Tasks' }).click();
  await page.getByRole('link', { name: '123' }).click();
});

 
また、同一ディレクトリに secret.json ファイルもできていました。

中身を確認するとCookie情報が出力されていました。

{
  "cookies": [
    {
      "name": "csrftoken",
      "value": "HWgAoaHgFP2vqyVzU0nVE8lHgOAEhnD0",
      "domain": "localhost",
      "path": "/",
      "expires": 1704112164.422583,
      "httpOnly": false,
      "secure": false,
      "sameSite": "Lax"
    },
    {
      "name": "sessionid",
      "value": "z8nhd1j1s56jn0af84g5b8uskosy0gj0",
      "domain": "localhost",
      "path": "/",
      "expires": 1673872159.310954,
      "httpOnly": true,
      "secure": false,
      "sameSite": "Lax"
    }
  ],
  "origins": []
}

 
生成されたテストコードを実行したところ、問題なくパスしました。

 

ファイルから認証情報を読み込み、Django管理サイトの認証をスキップする

先ほど secret.json として保存した認証情報は、 --load-storage オプションにより読み込むことができそうでした。

そこで、認証情報を読み込んだ上で、

  1. Task一覧ページを開く
  2. Taskにチェックを入れ、ドロップダウンから「削除」を選択して実行
  3. 削除はキャンセルする

という操作をしてみました。

$ npx playwright codegen localhost:8000/admin/ -o localhost_django_admin_with_load_storage.spec.ts --load-storage secret.json

 
コマンドを実行したところ、ブラウザが既にログインした状態で開きました。

 
操作を終えてブラウザを閉じると、以下のテストコードが生成されていました。

先ほどのテストコードとは異なり、ログインする処理のコードが見当たりません。一方、 test.use に認証情報ファイルが指定されていました。

import { test, expect } from '@playwright/test';

test.use({
  storageState: 'secret.json'
});

test('test', async ({ page }) => {
  await page.goto('http://localhost:8000/admin/');
  await page.getByRole('link', { name: 'Tasks' }).click();
  await page.getByText('変更する task を選択 task を追加 操作: --------- 選択された tasks の削除 実行 1個の内ひとつも選択されていません 内容 Upd').click();
  await page.locator('input[name="_selected_action"]').check();
  await page.getByRole('combobox', { name: '操作:' }).selectOption('delete_selected');
  await page.getByRole('button', { name: '実行' }).click();
  await page.getByRole('link', { name: '戻る' }).click();
});

 
このテストコードを実行したところ、テストが落ちました。認証情報ファイルが見当たらないようです。

 
調べてみたところ、 path.join() が必要そうでした。
typescript - ENOENT: no such file or directory, when attempting to upload a file with playwright - Stack Overflow

そこで、自動生成されたテストコードを修正します。

import { test, expect } from '@playwright/test';
import path from 'path';  // 追加

test.use({
  // 差し替え
  // storageState: 'secret.json'
  storageState: path.join(__dirname, 'secret.json')
});

// 以降は同じ

 
この状態で再度テストを実行したところ、パスしました。

 

おわりに

今回、気になるパターンをいくつか試してみて、ある程度は自動生成したものがそのまま使えそうだと分かりました。

 

ソースコード

Githubに上げました。

アプリ側のソースコードはこちら。

 
Playwright側のソースコードはこちら。