注目イベント!
アドベントカレンダー2024開催中!
一年を締めくくる特別なイベント、アドベントカレンダーを今年も開催しています!
初心者からベテランまで楽しめる内容で、毎日新しい技術トピックをお届けします。
詳細はこちらから!
event banner

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 の開発が止まった現在、正式対応が待たれるところです。

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。