Open4

Playwright で Firebase Authentication の認証情報を再利用して複数のテストを実行したい

shiratorishiratori

全てのテストで毎回ログイン画面のテストを実行するのは無駄なので、最初に一度ログインしたらそれ以降のテストではその認証状態を再利用してログイン画面のテストは端折りたい。

しかしPlaywrightのドキュメントには Firebase Authentication のユースケースは載っていない。対応しているのはクッキー、ローカルストレージを使った再利用方法のみ。

Firebase Authentication は IndexedDB に認証情報が保存される。

IndexedDB に対応してほしいというイシューは上がっているが対応されていない。

https://github.com/microsoft/playwright/issues/11164

shiratorishiratori

イシュー内のこちらのコメントが参考になった。

https://github.com/microsoft/playwright/issues/11164#issuecomment-1789950254

上記イシューを参考にやってみたやつのメモ

ログイン画面のテストコードを用意

最初のセットアップ時に一度ログインする必要があるのでそのための関数

e2e-tests/loginTest.ts
import type { Page } from "@playwright/test";

/** ログインのテスト */
export const loginTest = async (page: Page) => {

  // ログイン画面に遷移
  await page.goto('ログイン画面のパス')

  // フォームに入力してログインを実行
  await page.locator('input[name="email"]').fill('test')
  await page.locator('input[name="password"]').fill('test')
  await page.getByRole('button', { name: '確定' }).click()

  // 認証後のリダイレクトを待機
  await page.waitForURL(`/`);
};

IndexedDBで使う値を定数化しておく

e2e-tests/const.ts
/** ロカールストレージの内容を書き出すファイル */
export const AUTH_JSON_FILE = "playwright/.auth/user.json";
/** FirebaseのインデックスDB名 */
export const INDEX_DB_NAME = "firebaseLocalStorageDb";
/** FirebaseのインデックスDBのオブジェクトストア名 */
export const OBJECT_STORE_NAME = "firebaseLocalStorage";

セットアップファイルを用意

ブラウザを起動してログイン処理を実行したら、IndexedDBの内容をローカルストレージにコピーする。page.context().storageState()でjsonファイルに出力する。テスト実行時にこれを流用する。

e2e-tests/auth.setup.ts
import { test as setup } from "@playwright/test";
import { loginTest } from "./";
import { AUTH_JSON_FILE, INDEX_DB_NAME, OBJECT_STORE_NAME } from "./const";

/**
 * Firebaseのログイン実行と認証情報をlocalStorageとファイルに保存する
 */
setup("Firebase Authentication", async ({ page }) => {
  /** ログイン処理を実行する */
  await loginTest(page);

  /** IndexedDBの内容をローカルストレージにコピーする */
  await page.evaluate(
    async ([indexDBName, indexDBObjectStore]) => {
      const saveIndexDBtoStorage = () => {
        return new Promise<boolean>((resolve, reject) => {
          const indexDB = window.indexedDB;
          const openRequest = indexDB.open(indexDBName);

          openRequest.onsuccess = () => {
            const db = openRequest.result;
            const transaction = db.transaction([indexDBObjectStore], "readonly");
            const objectStore = transaction.objectStore(indexDBObjectStore);

            const getAllKeysRequest = objectStore.getAllKeys();
            const getAllValuesRequest = objectStore.getAll();

            getAllKeysRequest.onsuccess = () => {
              const keys = getAllKeysRequest.result;

              getAllValuesRequest.onsuccess = () => {
                const values = getAllValuesRequest.result;
                for (let i = 0; i < keys.length; i++) {
                  const key = keys[i];
                  const value = values[i];
                  localStorage.setItem(String(key), JSON.stringify(value));
                }
                resolve(true);
              };
            };
          };

          openRequest.onerror = () => {
            console.error("Error opening IndexedDB database:", openRequest.error);
            reject();
          };
        });
      };

      await saveIndexDBtoStorage();
    },
    [INDEX_DB_NAME, OBJECT_STORE_NAME]
  );

  /** ローカルストレージに保存した内容をファイルに保存 */
  await page.context().storageState({ path: AUTH_JSON_FILE });
});

このときawait loginTest(page);で先にブラウザを起動しないと以下のエラーになるので注意。

Failed to execute 'transaction' on 'IDBDatabase': One of the specified object stores was not found.
DOMException: Failed to execute 'transaction' on 'IDBDatabase': One of the specified object stores was not found.
    at openRequest.onsuccess (<anonymous>:6:30)

復元用のコードを用意

setupで保存しておいたjsonファイルからブラウザのIndexedDBに認証情報を復元するためのコードを用意する。イシューのサンプルではpage.evaluateを使っているが, page.addInitScriptを使うようにしした。page.addInitScript はページの読み込み前にスクリプトを注入して実行できる。そのため page.evaluateよりも早いタイミングで処理できる。(参考: Injecting code | Academy | Apify Documentation

e2e-tests/restoreFirebaseIndexedDB.ts
import * as fs from "fs";
import type { Page } from "@playwright/test";
import { AUTH_JSON_FILE, INDEX_DB_NAME, LOGIN_PATH, OBJECT_STORE_NAME } from "./const";

/** Firebase認証情報をファイルからインデックスDBに復元する */
export const restoreFirebaseIndexedDB = async (page: Page) => {
  /** e2e-tests/auth.setup.ts で保存しておいたファイルを読み込む */
  const authJson = JSON.parse(fs.readFileSync(AUTH_JSON_FILE, "utf8"));

  /**
   * ログイン画面に遷移
   * 先にブラウザを起動しないと後続の処理でIDBDatabaseが参照できない
   */
  await page.goto(LOGIN_PATH);

  /**
   * jsonファイルから取得したデータをインデックスDBにセットする
   * evaluate ではなく addInitScript を使うことでナビゲート後すぐに(全てのスクリプトの実行前に)実行できるので、画面のレンダリングを待たない分高速
   */
  await page.addInitScript(
    ([indexDBName, indexDBObjectStore, authJson]) => {
      const indexedDB = window.indexedDB;
      const openRequest = indexedDB.open(indexDBName);
      openRequest.onsuccess = () => {
        const db = openRequest.result;
        const transaction = db.transaction([indexDBObjectStore], "readwrite");
        const objectStore = transaction.objectStore(indexDBObjectStore);

        const localStorage = authJson.origins[0].localStorage;
        for (const element of localStorage) {
          const value = element.value;
          objectStore.put(JSON.parse(value));
        }
      };
      openRequest.onerror = (error) => {
        console.log("Restore Firebase IndexDB Error: ", error);
      };
    },
    [INDEX_DB_NAME, OBJECT_STORE_NAME, authJson]
  );
};

コンフィグでsetupファイルへの依存関係を設定する

これでplaywrightでテストコマンドを実行したら先にsetupファイルの内容が実行され、jsonファイルが吐き出される。

playwright.config.ts
import path from "path";
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({

  //中略

  projects: [
    {
      name: "setup",
      testMatch: /.*\.setup\.ts/,
    },
    {
      name: "Google Chrome",
      use: { ...devices["Desktop Chrome"], channel: "chrome" },
      dependencies: ["setup"],
    },
  ],
 
  //中略

});

テストファイルのbeforeEachで復元処理を実行する

e2e-tests/example.spec.ts
import { test, expect } from "@playwright/test";
import { restoreFirebaseIndexedDB } from "./";

test.describe("", () => {
  test.beforeEach(async ({ page }) => {
  
    /** 認証状態を復元 */
    await restoreFirebaseIndexedDB(page);

  });

});