🤖

Playwrightで自己署名証明書が必要な環境に接続してスクショを撮る方法

2023/06/08に公開

はじめに

playwrightは、様々な操作を自動化してくれる非常に便利なE2Eテストフレームワークです。
Puppeteerの後発なだけあり操作も簡単になっています。
ただタイトルの通り、自己署名証明書・・・平たく言えばオレオレ証明書が必要な環境に接続ができません・・・(これはPuppeteerも同じ)
以下のIssueには解決策が記述してありますが、こちらのコードをそのまま利用すると画像の読み込みに失敗したスクリーンショットを撮影してしまいます(´;ω;`)
https://github.com/microsoft/playwright/issues/1799
それだと非常に困ったので、上記のコードを改良して、正常にスクリーンショットが撮れるようにしようと思います。

この記事を読んでできるようになること

公式でサポートされていない自己署名証明書が必要となる環境にpage.goto()で移動して、スクリーンショットの撮影(それ以外の操作も可能です)ができるようになる。

環境

筆者の環境は以下の通りです。

  • node.js v18.16.0
  • axios@1.4.0
  • typescript@5.0.4
  • playwright@1.34.3

やりかた

上記のIssueで、@yyvessさんと@fargraphさんが提案してくれているやり方を踏襲して改良してみようと思います。

2023/06/17 追記

以前のコードでは、適切にheaderやcookieが設定できていなかったので、コードを修正しました。
以前のコードを参考にされている方は、修正したコードをご確認ください。

設定ファイル

テスト対象のブラウザを書き換えている以外は、全てデフォルトの設定値です。

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

export default defineConfig({
  testDir: './playwright-tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    trace: 'on-first-retry',
  },

  projects: [
    {
      name: 'Google Chrome',
      use: { ...devices['Desktop Chrome'], channel: 'chrome' },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'Microsoft Edge',
      use: { ...devices['Desktop Edge'], channel: 'msedge' },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 13'] },
    },
  ],
});

自己署名証明書の認証機能を追加したtest

提案されているコードはrequestを利用していますが、現在は非推奨パッケージになっているので、axiosに書き換えています。

caAuthenticationFixture.ts
import { test as base } from '@playwright/test';
import fs from 'fs';
import https from 'https';
import axios from 'axios';

export const test = base.extend({
    context: async ({ context }, use) => {
        await context.route('**/*', async (route, req) => {
	    // 自己署名証明書を設定
            const httpsAgent = new https.Agent({
                cert: fs.readFileSync('./certs/cert.pem'),
                key: fs.readFileSync('./certs/key.nopass.pem'),
                passphrase: fs.readFileSync('./certs/cert.passwd').toString(),
		ca: fs.readFileSync('./certs/ca.pem'),
		// caが設定できない場合は、以下のコメントを外してCAの検証を無視してください
                // rejectUnauthorized: false,
            });

            const options = {
                url: req.url(),
                method: req.method(),
                headers: req.headers(),
		data: req.postDataBuffer(),
                httpsAgent,
		withCredentials: true
            };
	    
	    // 一部ブラウザ(safari)は、formでデータを送信する際にcookieが付与されないので、手動で付与する
	    if (options.headers['content-type']?.match(/application\/x\-www\-form\-urlencoded/) {
	        if (!options.headers.cookie) {
		    let cookie = '';
		    const cookies = await context.cookies();
		    cookies.forEach(c => {
		        cookie += `${c.name}=${c.value}; `;
		    });
		    options.headers.cookie = cookie;
		}
	    }
	    
	    /* axiosに設定するresponseTypeを設定する
	     * ここを適切に設定しないとfirefoxやsafari等でエラーが発生する */
            switch (req.resourceType()){
                case 'image':
                    options['responseType']='arraybuffer';
                    break;
                case 'document':
                    options['responseType']='document';
                    break;
                case 'xhr':
                case 'script':
                case 'stylesheet':
                    options['responseType']='text';
                    break;
                default:
                    // responseTypeはデフォルト値(json)
                    break;
            }

            try {
                const res = await axios(options);
		let headers = JSON.parse(JSON.stringify(res.headers));
		
		// axiosで取得したレスポンスをそのまま渡してやる
                return route.fulfill({
                    status: res.status,
		    headers: headers,
                    contentType: headers['content-type'],
                    body: res.data,
                });
            } catch (error) {
                console.log(error);
		// axiosからエラーが返ってきた場合は無視する
                return route.abort();
            }
        });
        use(context);
    },
})

export * from '@playwright/test';

テストコード

通常と同様にスクリーンショットを撮影するだけです。

login_and_screenshot.spec.ts
// 先ほど機能を追加したtestをimport
import { test } from './certAuthenticationFixture';

test('login test', async ({ page, context }, testInfo) => {
    await page.goto('https://my.page.net', { waitUntil: 'load' });
    await page.screenshot({
        // projectsで設定したnameを、そのままファイル名に利用
        path: `./__screenshot__/${testInfo.project.name}.png`,
	fullPage: true
    });
});

まとめ

上記の方法で、無事に自己署名証明書が必要な環境でもスクリーンショットが撮影できるようになりました。
元の提案されているコードでは、requestが適切なデータフォーマットでファイルを取得していないのが原因でした(逆に言ってしまえば、svgなどは文字列形式なので正常に読み込まれてスクリーンショットにも映ります)。
ここを解決してやることで、正常にwebページが表示されるようにしてやっています。
私がresponseTypeを設定している部分は設定が甘い可能性が高いので、より良い方法があればコメントで教えていただけると嬉しいです。

Discussion