概要
プロジェクトでPlaywrightを用いてE2Eテストを実装しています。
今回のプロジェクトでは認証に外部のアプリケーションを用いています。
外部の認証アプリで描画されるUIの結果が冪等でない場合に行なったE2Eテストのテストコードの実装の工夫について紹介します。
実際のケース
今回携わったプロジェクトでは、MSALを使い、Microsoftのアカウントで認証を行う実装となっていました。
※MSAL(Microsoft Authentication Library)
MSALの認証では、通常であれば認証開始後、まずはメールアドレスの入力を求める画面が表示され、その後パスワードの入力を求める画面に遷移します。
しかし、パスワードを求める画面ではなく、登録されているメールアドレスへ認証コードを送信し、送られてきた認証コードの入力を求める画面に遷移する場合もある、ということが発覚しました。この条件はランダムで規則性はなさそうであり、MSALの内部での処理分岐のようでした。
認証コード送信画面には「Use your password」というボタンが設置してあり、このボタンをクリックするとPWの入力を求める画面に遷移します。
そのため、Playwrightで作成するシナリオとしては、
- 「Use your password」が表示されればクリックを行い、PWの入力を求める画面に遷移する。
- 「Use your password」が表示されなれければ何もしない。
のどちらか合致する方を選択すれば良い、ということになります。
実装のバッドケース
上記の要件を満たすため、まずは下記のような実装を行いました。
const usePwdBtn = page.getByRole("button", { name: /Use your password/i });
try {
await usePwdBtn.click({ timeout: 2000 });
} catch {
// パスワード入力画面へ遷移するボタンが存在しない場合は何もしない
}
ところが、上記の実装だと、実際はusePwdBtnが描画されているにも関わらず、timeout: 2000 を過ぎて描画されていないと判定され、テストがタイムアウトでエラーとなるケースがあ離ました。
表示されているのに2 秒で usePwdBtn.click() がタイムアウトする理由は、Playwright の「アクショナビリティチェック」を満たせていないからです。
click は「存在している」だけでなく、以下が 同時に 成立するまで待ちます。(成立しないまま 2 秒経つと timeoutする):
- attached: DOM にアタッチ済み
- visible: 可視(十分な視認領域がある)
- stable: レイアウトやアニメーションで動いていない
- enabled: 無効化されていない
- receives pointer events: 上にオーバーレイ等が被っていない(実クリックが当たる)
MSALのログイン UI は遷移直後にアニメーションやオーバーレイ、再描画が起きがちで、「人間の目には見えているが、Playwright 的にはまだ ‘actionable’ でない」瞬間がよくあります。
2 秒だと CI やネットワーク/フォント読み込みが入る環境だと足りないケースも存在します。
上記を解決するためにタイムアウトを伸ばすと、テスト全体の実行時間が伸びてしまうという別の問題も発生してしまいます。
また、根本的な解決になっておらず、テストはFlakyになってしまいます。
修正した実装
await Promise.any([
// A) 既にパスワード画面になっている(ボタンは出ない)ケース
(async () => {
await passwordInput.waitFor({ state: "visible" });
return "password-visible";
})(),
// B) 「Use your password」ボタンが出るケース(出たら押して→パスワード欄が出るまで待つ)
(async () => {
await usePwdBtn.waitFor({ state: "visible" });
await usePwdBtn.click();
await passwordInput.waitFor({ state: "visible" });
return "clicked-then-password-visible";
})(),
]).catch(() => {
throw new Error('Neither the "Use your password" button nor the password field appeared in time.');
});
// ここに来た時点で必ず passwordInput は見える想定
await passwordInput.fill(pass);
await page.click('button[type="submit"]');
上記のように、Promiss.any を使ってA/B2つのシナリオを同時に走らせ、どちらかが成功したら続行するようにしました。
上記のような実装とすることで、「実際は描画されているにも関わらず、描画されていないと判定されてしまう」という問題を解決しつつ、条件分岐があるシナリオの場合でもテストを勧められることが可能になりました。
まとめ
E2Eテストを行なっている環境で描画される結果に冪等性がない場合、テストがFlakyになってしまうので、このような分岐方法が誰かの役に立てれば嬉しいです。