Day 4: ページ操作とフォーム
今日学ぶこと
- page.goto() とナビゲーションオプション(waitUntil)
- page.goBack(), page.goForward(), page.reload()
- フォーム入力: fill(), clear(), pressSequentially()
- チェックボックス: check(), uncheck()
- ラジオボタンの操作
- セレクトボックス: selectOption()
- ファイルアップロード: setInputFiles()
- ファイルダウンロードの処理
- ダイアログ処理(alert, confirm, prompt)
- キーボード・マウス操作(press, click, dblclick, hover, dragTo)
ページナビゲーション
page.goto() の基本
page.goto() は指定したURLにナビゲーションします。戻り値としてレスポンスオブジェクトを受け取れます。
import { test, expect } from '@playwright/test';
test('basic navigation', async ({ page }) => {
// Basic navigation
await page.goto('https://example.com');
// Get the response object
const response = await page.goto('https://example.com/api');
console.log(response?.status()); // 200
});
waitUntil オプション
page.goto() の waitUntil オプションで、どの時点でナビゲーション完了とみなすかを制御できます。
test('waitUntil options', async ({ page }) => {
// Default: 'load' - window.onload event fires
await page.goto('https://example.com');
// Wait until DOM content is loaded (faster)
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded'
});
// Wait until there are no network connections for 500ms
await page.goto('https://example.com', {
waitUntil: 'networkidle'
});
// Don't wait - just start navigation
await page.goto('https://example.com', {
waitUntil: 'commit'
});
});
| オプション | 説明 | 用途 |
|---|---|---|
load |
load イベント発火まで待機(デフォルト) |
一般的なページ |
domcontentloaded |
DOMContentLoaded イベントまで待機 | 高速に操作を開始したい場合 |
networkidle |
ネットワーク接続が500ms以上ない状態まで待機 | SPAやAPI呼び出しが多いページ |
commit |
レスポンスを受信した時点 | 最小限の待機 |
タイムアウトの設定
test('navigation with timeout', async ({ page }) => {
await page.goto('https://slow-site.com', {
timeout: 60000 // 60 seconds
});
});
ブラウザナビゲーション
test('browser navigation', async ({ page }) => {
await page.goto('https://example.com/page1');
await page.goto('https://example.com/page2');
// Go back to page1
await page.goBack();
await expect(page).toHaveURL(/page1/);
// Go forward to page2
await page.goForward();
await expect(page).toHaveURL(/page2/);
// Reload the current page
await page.reload();
await expect(page).toHaveURL(/page2/);
});
フォーム入力
fill() - テキスト入力の基本
fill() はフィールドの既存の値をクリアしてから新しい値を設定します。
test('fill text inputs', async ({ page }) => {
await page.goto('https://example.com/form');
// Fill text input
await page.getByLabel('Username').fill('testuser');
// Fill email input
await page.getByLabel('Email').fill('test@example.com');
// Fill password input
await page.getByLabel('Password').fill('SecurePass123!');
// Fill textarea
await page.getByLabel('Bio').fill('Hello, I am a test user.\nNice to meet you.');
});
clear() - 入力値のクリア
test('clear input', async ({ page }) => {
await page.goto('https://example.com/form');
const input = page.getByLabel('Username');
await input.fill('testuser');
// Clear the input
await input.clear();
await expect(input).toHaveValue('');
});
pressSequentially() - 1文字ずつ入力
pressSequentially() はキーボードで1文字ずつ入力をシミュレートします。オートコンプリートやリアルタイムバリデーションのテストに便利です。
test('press sequentially for autocomplete', async ({ page }) => {
await page.goto('https://example.com/search');
// Type one character at a time with delay
await page.getByLabel('Search').pressSequentially('playwright', {
delay: 100 // 100ms delay between each keystroke
});
// Wait for autocomplete suggestions
await expect(page.getByRole('listbox')).toBeVisible();
});
fill() vs pressSequentially():
fill()は値を直接設定するため高速です。pressSequentially()はキーイベントを1つずつ発火するため、入力中のイベントに依存するUIのテストに適しています。
チェックボックスとラジオボタン
check() と uncheck()
test('checkboxes', async ({ page }) => {
await page.goto('https://example.com/settings');
// Check a checkbox
await page.getByLabel('Enable notifications').check();
await expect(page.getByLabel('Enable notifications')).toBeChecked();
// Uncheck a checkbox
await page.getByLabel('Enable notifications').uncheck();
await expect(page.getByLabel('Enable notifications')).not.toBeChecked();
// check() is idempotent - does nothing if already checked
await page.getByLabel('Accept terms').check();
await page.getByLabel('Accept terms').check(); // No error
});
ラジオボタン
test('radio buttons', async ({ page }) => {
await page.goto('https://example.com/survey');
// Select a radio button
await page.getByLabel('Monthly plan').check();
await expect(page.getByLabel('Monthly plan')).toBeChecked();
// Select a different radio button in the same group
await page.getByLabel('Annual plan').check();
await expect(page.getByLabel('Annual plan')).toBeChecked();
await expect(page.getByLabel('Monthly plan')).not.toBeChecked();
});
セレクトボックス
selectOption()
test('select dropdowns', async ({ page }) => {
await page.goto('https://example.com/form');
// Select by value
await page.getByLabel('Country').selectOption('jp');
// Select by label text
await page.getByLabel('Country').selectOption({ label: 'Japan' });
// Select by index
await page.getByLabel('Country').selectOption({ index: 2 });
// Multiple selection (for <select multiple>)
await page.getByLabel('Languages').selectOption(['en', 'ja', 'ko']);
});
選択値の検証
test('verify selected option', async ({ page }) => {
await page.goto('https://example.com/form');
await page.getByLabel('Country').selectOption('jp');
await expect(page.getByLabel('Country')).toHaveValue('jp');
});
ファイルアップロード
setInputFiles()
test('file upload', async ({ page }) => {
await page.goto('https://example.com/upload');
// Upload a single file
await page.getByLabel('Profile picture').setInputFiles('tests/fixtures/avatar.png');
// Upload multiple files
await page.getByLabel('Documents').setInputFiles([
'tests/fixtures/doc1.pdf',
'tests/fixtures/doc2.pdf'
]);
// Clear file selection
await page.getByLabel('Profile picture').setInputFiles([]);
});
バッファを使ったアップロード
実際のファイルを用意せずにテストしたい場合は、バッファを使えます。
test('upload from buffer', async ({ page }) => {
await page.goto('https://example.com/upload');
await page.getByLabel('CSV file').setInputFiles({
name: 'data.csv',
mimeType: 'text/csv',
buffer: Buffer.from('name,age\nAlice,30\nBob,25')
});
});
ファイルダウンロード
download イベントの監視
import { test, expect } from '@playwright/test';
import path from 'path';
test('file download', async ({ page }) => {
await page.goto('https://example.com/downloads');
// Start waiting for download before clicking
const downloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: 'Download Report' }).click();
const download = await downloadPromise;
// Verify the file name
expect(download.suggestedFilename()).toBe('report.pdf');
// Save the file to a specific path
await download.saveAs(path.join('tests/downloads', download.suggestedFilename()));
});
ダイアログ処理
Playwright では page.on('dialog') でブラウザのダイアログ(alert, confirm, prompt)を処理します。ダイアログハンドラはダイアログが表示される前に登録する必要があります。
alert
test('handle alert', async ({ page }) => {
await page.goto('https://example.com');
page.on('dialog', async dialog => {
expect(dialog.type()).toBe('alert');
expect(dialog.message()).toBe('Operation completed!');
await dialog.accept();
});
await page.getByRole('button', { name: 'Show Alert' }).click();
});
confirm
test('handle confirm - accept', async ({ page }) => {
await page.goto('https://example.com');
page.on('dialog', async dialog => {
expect(dialog.type()).toBe('confirm');
await dialog.accept(); // Click OK
});
await page.getByRole('button', { name: 'Delete' }).click();
await expect(page.getByText('Item deleted')).toBeVisible();
});
test('handle confirm - dismiss', async ({ page }) => {
await page.goto('https://example.com');
page.on('dialog', async dialog => {
await dialog.dismiss(); // Click Cancel
});
await page.getByRole('button', { name: 'Delete' }).click();
await expect(page.getByText('Item deleted')).not.toBeVisible();
});
prompt
test('handle prompt', async ({ page }) => {
await page.goto('https://example.com');
page.on('dialog', async dialog => {
expect(dialog.type()).toBe('prompt');
expect(dialog.defaultValue()).toBe('');
await dialog.accept('My Answer'); // Enter text and click OK
});
await page.getByRole('button', { name: 'Enter Name' }).click();
await expect(page.getByText('Hello, My Answer')).toBeVisible();
});
キーボード操作
press() - 特殊キー
test('keyboard actions', async ({ page }) => {
await page.goto('https://example.com/editor');
const editor = page.getByRole('textbox');
await editor.fill('Hello World');
// Press Enter
await editor.press('Enter');
// Press keyboard shortcuts
await editor.press('Control+a'); // Select all
await editor.press('Control+c'); // Copy
await editor.press('End');
await editor.press('Control+v'); // Paste
// Press Escape
await page.press('body', 'Escape');
// Press Tab to move focus
await editor.press('Tab');
});
使用可能なキー名
| キー | 値 |
|---|---|
| Enter | Enter |
| Tab | Tab |
| Escape | Escape |
| Backspace | Backspace |
| Delete | Delete |
| 矢印キー | ArrowUp, ArrowDown, ArrowLeft, ArrowRight |
| 修飾キー | Control, Shift, Alt, Meta |
マウス操作
click のバリエーション
test('mouse click variations', async ({ page }) => {
await page.goto('https://example.com');
// Standard click
await page.getByRole('button', { name: 'Submit' }).click();
// Double click
await page.getByText('Editable text').dblclick();
// Right click (context menu)
await page.getByText('Right click me').click({ button: 'right' });
// Click with modifier keys
await page.getByRole('link', { name: 'Open' }).click({ modifiers: ['Control'] });
// Click at specific position within the element
await page.getByTestId('canvas').click({ position: { x: 100, y: 200 } });
});
hover
test('hover action', async ({ page }) => {
await page.goto('https://example.com');
await page.getByText('Hover me').hover();
await expect(page.getByText('Tooltip content')).toBeVisible();
});
ドラッグ&ドロップ
test('drag and drop', async ({ page }) => {
await page.goto('https://example.com/kanban');
// Drag source to target
const source = page.getByText('Task 1');
const target = page.getByTestId('done-column');
await source.dragTo(target);
await expect(target).toContainText('Task 1');
});
実践: ユーザー登録フォームのテスト
これまでの内容を組み合わせた実践的なテスト例です。
import { test, expect } from '@playwright/test';
test.describe('User Registration Form', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://example.com/register');
});
test('complete registration with all fields', async ({ page }) => {
// Text inputs
await page.getByLabel('Full Name').fill('Taro Yamada');
await page.getByLabel('Email').fill('taro@example.com');
await page.getByLabel('Password').fill('SecurePass123!');
await page.getByLabel('Confirm Password').fill('SecurePass123!');
// Select dropdown
await page.getByLabel('Country').selectOption({ label: 'Japan' });
// Radio button
await page.getByLabel('Monthly plan').check();
// Checkboxes
await page.getByLabel('Technology').check();
await page.getByLabel('Music').check();
// File upload
await page.getByLabel('Avatar').setInputFiles({
name: 'avatar.png',
mimeType: 'image/png',
buffer: Buffer.from('fake-image-data')
});
// Terms agreement
await page.getByLabel('I agree to the terms').check();
// Submit
await page.getByRole('button', { name: 'Register' }).click();
// Verify success
await expect(page).toHaveURL(/\/welcome/);
await expect(page.getByText('Registration complete')).toBeVisible();
});
test('shows validation errors for empty required fields', async ({ page }) => {
await page.getByRole('button', { name: 'Register' }).click();
await expect(page.getByText('Name is required')).toBeVisible();
await expect(page.getByText('Email is required')).toBeVisible();
});
});
まとめ
| カテゴリ | メソッド | 用途 |
|---|---|---|
| ナビゲーション | page.goto(url, options) |
ページを開く |
| ナビゲーション | page.goBack() |
前のページに戻る |
| ナビゲーション | page.goForward() |
次のページに進む |
| ナビゲーション | page.reload() |
ページを再読み込み |
| テキスト入力 | locator.fill(value) |
値を設定(既存値をクリア) |
| テキスト入力 | locator.clear() |
入力値をクリア |
| テキスト入力 | locator.pressSequentially(text) |
1文字ずつ入力 |
| チェック | locator.check() |
チェックをオンにする |
| チェック | locator.uncheck() |
チェックをオフにする |
| セレクト | locator.selectOption(value) |
ドロップダウンを選択 |
| ファイル | locator.setInputFiles(files) |
ファイルをアップロード |
| ダウンロード | page.waitForEvent('download') |
ダウンロードを待機 |
| ダイアログ | page.on('dialog', handler) |
ダイアログを処理 |
| キーボード | locator.press(key) |
キーを押す |
| マウス | locator.click() |
クリック |
| マウス | locator.dblclick() |
ダブルクリック |
| マウス | locator.hover() |
ホバー |
| マウス | locator.dragTo(target) |
ドラッグ&ドロップ |
重要ポイント
- waitUntil を適切に選ぶ - SPAでは
networkidle、高速テストではdomcontentloadedを検討する - fill() を基本にする -
pressSequentially()はオートコンプリートなど特定のケースでのみ使う - ダイアログハンドラは事前登録 - ダイアログが表示される前に
page.on('dialog')を設定する - ダウンロードは Promise パターン -
waitForEventを先に呼んでからクリックする - check() は冪等 - 既にチェック済みでもエラーにならないため、安全に使える
練習問題
基本
page.goto()にwaitUntil: 'domcontentloaded'を指定してページを開き、networkidleとの速度差を体感してくださいpage.goBack()とpage.goForward()を使ったナビゲーションテストを書いてくださいfill()とpressSequentially()で同じテキストを入力し、動作の違いを観察してください
応用
- セレクトボックス、チェックボックス、ラジオボタンを含むフォームの操作テストを作成してください
setInputFiles()でバッファを使ったファイルアップロードテストを書いてください- alert, confirm, prompt の3種類のダイアログを処理するテストを作成してください
チャレンジ
- ドラッグ&ドロップを使ったUI操作テストを作成してください
- ファイルダウンロードとその内容の検証を行うテストを作成してください
参考リンク
次回予告
Day 5では、アサーションとスナップショットを学びます。expect() の豊富なマッチャー、自動リトライの仕組み、ビジュアルリグレッションテストとしてのスクリーンショット比較など、テストの信頼性を高める技術を習得しましょう。