Playwrightでサイトマップに記載されたページのスクリーンショットを保存する
E2EテストツールであるPlaywrightですが、スクリーンショット機能を持っています。今回は自動化の観点からPlaywrightの活用方法を検討するため、sitemap.xml
に記載された全URLにアクセスしてスクリーンショットを保存するプログラムをつくってみました。
セットアップ
はじめに環境構築を行います。
- Node.js
- TypeScjript
- Playwright
# ディレクトリを作成
mkdir test-playwright-snapshot && cd $_
# package.jsonを作成
npm init
# ライブラリをインストール (Playwrightはinitコマンドでインストール)
npm install @types/node typescript uuid
# tsconfig.jsonを作成
npx tsc --init
# Playwrightの設定ファイルを作成、不要なサンプルコードを削除
npm init playwright@latest && rm -rf tests tests-examples
> test-playwright-snapshot@1.0.0 npx
> create-playwright
Getting started with writing end-to-end tests with Playwright:
Initializing project in '.'
✔ Where to put your end-to-end tests? · tests
✔ Add a GitHub Actions workflow? (y/N) · false
✔ Install Playwright browsers (can be done manually via 'npx playwright install')? (Y/n) · true
tsconfig.json
と package.json
を編集して、ES Modulesとビルド周りの設定をします。ここではルートディレクトリ下の ./dest
ディレクトリにビルド後のJavaScriptコードが格納される様に設定しています。
"target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"module": "ES2022" /* Specify what module code is generated. */,
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
@@ -1,12 +1,19 @@
{
"name": "test-playwright-snapshot",
"version": "1.0.0",
- "main": "index.js",
+ "main": "./dist/index.js",
+ "type": "module",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "build": "tsc",
+ "start": "node ./dist/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
- "description": ""
+ "description": "",
+ "dependencies": {
+ "@types/node": "^22.10.5",
+ "typescript": "^5.7.3",
+ "uuid": "^11.0.5"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.49.1"
+ }
}
ルートディレクトリにエントリポイントとなる index.ts
を作成して、Playwrightが動作することを確認します。
import { chromium } from "@playwright/test";
(async () => {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto("https://playwright.dev/");
const title = await page.title();
console.log(title);
await browser.close();
})();
npm run build && npm run start
> test-playwright-snapshot@1.0.0 build
> tsc
> test-playwright-snapshot@1.0.0 start
> node ./dist/index.js
Playwright
コンソールにページタイトル Playwright
が出力されていればOKです。
スクリーンショットを試してみる
環境構築ができたので、早速スクリーンショットAPIを使ってみます。
ドキュメントを見ると、{ fullPage: true }
オプションを指定することでページ全体のキャプチャを取得できることがわかります。バックグラウンドで適当なローカルサーバーを動かしたうえで、トップページのキャプチャを撮ってみましょう。
const baseUrl = "http://localhost:3000";
await page.goto(baseUrl);
await page.waitForURL(baseUrl);
await page.screenshot({ path: "top.png", fullPage: true });
$ npm run build && npm run start && open top.png
サイトマップからページのスクリーンショットを保存する
サイトマップをもとにサイト内のページにアクセスして、スクリーンショットを保存していきます。手順としては以下のとおりです。
-
http://localhost:3000/sitemap.xml
のレスポンスから<loc>
タグで囲まれたURLを取り出す - URLの配列でループさせて、スクリーンショットの保存処理を回す
import { chromium, expect, Response } from "@playwright/test";
import path from "path";
import { v4 as uuidv4 } from "uuid";
const sitemapUrl = "http://localhost:3000/sitemap.xml";
(async () => {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(sitemapUrl);
const html = await page.content();
// URLの一覧を取得する
const urls =
html
.match(/<loc>(.*?)<\/loc>/g)
?.map((regex) => regex.replace(/<\/?loc>/g, "")) || [];
// URLの配列でループさせる
for (const url of urls) {
const page = await context.newPage();
await page.goto(url);
// 画像がすべて表示されるのを待つ
const lazyImages = await (
await page.locator('img[loading="lazy"]:visible')
).all();
for (const image of lazyImages) {
await image.scrollIntoViewIfNeeded({ timeout: 0 });
await expect(image).not.toHaveJSProperty("naturalWidth", 0);
}
// ファイル名が被らない様にuuidで生成した名前でスクリーンショットを保存する
const fullPath = path.join(process.pid.toString(), `${uuidv4()}.png`);
await page.screenshot({ path: fullPath, fullPage: true, timeout: 0 });
console.log(`${fullPath} saved`);
}
await browser.close();
})();
注意点として、高負荷によるタイムアウトを意識する必要がありました。for
ループで回している部分に Promise
を使いたくなるのですが、逐次処理でなければメモリリーク起因?のタイムアウトが発生してしまいます。
適切に page
インスタンスの生成・廃棄ができればよいのでしょうが、うまくプログラミングできなかったのでこの形になりました。
// タイムアウトになる書き方 (Promise.allで非同期に実行)
await Promise.all(
urls.map(async (url) => {
const page = await context.newPage();
await page.goto(url);
const lazyImages = await (
await page.locator('img[loading="lazy"]:visible')
).all();
for (const image of lazyImages) {
await image.scrollIntoViewIfNeeded({ timeout: 0 });
await expect(image).not.toHaveJSProperty("naturalWidth", 0);
}
const fullPath = path.join(process.pid.toString(), `${uuidv4()}.png`);
await page.screenshot({ path: fullPath, fullPage: true, timeout: 0 });
console.log(`${fullPath} saved`);
})
);
node:internal/process/promises:288
triggerUncaughtException(err, true /* fromPromise */);
^
page.goto: Timeout 30000ms exceeded.
Call log:
- navigating to "http://localhost:3000/example", waiting until "load"
at /Users/yuki/src/test-playwright-snapshot/dist/index.js:16:20
at /Users/yuki/src/test-playwright-snapshot/async file:/Users/yuki/src/test-playwright-snapshot/dist/index.js:14:5 {
name: 'TimeoutError'
}
Node.js v18.20.4
また、Playwrightの非同期関数に対して執拗に { timeout: 0 })
を付与していますが、これがないとタイムアウトで落ちたり・落ちなかったりします。
最後に
Playwrightのスクリーンショット機能を試してみました。サイト全体のキャプチャを取得する目的で使用する場合、動的なコンテンツ (特に表示領域に入ったらアニメーションが発火する類のもの) は意図した結果になりにくく、サイトやページごとに調整が必要可能性が高いです。
自動化全般に言えることですが100%のクオリティにするのは難しいので、8割くらいの作業を簡略化する方向で考えてみたいと思います。
Discussion