1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PlaywrightのPOM化 & CSVデータ管理をAIと最適に会話する試み

Last updated at Posted at 2025-05-19

目的
Playwrightを活用した Page Object Model (POM) 化 と CSVデータ取り込みによるテスト管理 について、AIとどのように会話すれば 最適な回答を得られるか試みる。
AIと最適に会話するためのポイント

☆具体的な質問をする

AIへの質問は 明確で具体的に すると、より正確な回答を得られます。
:relaxed:良い例:

Playwrightで POM を活用したログインテストを設計するにはどうすればいいですか?

:sweat_smile:避けるべき例:

PlaywrightでPOMを作りたい。助けて!

☆詳細な要件を伝えることで、コード例や設計戦略を得やすい!

エラーメッセージや環境情報を共有
⇒トラブルシューティングの際は、エラーメッセージの全文と環境情報を含める と、AIは正確な原因を特定し、適切な解決策を提案しやすくなります。
:relaxed:良い例:

CSVデータの actionTargetTwilio Voice JavaScript SDK v2 を設定すると、
以下のエラーが出ます。
Error: locator.waitFor: Test timeout of 120000ms exceeded
Playwrightのバージョン: 1.38.0
Node.jsのバージョン: 18.17.0

:sweat_smile:避けるべき例:

CSVデータを使っているけどリンクがクリックできない。なんで?

※注)エラーメッセージが具体的であるほど、AIの回答も最適化される。

☆ステップごとに会話する

一度に多くの質問をすると、AIの回答が曖昧になる ことがあります。
そのため、テーマを分割して段階的に会話するのがベスト です。
:relaxed:良い会話例:

  • POMの設計について聞く
    PlaywrightでログインページをPOM化する方法を教えてください。
  • セッション管理について聞く
    POM化したログインページでセッションを維持する方法は?
  • CSVデータ適用の方法について聞く
    テストケースをCSVから読み込んで実行する方法を教えてください。
    などなど。。。

:cake:公式リソースと組み合わせる
AIの回答は強力ですが、公式ドキュメントを 併用 することで、より確実な情報を得られます。

リソース リンク
Playwright公式ドキュメント https://playwright.dev
Microsoft Copilotの情報 https://copilot.microsoft.com
AIのプライバシーポリシー https://privacy.microsoft.com/en-us/privacystatement

✨試してみて、公式情報を確認しながらAIに質問すると、さらに改善点を修正していく感じ

下記に自分のローカル環境で検証した一例を記載します。※参考になれば幸いです。

参考:https://qiita.com/kzk_nemoto/items/567b698a44cd045f72ea
1、環境情報

OS:Windows10、VSC
参考:https://playwright.dev/docs/getting-started-vscode

 $npx --v
 10.9.2
 $ node -v
 v22.14.0
 $ npx playwright --version
 Version 1.52.0

2,playwrighの記録機能で検証スクリプト元ネタ作成

$ npx playwright codegen http://localhost:8080/LoginApp/ -o test.login.spec.ts

※検証用のログインアプリは最後に記載(これも実はAIで:information_desk_person_tone2:

・playwrighの記録結果

import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
  await page.goto('http://localhost:8080/LoginApp/');
  await page.locator('input[name="username"]').click();
  await page.locator('input[name="username"]').click();
  await page.locator('input[name="username"]').fill('admin');
  await page.locator('input[name="password"]').click();
  await page.locator('input[name="password"]').fill('password123');
  await page.getByRole('button', { name: 'ログイン' }).click();
  await page.getByText('ようこそ, admin さん! メニュー ログアウト').click();
  await page.getByRole('link', { name: 'メニュー' }).click();
  await page.locator('html').click();
  await page.getByRole('link', { name: 'Twilio Voice JavaScript SDK v2' }).click();
  await page.goto('http://localhost:8080/LoginApp/menu.jsp');
});

3,これをAIにCSV形式でテストケースを取り込む方式に変更とセッション管理するように依頼

import fs from 'fs';
import path from 'path';
import { parse } from 'csv-parse/sync';
import { test } from '@playwright/test';

// セッションファイルのパス
const sessionFilePath = path.resolve(__dirname, 'session.json');

