Playwrightで開いているタブから特定の画面が存在するか確認する方法

概要

今回はPlaywrightで「いま開いているタブの中に特定の画面が存在するか」をbooleanで返すヘルパー関数について書いていきます。

E2Eテストを書いていると、「ボタンを押したら別タブで詳細画面が開く」「リダイレクト後に意図した画面が立ち上がっているかを確認したい」みたいな場面が地味に多いです。

毎回specの中でcontext.pages()を舐めるコードを書くと冗長になりがちなので、「URLかtitleに対象文字列が含まれていればtrue」と返す関数を1つ用意しておくとシナリオがかなりスッキリします。

短い記事ですがサクッと使い回せるので、Playwrightで複数タブを扱う必要がある方はぜひ見ていってください!

それではやっていきましょう(^^!

目次

やりたかったこと

やりたいことを具体化するとこういう構成です。

  • 同一のBrowserContext内で複数タブ(Page)が開いている可能性がある
  • 引数で渡した文字列(screenName)が、いずれかのタブのURLまたはtitleに含まれていればtrueを返したい
  • Page Object Modelのメソッドや、シナリオのassertion前段で使い回せるようにしたい

ようはX画面が立ち上がってるのかを確認するための関数というだけの話です。

実装

実装はこれだけです。

helpers.ts

1
2
3
4
5
6
7
8
9
10
11
12
async checkScreen(page: Page, screenName: string): Promise<boolean> {
const pages = page.context().pages();

for (const p of pages) {
const url = p.url();
const title = await p.title();
if (url.includes(screenName) || title.includes(screenName)) {
return true;
}
}
return false;
}
  • page.context().pages()同じBrowserContext内の全Page(タブ)の配列を取る
  • 各タブのurl()title()を見て、screenNameが含まれていれば即trueを返す
  • 全部回しても見つからなければfalse

シンプルですが、この関数を使用すると1行で「画面の存在のチェック」ができます。

ポイント

page.context().pages()で同コンテキスト内の全タブを取得

ここが今回の肝です。

BrowserContextはCookieやlocalStorage、セッションを共有する単位で、その中で複数のPage(=タブ)を持てます。

context.pages()を呼ぶと、そのコンテキスト内で現在開かれている全ページの配列が返ってきます。

  • page.context() … そのpageが属するBrowserContext
  • page.context().pages() … 同じコンテキスト内の全タブの配列
  • browser.contexts() … さらに上位、別コンテキストまで広げて見るとき用

ふつうのE2Eシナリオで「ログイン後に開いた別タブ」を確認したいときは、同じコンテキスト内に居るはずなのでpage.context().pages()で十分です。

URLとtitleの両方を見る理由

URLだけ、titleだけだと取りこぼすケースがあるので両方見ています。

  • URL: /users/profileのようなパス命名
  • title: <title>プロフィール | アプリ名</title>のような表示文字列

どちらかにscreenNameが含まれていれば拾えるので、ルーティング命名と画面タイトル命名のブレを吸収できます。

profileで引きたいけどtitleには「プロフィール」しか出ない」「逆にtitleが「Profile」でURLが/u/123」みたいな現場あるある対策ですね(^^

for...ofを使う理由

これはちょっとした小話ですが、ループはfor...ofを使うのがポイントです。

1
2
3
4
5
6
7
8
9
10
11
// ❌ これだと await が外側に伝わらない
pages.forEach(async (p) => {
const title = await p.title();
// ...
});

// ✅ for...of なら素直に await できる
for (const p of pages) {
const title = await p.title();
// ...
}

Array.prototype.forEachはasyncコールバックを渡しても外側は待ってくれないので、こういう「逐次でawaitしたい」処理ではfor...ofが無難です。

specでの使い方

実際のspecではこんな感じで使います。「ボタンを押すと別タブでプロフィール画面が開く」ようなフローの検証パターンです。

profile.spec.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { test, expect } from "@playwright/test";
import { checkScreen } from "./helpers";

test("プロフィールリンクをクリックすると別タブでプロフィール画面が開く", async ({ page, context }) => {
await page.goto("/");

// 別タブが開くのを待ちながらリンクをクリック
const [newPage] = await Promise.all([
context.waitForEvent("page"),
page.click("a[data-testid='profile-link']"),
]);
await newPage.waitForLoadState("domcontentloaded");

// ヘルパーで「プロフィール画面が立ち上がっているか」を確認
expect(await checkScreen(page, "profile")).toBe(true);
});
  • context.waitForEvent("page")で新規タブが開くのを待ってからclickを発火
  • Promise.allで「待ち受け」と「クリック」を同時にスタートしないと、クリック後に発火するイベントを取りこぼす
  • 開いたあとにwaitForLoadState("domcontentloaded")DOM構築完了まで待ってからcheckScreenを呼ぶことで安定して実行できるようにしてます(^^

checkScreen引数のpageが属するcontextを見にいくだけなので、newPageではなく元のpageを渡してもOKです(同じcontext内なので結果は同じ)。

まとめ

いかがでしたか?
今回はPlaywrightで「開いているタブから特定の画面が存在するか」を確認するヘルパー関数を整理してみました!

今回紹介した点をまとめると以下の通り(^^

  • page.context().pages()で同BrowserContext内の全タブを取得できる
  • ・URLとtitleの両方をチェックすることで命名のブレを吸収
  • ・ループはfor...ofで書けばawaitがそのまま直列で利用可能

E2Eで複数タブを扱うシナリオは増えがちなので、こういう小さなヘルパーを1つ持っておくとシナリオがかなり読みやすくなります。

自分も仕事でも個人開発でもPlaywrightを回している民なので、こういう細かいユーティリティはどんどん記事として残していきたいですね!

ちなみに自分がPlaywrightでE2Eを組んでいる「こぴぺったり」は、コピー&ペーストを簡単に行えるツールになっております。
コピーしたいデータを登録し、登録したデータを押すと文字列がコピーされる仕組みです!
メールアドレスやメールの定型文、PGのよくある書き方集など使い道は様々、ディレクトリ階層がアプリ内にあり、分けやすさも兼ね備えております!

そしてなんと、「完全無料」「登録不要」「コピペデータのデータ送信なし」というセキュリティ的にも安心の一品です。
copype.shinpinoshi.comこぴぺったり
ここから使用できるので、是非使用してみてください!

以上となります。
こういった細かい関数をナレッジとして残しておくとあとで誰かから感謝されたりします!
それでは、お疲れさまでした(^^b