Playwright で Azure AD B2C 認証ありの UI テストをさっくり実行する

# はじめに
Playwright は後発の UI テストフレームワークとして、機能的にとっても好きなんですが、とか言いつつ暫く見ていなかったら色々充実していたので、AAD 認証が必要な UI テストをちゃちゃっと作成＆実行する手順をまとめました。
基本的には[公式ドキュメント](https://playwright.dev/docs/intro)が充実しているので、適宜そちらを参照くださいまし。

ポイントは AAD 認証(AAD B2C認証) だと認証時にURLが遷移していくので、そこを加味したコードにしておかないと認証状態がきちんととれない、っていうところです。

# 環境準備
VS Code の [Playwright Extension](https://playwright.dev/docs/getting-started-vscode) がかなり充実してきていますが、とりあえず CUI で進めます。Node とかは最新が入っている前提で、playwright の構成を作ります。
どこかに適当なフォルダを作って、そのフォルダ内で以下を実行します。
基本的には[公式のインストール手順](https://playwright.dev/docs/intro#installing-playwright)のままです。

```cmd
npm init playwright@latest
```
TypeScript か JavaScript か、とかテストフォルダの名前とか GitHub Actionsのテンプレートをつくるかとか聞かれますが、基本全部デフォルトでOK。（以下の画像では GitHub Actions だけデフォルトから変えて、yを選択してテンプレートを生成してます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/977156/6dc8497f-9b8c-6bee-ec51-ee0fb6190055.png)

そうするとこんなファイル構成が出来ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/977156/f13b7c1b-78bd-76f1-f9b4-d213abb8f597.png)

playwright.config.ts が playwright の設定ファイルとなっていて、内容をみるとデフォルトで色々入っています。とりあえずのポイントは tests フォルダが UI テストファイルを置く場所に指定されているところですかね。

ひとまずこれにて準備は完了です。

# AAD B2C 認証する UI テストコードを生成する
認証が入っているサイトの URL をひかえて、以下を実行します

```bash
npx playwright codegen https://認証が必要なターゲットURL
```

そうすると、例の認証画面と、コードテキストの2つのウィンドウが立ち上がります。
普段通りにユーザ名とパスワードとかをいれて Signin してください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/977156/f7451e14-c8bd-9b31-546f-0af4c3f50f90.png)

サインインが終わったらコードウィンドウの方の左上にある Record をクリックして、生成をストップします。（これしないとコピペができないです）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/977156/837a8e3e-9bc4-ad76-ce66-a0d552925c26.png)

こんなコードが出ているかなと思います
```ts
import { test, expect } from '@playwright/test';

test('test', async ({ page }) => {
  await page.goto('https://なんかAAD B2C の認証用URL（長い）');
  await page.getByPlaceholder('Email Address').fill('******ログインできるメールアドレス******');
  await page.getByPlaceholder('Email Address').press('Tab');
  await page.getByPlaceholder('Password').fill('******ログインできるパスワード******');
  await page.getByRole('button', { name: 'Sign in' }).click();
  await page.goto('https://ターゲットURL');
});
```
このコードをとりあえずどこかに確保しておいてください。

