目的
Playwrightを活用した Page Object Model (POM) 化 と CSVデータ取り込みによるテスト管理 について、AIとどのように会話すれば 最適な回答を得られるか試みる。
AIと最適に会話するためのポイント
☆具体的な質問をする
AIへの質問は 明確で具体的に すると、より正確な回答を得られます。
良い例:
Playwrightで POM を活用したログインテストを設計するにはどうすればいいですか?
避けるべき例:
PlaywrightでPOMを作りたい。助けて!
☆詳細な要件を伝えることで、コード例や設計戦略を得やすい!
エラーメッセージや環境情報を共有
⇒トラブルシューティングの際は、エラーメッセージの全文と環境情報を含める と、AIは正確な原因を特定し、適切な解決策を提案しやすくなります。
良い例:
CSVデータの
actionTarget
にTwilio Voice JavaScript SDK v2
を設定すると、
以下のエラーが出ます。
Error: locator.waitFor: Test timeout of 120000ms exceeded
Playwrightのバージョン: 1.38.0
Node.jsのバージョン: 18.17.0
避けるべき例:
CSVデータを使っているけどリンクがクリックできない。なんで?
※注)エラーメッセージが具体的であるほど、AIの回答も最適化される。
☆ステップごとに会話する
一度に多くの質問をすると、AIの回答が曖昧になる ことがあります。
そのため、テーマを分割して段階的に会話するのがベスト です。
良い会話例:
- POMの設計について聞く
PlaywrightでログインページをPOM化する方法を教えてください。- セッション管理について聞く
POM化したログインページでセッションを維持する方法は?- CSVデータ適用の方法について聞く
テストケースをCSVから読み込んで実行する方法を教えてください。
などなど。。。
公式リソースと組み合わせる
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で)
・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}`);
}
});
});
});
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
{
"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依頼
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}`);
}
});
});
});
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(`✔ ログイン成功`);
}
}
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 });
}
}
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 });
}
}
5,テスト結果
$ npx playwright test AI.Test.Pom.spec.ts --ui