// セッションファイルの存在を確認
if (!fs.existsSync(sessionFilePath)) {
    throw new Error(`⚠ セッションファイルが見つかりません: ${sessionFilePath} を生成してください!`);
}

// セッション情報を適用(全テストでログイン済み状態を維持)
test.use({ storageState: sessionFilePath });

// CSVファイルのパス
const csvFilePath = path.resolve(__dirname, 'test-cases.csv');

// CSVデータを読み込んでパース
const cases = parse(fs.readFileSync(csvFilePath, 'utf-8'), {
    columns: true,
    skip_empty_lines: true,
    relax_column_count: true
});

console.log('✔ CSVデータ読み込み完了:', cases);

test.describe('AI生成を利用したテスト', () => {
    cases.forEach((data, index) => {
        test(`テストケース ${index + 1}: ${data.case || '不明なケース'}`, async ({ page }) => {
            test.setTimeout(120_000);
            console.log(`実行中のテスト: ${data.case}`);
            console.log(`現在のページURL: ${page.url()}`);

            switch (data.case?.toLowerCase()) {
                case 'login':
                    console.log(`→ [${data.case}] ログイン処理開始: ユーザー名=${data.username}`);
                    await page.goto('http://localhost:8080/LoginApp/');
                    await page.locator('input[name="username"]').fill(data.username);
                    await page.locator('input[name="password"]').fill(data.password);
                    await page.getByRole('button', { name: 'ログイン' }).click();

                    // 認証後のページ遷移を待機
                    await page.waitForURL('http://localhost:8080/LoginApp/welcome.jsp', { timeout: 60000 });

                    // セッション情報を保存(初回ログイン時のみ)
                    await page.context().storageState({ path: sessionFilePath });
                    console.log(`✔ ログイン完了 - セッション情報を保存`);
                    break;

                case 'welcome':
                case 'menu':
                    console.log(`→ [${data.case}] ログイン済みのセッションでページ遷移`);

                    if (data.actionType?.toLowerCase() === 'link') {
                        console.log(`→ [${data.case}] リンククリック: ${data.actionTarget}`);

                        // ページロード完了を待機
                        await page.waitForLoadState('networkidle');

                        // `href` があるか確認
                        let linkLocator;
                        if (data.actionTarget.startsWith('http')) {
                            linkLocator = page.getByRole('link', { name: data.actionTarget });
                        } else {
                            linkLocator = page.locator(`text=${data.actionTarget}`);
                        }

                        // リンク要素の存在を確認
                        await linkLocator.waitFor({ state: 'visible', timeout: 60000 });

                        // 強制クリック
                        await linkLocator.click({ force: true });

                        // クリック後のページ遷移を待機
                        await page.waitForURL(data.actionTarget, { timeout: 60000 });
                    } else {
                        throw new Error(`未定義のアクションタイプ: ${data.actionType}`);
                    }
                    break;

                default:
                    throw new Error(`未定義のケース: ${data.case}`);
            }
        });
    });
});
test-cases.csv
case,username,password,actionType,actionTarget
Login,admin,password123,url,http://localhost:8080/LoginApp/welcome.jsp
welcome,,,link,http://localhost:8080/LoginApp/menu.jsp
Menu,,,link,https://qiita.com/halapolo/items/6c0dd905c665fd2e7e51
session.json
{
  "cookies": [
    {
      "name": "JSESSIONID",
      "value": "B0A9D5433D1A99B0C660FB1EC67FC0E7",
      "domain": "localhost",
      "path": "/LoginApp",
      "expires": -1,
      "httpOnly": true,
      "secure": false,
      "sameSite": "Lax"
    }
  ],
  "origins": []
}

https://www.npmjs.com/package/csv-parse

npm i csv-parse

4,これをPOM化対応にAI依頼

AI.Test.Pom.spec.ts
import fs from 'fs';
import path from 'path';
import { parse } from 'csv-parse/sync';
import { test } from '@playwright/test';
import { LoginPage } from './LoginPage';
import { WelcomePage } from './WelcomePage';
import { MenuPage } from './MenuPage';

// セッションファイルのパス
const sessionFilePath = path.resolve(__dirname, 'session.json');

