QR Send

スマホとPCのChrome間で、URLやテキストをQRコード1回のペアリングで送り合える自作Chrome拡張。アカウント不要・無料。

使ってみる →

Playwrightで「ファイルがダウンロードできたか」をテストする:waitForEvent(‘download’)とsaveAs()、Seleniumのprefs設定はもう要らない

Selenium・自動化

「selenium ダウンロード」で検索すると、`download.prompt_for_download`をはじめとしたChromeオプション(prefs)でダウンロードダイアログを抑止する話が大量に出てきます。Seleniumで自動テストをやってきた人なら、一度は書いたことがある設定だと思います。私も長年Rubyでこれをやってきました。

Playwrightに移行してから、この手の設定を書いた記憶がありません。最初は「移行ガイドに書き忘れているのでは」と自分の記事を疑ったくらいです。調べてみたら理由はシンプルで、Playwrightはブラウザ側の設定をいじらなくてもダウンロードを自動で受け取ってくれる作りになっていました。今回はこの「もう要らない設定」の話と、代わりに何を書けばいいのかをまとめます。

Seleniumで書いていたこと(おさらい)

Seleniumだと、ダウンロードダイアログを出さずに指定フォルダへ自動保存させるために、ブラウザのプロファイル設定(prefs)をこんな形で組んでいました。

prefs = {
  "download.default_directory" => "/path/to/downloads",
  "download.prompt_for_download" => false,
  "download.directory_upgrade" => true
}
options.add_option(:prefs, prefs)

これでダイアログを出さずに勝手に保存してくれるようになるのですが、そのあとが厄介でした。ファイルが本当に保存し終わったのかをコード側から確実に知る手段がなく、`.crdownload`拡張子の消滅やファイルサイズの安定を自前で監視するしかなかったんですね。このあたりの泥臭い待ち方は、以前Seleniumでファイルのダウンロード完了を確実に待つ記事にまとめました。Playwrightではこの監視コードがまるごと不要になります。

Playwrightは何もしなくてもダウンロードを受け取っている

公式ドキュメント(class-download)を確認したところ、Playwrightではダウンロードが発生すると`Download`オブジェクトが自動的に作られ、ブラウザコンテキストの`page.on(‘download’)`イベントとして飛んでくる、と明記されています。ダイアログを抑止するprefsのような設定は登場しません。ブラウザに任せず、Playwright自身がダウンロードを横取りして管理している、というイメージです。

つまりSeleniumで書いていた「ダウンロードダイアログを出さない設定」自体が、Playwrightの世界には存在しないんですね。設定を移植しようとして公式docsを探しても見つからないのは当然で、探しているものがそもそも無いからです。

まず基本形:waitForEvent(‘download’)

単発のダウンロードをテストするなら、`page.waitForEvent(‘download’)`でイベントを先に待ち構えてから、ダウンロードを発火させるクリックを実行します。

const [download] = await Promise.all([
  page.waitForEvent('download'),
  page.getByRole('button', { name: 'CSVダウンロード' }).click(),
]);

await download.saveAs('test-results/downloads/export.csv');

`waitForEvent`を先に呼んでから`click`を実行する、という順番がポイントです。クリックを先にやってから`waitForEvent`を呼ぶと、ダウンロードがクリックの一瞬で終わってしまい、イベントを取りこぼす場合があります。`Promise.all`でまとめて書くのは、Seleniumで明示的waitを使い慣れた人にはやや不思議な書き方に見えるかもしれませんが、Playwrightのイベント待ちではよく出てくる定型です。

公式docsによると、`saveAs()`はダウンロードが完了する前に呼んでも安全(ダウンロード完了まで内部で待ってくれる)だそうです。なので律儀に完了を待つ処理を別途書く必要もありません。ここが一番「Seleniumの頃の苦労は何だったのか」と感じたポイントでした。

ファイル名の検証にはsuggestedFilename()

保存先のファイル名を自分で決め打ちするのではなく、サーバーが指定したファイル名を検証したいこともあります。そういうときは`suggestedFilename()`を使います。

const filename = download.suggestedFilename();
expect(filename).toMatch(/^export_\d{8}\.csv$/);

await download.saveAs(`test-results/downloads/${filename}`);

`suggestedFilename()`はレスポンスの`Content-Disposition`ヘッダやリンクの`download`属性から、ブラウザが計算するファイル名を返してくれます。命名規則にバグが混入していないかを確認したいテストでは、ここを見るのが一番手っ取り早いと思います。

注意です:コンテキストを閉じるとファイルは消える

