Playwright Browser Automation

初步認識 Playwright

2023-03-04

什麼是 Playwright?

Playwright 是 Microsoft 開發的一套瀏覽器自動化框架。它可以讓你用程式碼控制瀏覽器,做到「人類用瀏覽器能做的任何事」——點擊按鈕、填寫表單、截圖、攔截網路請求等等。它的定位與 Puppeteer 類似,但支援的瀏覽器更多,API 設計也更完善。


底層原理

Playwright 的核心是透過 CDP(Chrome DevTools Protocol) 與 Chromium 溝通,對於 Firefox 則是透過其自家的 protocol,對 WebKit 也有類似的機制。簡單來說,它直接與瀏覽器核心對話,而不是模擬使用者的滑鼠和鍵盤事件(雖然它也可以那樣做)。

整個架構分為幾層:

  1. Browser:代表一個瀏覽器實例(例如 Chromium、Firefox 或 WebKit)。
  2. BrowserContext:類似於一個獨立的「無痕視窗」,每個 Context 有自己的 Cookie、LocalStorage 和 Session,彼此完全隔離。
  3. Page:就是一個分頁,所有操作都發生在 Page 上。

這個設計讓 Playwright 在做平行化測試時非常有優勢。你可以同時開多個 BrowserContext,每個 Context 跑自己的測試,互不干擾,速度極快。


基本安裝與使用

安裝很簡單:

pnpm init playwright@latest

這個指令會幫你初始化設定、安裝瀏覽器 binary,並且建立範例測試檔。

如果只是要在現有專案中加入 Playwright:

pnpm install -D @playwright/test
pnpm exec playwright install

寫一個最基本的測試

// tests/example.spec.ts
import { test, expect } from '@playwright/test';

test('title should be correct', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  await expect(page).toHaveTitle(/Playwright/);
});

執行測試:

pnpm exec playwright test

常用操作

// 點擊
await page.click('button#submit');

// 填寫輸入框
await page.fill('input[name="email"]', 'test@example.com');

// 等待某個元素出現
await page.waitForSelector('.success-message');

// 取得文字內容
const text = await page.textContent('.title');

// 截圖
await page.screenshot({ path: 'screenshot.png' });

Playwright 預設會自動等待元素變成可互動狀態(稱為 Auto-waiting),所以你通常不需要手動加 sleep 或等待語句,這一點比舊世代工具好很多。


特殊功能

1. 多瀏覽器支援

Playwright 同時支援 Chromium、Firefox、WebKit(Safari 核心),只需要在設定檔裡指定,就能讓同一套測試跑在三種引擎上。

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

2. 網路攔截(Network Interception)

你可以攔截特定的 API 請求,並替換為假資料,這對測試非常有用:

await page.route('**/api/users', async (route) => {
  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify([{ id: 1, name: 'Test User' }]),
  });
});

3. 模擬裝置與地理位置

const context = await browser.newContext({
  ...devices['iPhone 13'],
  geolocation: { longitude: 121.5654, latitude: 25.0330 }, // 台北
  permissions: ['geolocation'],
});

4. 視覺比對(Visual Comparison)

await expect(page).toHaveScreenshot('homepage.png');

Playwright 會自動比對截圖與之前儲存的基準圖,若有差異就報錯。這對於 UI 回歸測試非常方便。

5. Trace Viewer

當測試失敗時,Playwright 可以錄製一份包含截圖、網路記錄、Console log 的 trace 檔案:

pnpm exec playwright test --trace on
pnpm exec playwright show-trace trace.zip

開啟後你可以像看影片一樣,一步一步重播測試過程,找出哪個步驟出了問題。


奇異的使用技巧

技巧一:用 page.evaluate() 執行任意 JavaScript

page.evaluate() 讓你在瀏覽器的環境裡執行任意 JavaScript,並把結果回傳到 Node.js 端:

const scrollHeight = await page.evaluate(() => document.body.scrollHeight);
console.log('頁面總高度:', scrollHeight);

你甚至可以傳參數進去:

const result = await page.evaluate((selector) => {
  return document.querySelector(selector)?.innerText;
}, '.title');

技巧二:攔截並修改 Response

不只是 Request,連 Response 也可以攔截和修改:

await page.route('**/api/config', async (route) => {
  const response = await route.fetch();
  const json = await response.json();
  json.featureFlag = true; // 強制開啟某個 feature flag
  await route.fulfill({ response, json });
});

技巧三:多個 Page 同時操作

有些功能需要模擬兩個使用者同時操作(例如聊天測試),可以開兩個 Page:

const page1 = await context.newPage();
const page2 = await context.newPage();

await page1.goto('https://chat-app.example.com/room/1');
await page2.goto('https://chat-app.example.com/room/1');

await page1.fill('#message-input', 'Hello from user 1!');
await page1.click('#send-button');

await expect(page2.locator('.message-list')).toContainText('Hello from user 1!');

技巧四:利用 storageState 儲存登入狀態

每個測試都重新登入非常耗時。Playwright 可以把登入後的 Cookie 和 LocalStorage 存下來,之後直接載入:

// 先登入並儲存狀態
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('/login');
await page.fill('#username', 'admin');
await page.fill('#password', 'password');
await page.click('#login-button');
await context.storageState({ path: 'auth.json' });

// 之後的測試直接使用儲存的狀態
const loggedInContext = await browser.newContext({ storageState: 'auth.json' });

playwright.config.ts 中,也可以設定全域的 storageState,讓所有測試都自動帶入登入狀態。


神奇的使用方法

方法一:用 Playwright 做爬蟲

Playwright 並不只能拿來做測試,它是一個完整的瀏覽器控制工具,所以也很適合做爬蟲。特別是對於需要 JavaScript 渲染的頁面,Playwright 比傳統的 HTTP 爬蟲更有優勢:

import { chromium } from 'playwright';

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://some-spa-website.com/products');

  // 等待動態渲染完成
  await page.waitForSelector('.product-card');

  const products = await page.$$eval('.product-card', (cards) =>
    cards.map((card) => ({
      name: card.querySelector('.product-name')?.textContent,
      price: card.querySelector('.product-price')?.textContent,
    }))
  );

  console.log(products);
  await browser.close();
})();

方法二:用 codegen 自動生成程式碼

這是 Playwright 內建的一個殺手級功能。你只要執行:

pnpm exec playwright codegen https://example.com

Playwright 會打開瀏覽器,並在你操作瀏覽器的同時,自動把你的每一個動作轉換成對應的程式碼,即時顯示在旁邊的視窗中。對於不熟悉 Playwright API 的人來說,這個功能可以大幅降低學習門檻。

方法三:在 CI/CD 中跑 Headless 測試

Playwright 在無介面(headless)模式下可以完美在 CI/CD 環境(例如 GitHub Actions)中執行:

# .github/workflows/playwright.yml
name: Playwright Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
      - uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-report
          path: playwright-report/

測試完成後,HTML 報告會被上傳為 artifact,讓你在 CI 介面上就能看到詳細的測試結果。


總結

Playwright 是目前最成熟、功能最完整的瀏覽器自動化工具之一。它的多瀏覽器支援、Auto-waiting、網路攔截、Trace Viewer 等功能,讓撰寫和除錯測試都變得更加容易。不論你是要做 E2E 測試、視覺回歸測試,還是做一些需要操控瀏覽器的自動化任務,Playwright 都是一個值得深入學習的工具。