// セッションファイルの存在を確認
if (!fs.existsSync(sessionFilePath)) {
    throw new Error(`⚠ セッションファイルが見つかりません: ${sessionFilePath} を生成してください!`);
}

// セッション情報を適用
test.use({ storageState: sessionFilePath });

// CSVファイルのパス
const csvFilePath = path.resolve(__dirname, 'test-cases.csv');

// CSVデータを読み込む
const cases = parse(fs.readFileSync(csvFilePath, 'utf-8'), {
    columns: true,
    skip_empty_lines: true,
    relax_column_count: true
});

console.log('✔ CSVデータ読み込み完了:', cases);

test.describe('AI生成を利用したテスト', () => {
    cases.forEach((data, index) => {
        test(`テストケース ${index + 1}: ${data.case || '不明なケース'}`, async ({ page }) => {
            test.setTimeout(120_000);
            console.log(`実行中のテスト: ${data.case}`);

            const loginPage = new LoginPage(page);
            const welcomePage = new WelcomePage(page);
            const menuPage = new MenuPage(page);

            switch (data.case?.toLowerCase()) {
                case 'login':
                    await loginPage.navigate();
                    await loginPage.login(data.username, data.password);

                    // 初回ログイン時にセッションを保存
                    await page.context().storageState({ path: sessionFilePath });
                    console.log(`✔ ログイン完了 - セッション情報を保存`);
                    break;

                case 'welcome':
                    await welcomePage.navigate();
                    await welcomePage.clickMenu(data.actionTarget);
                    break;

                case 'menu':
                    await menuPage.navigate();
                    await menuPage.clickLink(data.actionTarget);
                    break;

                default:
                    throw new Error(`未定義のケース: ${data.case}`);
            }
        });
    });
});
LoginPage.ts
import { Page } from '@playwright/test';

export class LoginPage {
    constructor(private page: Page) {}

    async navigate() {
        await this.page.goto('http://localhost:8080/LoginApp/');
    }

    async login(username: string, password: string) {
        await this.page.locator('input[name="username"]').fill(username);
        await this.page.locator('input[name="password"]').fill(password);
        await this.page.getByRole('button', { name: 'ログイン' }).click();

        // 認証後のページ遷移を待機
        await this.page.waitForURL('http://localhost:8080/LoginApp/welcome.jsp', { timeout: 60000 });
        console.log(`✔ ログイン成功`);
    }
}
WelcomePage.ts
import { Page } from '@playwright/test';

export class WelcomePage {
    constructor(private page: Page) {}

    async navigate() {
        await this.page.goto('http://localhost:8080/LoginApp/welcome.jsp');
    }

    async clickMenu(linkName: string) {
        await this.page.waitForLoadState('networkidle');
        // `href` で要素を検索する
        const linkLocator = this.page.locator(`a[href="${linkName}"]`);
        const menuLink = this.page.getByRole('link', { name: 'メニュー' });

        await menuLink.waitFor({ state: 'visible', timeout: 60000 });
        await menuLink.click({ force: true });

        // ページ遷移の待機
        await this.page.waitForURL(linkName, { timeout: 60000 });
        //await this.page.waitForURL('http://localhost:8080/LoginApp/menu.jsp', { timeout: 60000 });
    }
}
MenuPage.ts
import { Page } from '@playwright/test';

export class MenuPage {
    constructor(private page: Page) {}
    
    async navigate() {
        await this.page.goto('http://localhost:8080/LoginApp/menu.jsp');
    }
    async clickLink(linkName: string) {
        console.log(`→ メニューのリンククリック: ${linkName}`);
    
        // ページロード完了を待機
        await this.page.waitForLoadState('networkidle');

        // `href` で要素を検索する
       const linkLocator = this.page.locator(`a[href="${linkName}"]`);
       const menuLink = this.page.getByRole('link', { name: 'Twilio Voice JavaScript SDK v2' });

        await menuLink.waitFor({ state: 'visible', timeout: 60000 });
        await menuLink.click({ force: true });

        // ページ遷移の待機
        await this.page.waitForURL(linkName, { timeout: 60000 });
    }
}

検証アプリ及び検証スクリプトZIP

5,テスト結果

$ npx playwright test AI.Test.Pom.spec.ts --ui 

Ai.pom.snapshot.JPG

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?