Electron アプリの E2E テストを Playwright で書く

| 6 min read
Author: masahiro-kondo masahiro-kondoの画像

Electron アプリの E2E テストフレームワークとして Spectron というプロジェクトがありましたが、今年の2月に非推奨になりました。

Spectron 非推奨通知 | Electron

Spectron が Electron のリリーススピードに追従できなくなってるというのは知っていましたので、「やはり。」という感想でした。Spectron は Electron v14 で廃止された remote module に依存しており、メンテナが少ない状態でコードベースを書き換えることを断念したようです。

筆者が開発している Electron アプリでも Spectron の使用をやめました。しかし Electron アプリはクロスプラットフォームで動作するため、各プラットフォームでの簡単な動作は CI で確認できるようにしておきたいところです。

公式ドキュメントを見てたら Playwright が使えるようなので試してみました。

自動テスト | Electron

Playwright は Microsoft が開発するクロスブラウザに対応した E2E テストライブラリです。

Fast and reliable end-to-end testing for modern web apps | Playwright

Playwright のドキュメントでは、Android や AndroidWebView などと共に Electron のサポートは Experimental と位置付けられています。

Electron のプロジェクトに、Playwright 関連の NPM パッケージを追加します。

npm i -D playwright @playwright/test

対象のプロジェクトは src 配下にコードを配置しており、test 配下に テストコードを追加しました。

.
├── node_modules
├── package.json
├── src
│   ├── index.html
│   ├── main.js
│   ├── renderer.js
│   └── styles.css
└── test
    └── test.js

ひとまず、アプリを起動してクローズするだけのテストコードを書いてみました。

const { _electron: electron } = require('playwright');
const { test } = require('@playwright/test');

test('launch app', async () => {
const electronApp = await electron.launch({ args: ['src/main.js'] });
await electronApp.close();
});

package.json の scripts にテスト実行用のスクリプトを追加しました。

  "scripts": {
"start": "electron .",
"test": "playwright test",
"pack": "electron-builder --dir",
"dist": "electron-builder"
},

テストの実行。

npm test

これで ウィンドウが一瞬表示されました。

Playwright の API を使って、ウィンドウの起動を待ち、起動後にコンテンツの読み込みを2秒待ってスクリーンショットを取得してみます。

test('launch app', async () => {
const electronApp = await electron.launch({ args: ['src/main.js'] });
const mainWindow = await electronApp.firstWindow();
await mainWindow.waitForTimeout(2000);
await mainWindow.screenshot({ path: './screenshot/main.png' });
await electronApp.close();
});

取得したスクリーンショット。

このアプリは、Scrapbox のサイトを WebContents として読み込んでいるのですが、アプリのフレーム部分しか写っていません。

実はこのアプリは以前のブログで書いた、BrowserView を使用しており、ウィンドウ内のコンテンツは埋め込みではなく、別の子ウィンドウのようなものなので、Playwright の page.screenshot() では採取できません。

そこで、electronApplication.windows() を使って、アプリの全てのウィンドウを取得し、BrowserView のスクリーンショットを採取してみました。

https://playwright.dev/docs/api/class-electronapplication#electron-application-windows

test('launch app', async () => {
const electronApp = await electron.launch({ args: ['src/main.js'] });
const mainWindow = await electronApp.firstWindow();
await mainWindow.waitForTimeout(2000);
await mainWindow.screenshot({ path: './screenshot/main.png' });

const windows = await electronApp.windows();
await windows[1].screenshot({ path: './screenshot/child.png' });

await electronApp.close();
});

無事取得できました。

次に、ツールバーの Fav ボタン(☆アイコン)をクリックして Fav ページのスクリーンショットを取得するコードを追加。ボタンの Selector は Electron の DevTools で取得しました。これも無事動きました。

test('launch app', async () => {
const electronApp = await electron.launch({ args: ['src/main.js'] });
const mainWindow = await electronApp.firstWindow();
await mainWindow.waitForTimeout(2000);
await mainWindow.screenshot({ path: './screenshot/main.png' });

let windows = await electronApp.windows();
await windows[1].screenshot({ path: './screenshot/child.png' });

await mainWindow.click('#inspire > div.v-application--wrap > header > div.v-toolbar__content > header > div > button:nth-child(11)');
await mainWindow.waitForTimeout(2000);
windows = await electronApp.windows();
await windows[2].screenshot({ path: './screenshot/favs1.png' });

await electronApp.close();
});

この 'launch app' テストは色々やりすぎてごちゃついてきたし、Assertion も入れていないので、リファクタリングします。
テストメソッドを分割して、Playwright の test.beforeEach()test.afterEache() を使ってテスト毎にアプリを起動、終了するようにしました。起動テストと、Fav ページのテストメソッドを分割し、expect API を使ってタイトル文字列や、取得したウィンドウの数などを検証するコードを追加しました。

const { _electron: electron } = require('playwright');
const { test, expect } = require('@playwright/test');

let electronApp;
let mainWindow;

test.beforeEach(async ({ page }, testInfo) => {
console.log(`Running ${testInfo.title}`);
electronApp = await electron.launch({ args: ['src/main.js'] });
mainWindow = await electronApp.firstWindow();
await mainWindow.waitForTimeout(2000);
});

test.afterEach(async ({ page }, testInfo) => {
await electronApp.close();
});

test('launch app', async () => {
const title = await mainWindow.title();
expect(title).toBe('sbe');
await mainWindow.screenshot({ path: './screenshot/main.png' });

const windows = await electronApp.windows();
expect(windows.length).toBe(2);
console.log(await windows[1].title());
await windows[1].screenshot({ path: './screenshot/child.png' });
});

test('open fav page', async() => {
await mainWindow.click('#inspire > div.v-application--wrap > header > div.v-toolbar__content > header > div > button:nth-child(11)');
await mainWindow.waitForTimeout(2000);
const windows = await electronApp.windows();
expect(windows.length).toBe(3);
await windows[2].screenshot({ path: './screenshot/favs.png' });
});

実行結果。

> sbe@3.0.0-beta.1 test
> playwright test


Running 2 tests using 1 worker

  ✓  test/test.js:18:1 › launch app (3s)
Running launch app
Scrapbox - チームのための新しい共有ノート
  ✓  test/test.js:32:1 › open fav page (5s)
Running open fav page

以上、Playwright を使って Electron アプリの簡単な動作確認テストを書いてみました。Spectron に比べてもテストは書きやすい印象でした。まだ実験的なサポートという位置付けですが、Spectron の開発が止まった現在、正式対応が待たれるところです。

豆蔵デベロッパーサイト - 先週のアクセスランキング
  1. 基本から理解するJWTとJWT認証の仕組み (2022-12-08)
  2. AWS認定資格を12個すべて取得したので勉強したことなどをまとめます (2022-12-12)
  3. Nuxt3入門(第4回) - Nuxtのルーティングを理解する (2022-10-09)
  4. Nuxt3入門(第1回) - Nuxtがサポートするレンダリングモードを理解する (2022-09-25)
  5. Nuxt3入門(第8回) - Nuxt3のuseStateでコンポーネント間で状態を共有する (2022-10-28)
  6. Jest再入門 - 関数・モジュールモック編 (2022-07-03)
  7. 自然言語処理初心者が「GPT2-japanese」で遊んでみた (2022-07-08)
  8. IoT を使ってみる(その6:MQTTブローカー Mosquitto編) (2022-10-08)
  9. Nuxt3入門(第3回) - ユニバーサルフェッチでデータを取得する (2022-10-06)
  10. 統計学で避けて通れない自由度の話 (2022-06-20)