10日で覚えるPlaywrightDay 4: ページ操作とフォーム

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) ドラッグ&ドロップ

重要ポイント

  1. waitUntil を適切に選ぶ - SPAでは networkidle、高速テストでは domcontentloaded を検討する
  2. fill() を基本にする - pressSequentially() はオートコンプリートなど特定のケースでのみ使う
  3. ダイアログハンドラは事前登録 - ダイアログが表示される前に page.on('dialog') を設定する
  4. ダウンロードは Promise パターン - waitForEvent を先に呼んでからクリックする
  5. check() は冪等 - 既にチェック済みでもエラーにならないため、安全に使える

練習問題

基本

  1. page.goto()waitUntil: 'domcontentloaded' を指定してページを開き、networkidle との速度差を体感してください
  2. page.goBack()page.goForward() を使ったナビゲーションテストを書いてください
  3. fill()pressSequentially() で同じテキストを入力し、動作の違いを観察してください

応用

  1. セレクトボックス、チェックボックス、ラジオボタンを含むフォームの操作テストを作成してください
  2. setInputFiles() でバッファを使ったファイルアップロードテストを書いてください
  3. alert, confirm, prompt の3種類のダイアログを処理するテストを作成してください

チャレンジ

  1. ドラッグ&ドロップを使ったUI操作テストを作成してください
  2. ファイルダウンロードとその内容の検証を行うテストを作成してください

参考リンク


次回予告

Day 5では、アサーションとスナップショットを学びます。expect() の豊富なマッチャー、自動リトライの仕組み、ビジュアルリグレッションテストとしてのスクリーンショット比較など、テストの信頼性を高める技術を習得しましょう。