ここ、私が最初にハマったところです。ダウンロードのテストを書いて、`saveAs()`もちゃんと呼んでいるのに、テスト終了後にファイルを見に行ったら消えている、ということがありました。

公式docsに「ブラウザコンテキストに属するダウンロード済みファイルは、そのコンテキストが閉じられると削除される」とはっきり書いてあります。Playwright Testのフィクスチャは、テストが終わるタイミングで自動的にコンテキストを閉じるので、テスト内で`saveAs()`によるコピーを済ませておかないと、Playwrightが裏で持っている一時ファイルごと消えてしまうわけです。

なので、CSVやPDFの中身までテストで検証したいなら、`saveAs()`で自分の管理下にコピーしてから読み込む、という順番を必ず守ってください。テスト内で読み切ってしまうのであれば、`saveAs()`を挟まず`createReadStream()`で直接読む手もありますが、あとから中身を目視確認したい場合は`saveAs()`で残しておくほうが安心だと思います。

複数ファイルを同時にダウンロードする場合

1クリックで複数ファイルが同時に落ちてくるケースは、`waitForEvent`を1回呼ぶだけでは対応できません。こういうときは`page.on(‘download’)`で継続的に監視して、配列に溜めてから処理する形にします。

const downloads: Download[] = [];
page.on('download', (download) => {
  downloads.push(download);
});

await page.getByRole('button', { name: '一括ダウンロード' }).click();
await page.waitForTimeout(500); // 全ダウンロードが出揃うのを軽く待つ

for (const download of downloads) {
  const filename = download.suggestedFilename();
  await download.saveAs(`test-results/downloads/${filename}`);
}

`waitForTimeout`をここで使うのは正直あまり褒められた書き方ではないのですが(普段は`waitForTimeout`をなるべく使わない派です)、複数ダウンロードが「何個来るか」を事前に知らない場合、期待件数分のイベントを待つ以外の確実な待ち方が今のところ見当たりませんでした。期待するダウンロード件数が分かっているなら、`page.waitForEvent(‘download’)`を件数分`Promise.all`で並べるほうが安全です。ここは実装するUIの挙動に合わせて選んでください。

保存したファイルの中身まで検証する

ファイル名だけ検証して満足していないか、というのは正直自分にも刺さる指摘でした。ファイル名の形式が正しくても、中身が空だったり壊れていたりしたら意味がありません。CSVならNode.jsの標準機能で十分読めます。

import fs from 'fs';

const filePath = 'test-results/downloads/export.csv';
const content = fs.readFileSync(filePath, 'utf-8');

expect(content).toContain('氏名,メールアドレス');
expect(content.split('\n').length).toBeGreaterThan(1);

PDFの中身までテキスト検証したい場合は、`pdf-parse`のような専用ライブラリをテストコードに追加する必要が出てきます(Playwright自体にPDFの中身を読む機能はありません)。ここまでやって初めて「ダウンロードできた」ではなく「正しい中身がダウンロードできた」と言えるテストになると思います。

TypeScriptで書くときの型

`Download`型は`@playwright/test`から普通にimportできます。

import { test, expect, type Download } from '@playwright/test';

もしこのあたりのimportで`Cannot find module`系のエラーに当たった場合は、Playwright自体ではなく`tsconfig.json`側の設定が怪しいことが多いです。以前TypeScriptでCannot find moduleが出たときに順番に確認したことをまとめた記事を書いたので、よければあわせて見てください。

まとめ

SeleniumからPlaywrightに移行すると、ダウンロードダイアログを抑止するprefs設定はそもそも書かなくてよくなります。代わりに覚えるのは、`waitForEvent(‘download’)`でイベントを待ち構えてクリックする定型と、`saveAs()`でコンテキストが閉じる前にファイルをコピーしておくこと、それから複数ダウンロードなら`page.on(‘download’)`で配列に溜めることの3つくらいです。Seleniumで散々苦労した`.crdownload`監視のようなコードは、私の経験上まったく必要ありませんでした。

逆に言うと、Seleniumから移行してきた方ほど「昔ながらの設定を移植する箇所を探して見つからず不安になる」というポイントだと思うので、今回の記事がその不安の解消に役立てば幸いです。Selenium時代の他の設定がPlaywrightでどう変わるかはSeleniumからPlaywrightへの移行ガイドにAPI対応表としてまとめているので、あわせてどうぞ。他にも「これもう要らなくなったの?」という設定があれば、コメントで教えてもらえるとうれしいです。

コメント

タイトルとURLをコピーしました