📌

Playwright(Java)を最低限使えるようになる記事

に公開

環境セットアップ

まず、Playwrightを使用するためにMavenまたはGradleに依存関係を追加します。
(JUnitも使います)

Maven(pom.xml)の場合:

<dependency>
    <groupId>com.microsoft.playwright</groupId>
    <artifactId>playwright</artifactId>
    <version>1.52.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

Gradle(build.gradle)の場合:

dependencies {
    testImplementation 'com.microsoft.playwright:playwright:1.52.0'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

バージョンは状況に応じて適宜変更してください。

Playwrightの基本操作

Locator(要素の特定)

Playwrightでは、HTML要素を特定するためにLocatorを使用します。主な特定方法は以下のとおりです:

// ID属性で特定
page.locator("#username")

// クラス名で特定  
page.locator(".submit-button")

// テキスト内容で特定
page.locator("text=ログイン")

// XPathで特定
page.locator("//input[@name='password']")

// CSS Selectorで特定
page.locator("button:has-text('送信'):enabled")

// data-testid属性で特定(推奨)
page.locator("[data-testid='login-form']")

基本的な操作メソッド

入力操作:

// テキスト入力
page.locator("#username").fill("testuser");

// クリック
page.locator("#login-button").click();

// チェックボックスの操作
page.locator("#agreement").check();

// セレクトボックスの選択
page.locator("#category").selectOption("electronics");

待機処理:

// 要素が表示されるまで待機
page.waitForSelector("#result-table");

// URLの変化を待機
page.waitForURL("**/dashboard");

// 特定の条件を待機
page.waitForFunction("() => document.title.includes('完了')");

検証操作:

// 要素の表示状態確認
boolean isVisible = page.locator("#error-message").isVisible();

// 要素の活性状態確認
boolean isEnabled = page.locator("#submit-button").isEnabled();

// テキスト内容の取得
String titleText = page.locator("h1").textContent();

Page Object Model(POM)パターン

Page Object Modelは、Webページの構造をオブジェクト指向的に表現するデザインパターンです。必ずしも従う必要は無いですが、キレイで見やすいので覚えておいて損は無いです。

Page Object Modelの構成要素

1. Locatorsクラス(要素の定義)

各ページの要素を定数として定義します:

public class ProductListPageLocators {
    // ページ要素
    public static final String PAGE_TITLE = "h1:has-text('商品一覧')";
    public static final String HELP_TEXT = "text=商品について";
    
    // ボタン要素
    public static final String CREATE_NEW_BUTTON_ENABLED = "button:has-text('新規作成'):enabled";
    public static final String CREATE_NEW_BUTTON_DISABLED = "button:has-text('新規作成')[disabled]";
    public static final String FILTER_BUTTON = "button:has-text('絞り込み')";
    
    // テーブル要素
    public static final String TABLE_ROWS = ".table-row";
    public static final String FIRST_TABLE_ROW = ".table-row:nth-child(1)";
    
    // メッセージ要素
    public static final String NO_DATA_MESSAGE = "text=該当する商品がありません。";
}

2. BasePage(基底ページクラス)

全ページ共通の機能を定義します:

public class BasePage {
    protected Page page;

    public BasePage(Page page) {
        this.page = page;
    }

    /**
     * 指定されたURLパスへの遷移を待機
     */
    public boolean waitForUrlToContain(String expectedUrlPath, int timeoutMs) {
        try {
            page.waitForFunction(
                "() => window.location.pathname.includes('" + expectedUrlPath + "')",
                new Page.WaitForFunctionOptions().setTimeout(timeoutMs)
            );
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

3. 具体的なPageクラス

各ページの操作を定義します:

public class ProductListPage extends BasePage {

    public ProductListPage(Page page) {
        super(page);
    }

    /**
     * 商品一覧ページに遷移
     */
    public void navigate() {
        page.navigate("http://localhost:3010/products");
    }

    /**
     * ページタイトルが表示されているかを確認
     */
    public boolean isPageTitleVisible() {
        return page.locator(ProductListPageLocators.PAGE_TITLE).isVisible();
    }

    /**
     * 新規作成ボタンをクリック
     */
    public void clickCreateNewButton() throws InterruptedException {
        Thread.sleep(1000); // 要素の安定化を待機
        page.locator(ProductListPageLocators.CREATE_NEW_BUTTON_ENABLED).click();
    }

    /**
     * テーブルにデータが表示されているかを確認
     */
    public boolean isTableDataVisible() {
        return page.locator(ProductListPageLocators.TABLE_ROWS).count() > 0;
    }

    /**
     * データなしメッセージが表示されているかを確認
     */
    public boolean isNoDataMessageVisible() {
        return page.locator(ProductListPageLocators.NO_DATA_MESSAGE).isVisible();
    }

    /**
     * ページの読み込み完了を待機
     */
    public void waitForPageLoad() throws InterruptedException {
        page.waitForSelector(ProductListPageLocators.PAGE_TITLE);
    }
}

Page Object Modelを活用したログインページの例

public class LoginPageLocators {
    public static final String USERNAME_INPUT = "#username";
    public static final String PASSWORD_INPUT = "#password";
    public static final String LOGIN_BUTTON = "button:has-text('ログイン')";
    public static final String ERROR_MESSAGE = ".error-message";
}

public class LoginPage extends BasePage {
    
    public LoginPage(Page page) {
        super(page);
    }

    public void inputUsername(String username) throws InterruptedException {
        Thread.sleep(1000);
        page.locator(LoginPageLocators.USERNAME_INPUT).fill(username);
    }

    public void inputPassword(String password) throws InterruptedException {
        Thread.sleep(1000);
        page.locator(LoginPageLocators.PASSWORD_INPUT).fill(password);
    }

    public void clickLoginButton() throws InterruptedException {
        Thread.sleep(1000);
        page.locator(LoginPageLocators.LOGIN_BUTTON).click();
    }

    public boolean isErrorMessageVisible() {
        return page.locator(LoginPageLocators.ERROR_MESSAGE).isVisible();
    }
}

テストの書き方例

テスト基底クラスの作成

すべてのテストで共通するセットアップ処理を基底クラスに定義します:

public class BaseTest {

    protected Playwright playwright;
    protected Browser browser;
    protected Page page;
    protected BrowserContext context;
    private static final boolean HEADLESS = true;

    @BeforeEach
    public void setUp() {
        playwright = Playwright.create();
        browser = playwright.chromium().launch(
            new BrowserType.LaunchOptions().setHeadless(HEADLESS)
        );
        context = browser.newContext();
        context.tracing().start(
            new Tracing.StartOptions().setScreenshots(true).setSnapshots(true)
        );
        page = context.newPage();
        page.navigate("http://localhost:3000/");
    }

    @AfterEach
    public void tearDown() throws InterruptedException {
        Thread.sleep(1000);
        context.tracing().stop(new Tracing.StopOptions().setPath(Paths.get("trace.zip")));
        page.close();
        context.close();
        browser.close();
        playwright.close();
    }
}

このコードのポイント:

  • playwright.chromium().launch() でブラウザを起動
  • setHeadless(true) でヘッドレスモード(ブラウザウィンドウなし)で実行
  • context.tracing() でデバッグ用のトレース機能を有効化
  • page.navigate() でテスト対象のURLに遷移

テスト例

public class ProductTest extends BaseTest {

    @Nested
    @DisplayName("商品一覧表示のテスト")
    public class ProductListDisplayTest {

        @Test
        @DisplayName("正常系_商品一覧表示_データ有り")
        void testProductListWithData() throws InterruptedException {
            // ログイン処理
            LoginPage loginPage = new LoginPage(page);
            loginPage.inputUsername("testuser");
            loginPage.inputPassword("password");
            loginPage.clickLoginButton();

            // 商品一覧ページに遷移
            ProductListPage productListPage = new ProductListPage(page);
            productListPage.navigate();
            productListPage.waitForPageLoad();

            // 期待結果の確認
            assertTrue(productListPage.isPageTitleVisible());
            assertTrue(productListPage.isTableDataVisible());
        }

        @Test
        @DisplayName("正常系_商品一覧表示_データ無し")
        void testProductListWithoutData() throws InterruptedException {
            // ログイン処理
            LoginPage loginPage = new LoginPage(page);
            loginPage.inputUsername("testuser");
            loginPage.inputPassword("password");
            loginPage.clickLoginButton();

            // 商品一覧ページに遷移
            ProductListPage productListPage = new ProductListPage(page);
            productListPage.navigate();
            productListPage.waitForPageLoad();

            // 期待結果の確認
            assertTrue(productListPage.isPageTitleVisible());
            assertTrue(productListPage.isNoDataMessageVisible());
        }
    }
}

トレースとデバッグ

トレース機能を活用すると、テストの実行過程を詳細に分析できます:

@BeforeEach
public void setUp() {
    playwright = Playwright.create();
    browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false));
    context = browser.newContext();
    
    // トレース開始
    context.tracing().start(new Tracing.StartOptions()
        .setScreenshots(true)
        .setSnapshots(true)
        .setSources(true)
    );
    
    page = context.newPage();
}

@AfterEach
public void tearDown() {
    // トレース保存
    context.tracing().stop(new Tracing.StopOptions().setPath(Paths.get("trace.zip")));
    // 後のクリーンアップ処理...
}

生成されたtrace.zipファイルは、Playwright公式のビューワーで開いて詳細な分析が可能です。
ビューワー:https://trace.playwright.dev/

余談

PlaywrightとSpring Test DBUnitを組み合わせて、CSV形式でデータを準備し、ケース毎に切り替えてテストを行う手法を紹介しています。ぜひ読んでみてください
https://zenn.dev/izumi_ren/articles/07d6906e23f6cd

関連記事

aShot で簡単にVRTを行う:https://zenn.dev/izumi_ren/articles/f5e166a9b3e7df

Discussion