# AAD B2C 認証できる UI テストコードを共通化する
色々と認証を UI テストに入れるパターンはあるのですが、認証された状態を再利用する方法を、[公式ドキュメントの reuse 認証パート](https://playwright.dev/docs/auth#reuse-signed-in-state)に従って準備していきます。
※テストケースごとに違う認証状態を使うとか、色々シナリオはありますが、今回はすべてのテスト共通で認証状態を前提として使う構成をします。

###  1. global-setup.ts を準備
ルートフォルダに global-setup.ts というファイルをつくります。今回は前のパートでつくったコードを入れ込んでいきます。
ポイントとしては、特に赤字のところでしょうか
 - 長いAAD B2Cの認証は、ターゲットURL に変える
 - <font color="red">AAD 認証はリダイレクトとか入ってしまうので、page.waitForURLで、ターゲットURLに遷移するまで待つ</font>
 - page.context().storageState()をつかって、認証状態を json ファイルに出力する


```ts
import { chromium, FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  // 前のパートでは B2C の長いログインURLだったのですが、ここはターゲットURLに変えます。
  await page.goto('https://ターゲットURL');
  await page.getByPlaceholder('Email Address').fill('******ログインできるメールアドレス******');
  await page.getByPlaceholder('Email Address').press('Tab');
  await page.getByPlaceholder('Password').fill('******ログインできるパスワード******');
  await page.getByRole('button', { name: 'Sign in' }).click();

  //前のパートでは gotoでしたが、ここではターゲットURLまでwaitForするように変えます
  await page.waitForURL('https://ターゲットURL');

  // Save signed-in state to 'storageState.json'.
  await page.context().storageState({ path: 'storageState.json' });
  await browser.close();
}

export default globalSetup;
```
### 2. playwright.config.ts に global-setup を入れ込む
playwright.config.ts の一番上に global-setup を宣言します
```ts
const config: PlaywrightTestConfig = {
  globalSetup: require.resolve('./global-setup'),
```
その上で、use 内の storageState として、生成した storageState.json を指定します
```ts
  use: {
    storageState: 'storageState.json',
```
これでテストを実行する前にログインされてその状態を維持したままテストが可能です。

### 3. UI テストケースをつくる
testsフォルダ内に、tekito.spec.ts を作成して AAD B2C 認証付きのターゲットURLに遷移して、ターゲットURLになっていることを確認するテストを書きます。
なんだこれっていうテストですが、認証が突破できることを確認します

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

test('認証付きのサイトにアクセスして、そのURLを確認する', async ({ page }) => {
  await page.goto('https://認証が必要なターゲットURL');

  await expect(page).toHaveURL('https://認証が必要なターゲットURL');
});
```

テンプレートで作成された example.spec.ts がある場合は、6個のテストケースが Pass します。
なお、テスト実施時にルートに storageState.json が作成されてることも確認できるかなと

うまく成功したら、レポートを表示してみます。
```bash
npx playwright show-report
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/977156/c8123c49-1afb-4447-f8fc-998c3ae7ac7e.png)

認証操作を共通化して、テストケースではそれを加味せずに、うまく認証を突破していることが確認できました！

なお、今回認証は chromium でやっていて、実際のテストは chromium, firefox, webkit でやっているので、もし認証状態がブラウザ依存するようなものであれば、それぞれのブラウザで認証状態を別名で保存した上でそれぞれのブラウザテストで利用する必要がありそうですね。

### おまけ.テストケースを拡充する

今回テストを実施したことでルートフォルダに認証状態を保存した storageState.json を使ってテストコードを追加してみます。

```bash
npx playwright codegen --load-storage=storageState.json https://認証が必要なターゲットURL
```
を実行すると先ほど確保された認証状態をつかって、認証要求なしにサイトが開きますので、テストに必要な遷移をしたりしてテストコードを tekito.spec.ts に追加します。
なお、生成したコードに test.use で storageState.json を指定されたりしますが、今回の手順であれば global-setup に書いてあるので、その部分は不要です。

```ts
test('追加したテストケース', async ({ page }) => {
    await page.goto('https://認証が必要なターゲットURL');

    //レコーダーで記録した遷移もろもろ

    //非推奨ですが、とりあえず Xpath で検証。できれば Role Locator か TestId で。
    await expect(page.locator('xpath=/html/body/div[3]/div/div/h3')).toHaveText('Hello, XXXX');
});
```

再度テスト実行します(今回はテストファイルを指定して実行）

```bash
npx playwright test tekito.spec.ts 
```

成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/977156/687cf03c-d4a9-eca2-5a51-d29e7841621d.png)

# おわりに
Playwright とっても強力なツールで、UI テストを、テストフレームワークとしても記録ツールとしても使えますね。認証系の状態も共有してテストができるので、認証あるからなぁっていうシナリオもしっかりカバーできそうです。

なお、上記では XPath で要素の検証をしていますが、公式にも言及のある通りお勧めされない方法です。壊れやすいので。[Role locator](https://playwright.dev/docs/locators#locate-by-role)や[Test ID](https://playwright.dev/docs/locators#locate-by-test-id)を用いた方法で検証しておくようにきちんと書いておくとメンテナンス性も高くなりますので、実際に使う際にはご検討くださいまし。