BackstopJS を試す (Error: Failed to launch the browser process! エラー発生→ puppeteer のバージョンを指定して解決)

経緯

Jest でユニットテストができることは分かりました。何かを処理するものに関しては行けそうな気がします。

……しかし、Webサイトの場合は見た目も気になります。見た目をテストするにはどううれば良いのか……と考えていたところ、 BackstopJS というものがあることを知ったので試してみることにします。

準備

見た目のテストのためには何かページを生成するものが必要です。

今回は簡単に試せる環境として、手前味噌ですが arm-band/mori を使うことにしました。

1. パッケージ追加

package.jsonbackstopjs を追加します。

    // 略
    "devDependencies": {
        "backstopjs": "^5.0.6",      // 追加
        "browser-sync": "^2.26.12",
        // 略
    },
    // 略

2. yarn

これで yarn してインストールを待ちます。

> yarn

BackstopJS の準備

後は手順通りに backstop init して

> backstop init

生成された backstop.jsonscenarios -> url を編集。今回は Browsersync のローカルサーバにしたいので https://localhost:3000/ で。

    // 略
    "scenarios": [
        {
          "label": "BackstopJS Homepage",
          "cookiePath": "backstop_data/engine_scripts/cookies.json",
//          "url": "https://garris.github.io/BackstopJS/",
          "url": "https://localhost:3000/",                // 変更
          "referenceUrl": "",
            // 略
        }
    ]
    // 略

検証1 (失敗)

ここまで準備をしたら、正解の見本を保存するために backstop reference でスクリーンショットを撮ります。

まずは yarn start で Gulpタスク を走らせて https://localhost:3000/ を上げます。

次に、上述の backstop reference コマンドを実行します。

> backstop reference
BackstopJS v5.0.6
Loading config:  PATH\TO\TEST_PROJECT\randa_back_stopper\backstop.json 

COMMAND | Executing core for "reference"
  clean | backstop_data/bitmaps_reference was cleaned.
createBitmaps | Selected 1 of 1 scenarios.
      COMMAND | Command "reference" ended with an error 
after [1.414s]
      Error: Failed to launch the browser process!

                TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md     

                    at onClose (PATH\TO\TEST_PROJECT\randa_back_stopper\node_modules\puppeteer\lib\Launcher.js:750:14)
                    at ChildProcess.<anonymous> (PATH\TO\TEST_PROJECT\randa_back_stopper\node_modules\puppeteer\lib\Launcher.js:740:61)
                    at ChildProcess.emit (events.js:326:22)
                    at Process.ChildProcess._handle.onexit (internal/child_process.js:276:12)

# 略

……あれ?失敗しました。

Command “reference” ended with an error after

Error: Failed to launch the browser process!

エラーメッセージ的にはこの部分が原因っぽいですね。

そして、問題を起こしているのは puppeteer のようです。

ちなみに、 BackstopJS がスクリーンショットを撮る際は Puppeteer からヘッドレスブラウザとして Chromium でレンダリングしている模様。

ということで、原因は Puppeteer にあり。エラー文で検索しましたが、イマイチ情報が少ない上にあまり新しい情報がないので、半ば手探りで追及してみることになりました……。

Puppeteer の検証1

まずはそもそも Puppeteer が自分の環境で動くか確認してみます。

> node -v
v14.8.0

> npm -v
6.14.7

Node.js とnpm のバージョン確認。

この状態で新規のプロジェクトを作成します。

package.json

{
  "name": "puppeteer_test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "puppeteer": "^5.5.0"
  }
}

npm initpackage.json を用意して、 yarn add --dev puppeteerdevDependencies に追加します。

index.js

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://google.com/');
  await page.screenshot({path: 'screenshot.png'});

  await browser.close();
})();

Google のスクリーンショットを撮影するだけの簡単な JS を用意します。

これで

> yarn

# 略

> node index.js
>

……普通に撮影でき、 screenshot.png が保存されました。問題ありません。

Puppeteer の検証2

次に、 BackstopJS の内部で使用されている Puppeteer のバージョンを確認するために node_modules/backstopjs/package.json を調べます。

{
  "name": "backstopjs",
  "version": "5.0.6",
  "description": "BackstopJS: Catch CSS curveballs.",
  "bin": {
    "backstop": "./cli/index.js"
  },
  "scripts": {
    "lint": "eslint 'compare/*.js' 'compare/src/**/*.js' 'core/**/*.js' 'cli/**/*.js' 'capture/**/*.js' 'test/**/*.js'",
    "format": "prettier-eslint --write \"compare/src/**/*.js\"",
    "genConfig": "node ./cli/index.js genConfig",
    "init": "node ./cli/index.js init",
    "reference": "node ./cli/index.js reference",
    "test": "node ./cli/index.js test",
    "approve": "node ./cli/index.js approve",
    "openReport": "node ./cli/index.js openReport",
    "report": "node ./cli/index.js openReport",
    "echo": "node ./cli/index.js echo",
    "unit-test": "mocha --reporter spec --recursive 'test/**/*_spec.js'",
    "precommit": "lint-staged",
    "build-compare": "cp ./node_modules/diverged/src/diverged.js ./compare/output/ && cp ./node_modules/diff/dist/diff.js ./compare/output/ && webpack --config ./compare/webpack.config.js && npm run lint",
    "dev-compare": "webpack-dev-server --content-base ./compare/output --config ./compare/webpack.config.js",
    "integration-test": "rm -rf newdir && mkdir newdir && cd newdir && node ../cli/index.js genConfig && node ../cli/index.js reference && node ../cli/index.js test && node -e \"require('../')('test')\"",
    "smoke-test": "cd test/configs/ && node ../../cli/index.js test --config=backstop_features",
    "smoke-test-docker": "cd test/configs/ && node ../../cli/index.js test --config=backstop_features --docker",
    "sanity-test": "cd test/configs/ && node ../../cli/index.js test",
    "sanity-test-docker": "cd test/configs/ && node ../../cli/index.js test --docker",
    "kill-zombies": "pkill -f \"(chrome)?(--headless)\"",
    "copy-report-bundle": "mkdir -p test/configs/backstop_data/html_report && cp compare/output/index_bundle.js test/configs/backstop_data/html_report/",
    "build-and-copy-report-bundle": "npm run build-compare && npm run copy-report-bundle",
    "remote": "node ./cli/index.js remote",
    "stop": "curl http://localhost:3000/stop"
  },
  "lint-staged": {
    "compare/src/**/*.js": [
      "node_modules/.bin/prettier --single-quote --write",
      "git add"
    ]
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/garris/backstopjs.git"
  },
  "author": "https://github.com/garris/BackstopJS/graphs/contributors",
  "license": "MIT",
  "main": "core/runner.js",
  "devDependencies": {
    "assert": "^1.4.1",
    "babel-core": "^6.26.3",
    "babel-loader": "^7.1.5",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "backstop-twentytwenty": "^1.0.4",
    "eslint": "^5.6.1",
    "eslint-config-semistandard": "^12.0.1",
    "eslint-config-standard": "^12.0.0",
    "eslint-plugin-import": "^2.14.0",
    "eslint-plugin-node": "^7.0.1",
    "eslint-plugin-promise": "^4.0.1",
    "eslint-plugin-react": "^7.11.1",
    "eslint-plugin-standard": "^4.0.0",
    "file-loader": "^2.0.0",
    "lint-staged": "^4.3.0",
    "mocha": "^5.2.0",
    "mockery": "^1.4.0",
    "prettier": "^1.14.3",
    "prettier-eslint-cli": "^4.7.1",
    "react": "^15.6.1",
    "react-dom": "^15.6.1",
    "react-modal": "^3.0.3",
    "react-redux": "^5.0.6",
    "react-sticky": "^6.0.1",
    "react-toggle-button": "^2.1.0",
    "react-visibility-sensor": "^3.11.1",
    "redux": "^3.7.2",
    "sinon": "^1.17.7",
    "styled-components": "^2.1.2",
    "url-loader": "^1.1.1",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.3.1"
  },
  "dependencies": {
    "chalk": "^1.1.3",
    "diverged": "^0.1.3",
    "fs-extra": "^0.30.0",
    "jump.js": "^1.0.2",
    "junit-report-builder": "^1.3.3",
    "lodash": "^4.17.11",
    "merge-img": "^2.1.3",
    "minimist": "^1.2.0",
    "node-resemble-js": "^0.2.0",
    "object-hash": "1.1.5",
    "opn": "^5.3.0",
    "os": "^0.1.1",
    "p-map": "^1.1.1",
    "path": "^0.12.7",
    "portfinder": "^1.0.17",
    "puppeteer": "^2.0.0",
    "resolve": "^1.11.1",
    "super-simple-web-server": "^1.1.2",
    "temp": "^0.8.3"
  }
}

puppeteer のバージョンは…… 2.0.0 ?!先ほど入れた最新版は 5.5.0 だったのでかなりバージョンに乖離があります。

ということで先程の Puppeteer のテストプロジェクトの package.json を直します。

{
  "name": "puppeteer_test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "puppeteer": "^2.0.0"
  }
}

はい。 2.0.0 です。これで yarn し直して node index.js を実行。

> node index.js
(node:28172) UnhandledPromiseRejectionWarning: Error: Failed to launch the browser process!


TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md

    at onClose (PATH\TO\TEST_PROJECT\puppeteer_test\node_modules\puppeteer\lib\Launcher.js:750:14)      
    at ChildProcess.<anonymous> (PATH\TO\TEST_PROJECT\puppeteer_test\node_modules\puppeteer\lib\Launcher.js:740:61)
    at ChildProcess.emit (events.js:326:22)
    at Process.ChildProcess._handle.onexit (internal/child_process.js:276:12)

Error: Failed to launch the browser process! のエラーが出て、 BackstopJS のプロジェクトで出た内容を再現できました。

エラーの発生個所も node_modules\puppeteer\lib\Launcher.js:750:14 以下同じです。

念のための確認

エラー文の中にあるトラブルシューティングなど、いくつかの記事で「 puppeteer.launch() のオプションで ignoreDefaultArgs: ['--disable-extensions'] を指定すると良い」と書かれていたので、一応やってみます。

const puppeteer = require('puppeteer');

(async () => {
  //const browser = await puppeteer.launch();
  const browser = await puppeteer.launch({
    ignoreDefaultArgs: ['--disable-extensions'],
  });

  const page = await browser.newPage();
  await page.goto('https://google.com/');
  await page.screenshot({path: 'screenshot.png'});

  await browser.close();
})();

修正して、実行。

> node index.js
(node:31444) UnhandledPromiseRejectionWarning: Error: Failed to launch the browser process!


TROUBLESHOOTING: https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md

    at onClose (PATH\TO\TEST_PROJECT\puppeteer_test\node_modules\puppeteer\lib\Launcher.js:750:14)      
    at ChildProcess.<anonymous> (PATH\TO\TEST_PROJECT\puppeteer_test\node_modules\puppeteer\lib\Launcher.js:740:61)
    at ChildProcess.emit (events.js:326:22)
    at Process.ChildProcess._handle.onexit (internal/child_process.js:276:12)

はい、変わりませんでした。

検証2 (成功)

以上より、 Puppeteer のバージョンをどうにかすれば BackstopJS が動きそうなことが分かりました。

そこで、 Yarn の依存関係の解決オプションの resolutions を試してみることにします。

    // 略
    "devDependencies": {
        "backstopjs": "^5.0.6",      // 追加
        "browser-sync": "^2.26.12",
        // 略
    },
    "resolutions": {
        "puppeteer": "^5.5.0"        // 追加
    },
    // 略

これで yarn し直して、念のため backstop_dataディレクトリ と backstop.json を削除して backstop init をやり直し、再度 urlhttps://localhost:3000/ に直した上で、 backstop reference を実行。

> backstop reference
BackstopJS v5.0.6
Loading config:  PATH\TO\TEST_PROJECT\randa_back_stopper\backstop.json

COMMAND | Executing core for "reference"
  clean | backstop_data/bitmaps_reference was cleaned.
createBitmaps | Selected 1 of 1 scenarios.
CREATING NEW REFERENCE FILE
CREATING NEW REFERENCE FILE
Cookie state restored with: [
  {
    "path": "/",
    "name": "yourCookieName",
    "value": "yourCookieValue",
    "expirationDate": 1798790400,
    "hostOnly": false,
    "httpOnly": false,
    "secure": false,
    "session": false,
    "sameSite": "no_restriction",
    "url": "https://.www.yourdomain.com"
  }
]
Cookie state restored with: [
  {
    "path": "/",
    "name": "yourCookieName",
    "value": "yourCookieValue",
    "expirationDate": 1798790400,
    "hostOnly": false,
    "httpOnly": false,
    "secure": false,
    "session": false,
    "sameSite": "no_restriction",
    "url": "https://.www.yourdomain.com"
  }
]
Browser Console Log 0: JSHandle:BackstopTools have been 
installed.
SCENARIO > BackstopJS Homepage
x Close Browser
Browser Console Log 0: JSHandle:BackstopTools have been 
installed.
SCENARIO > BackstopJS Homepage
x Close Browser

Run `$ backstop test` to generate diff report.

      COMMAND | Command "reference" successfully executed in [3.752s]

動きました!

リグレッションテストの検証

ようやく backstop reference が動いたので、リグレッションテストを試してみます。

まずは編集前( backstop reference したとき)のページのキャプチャ画像。

編集前のページのキャプチャ画像
編集前のページのキャプチャ画像

次に、 Scss や JS などいくつかパラメータやコードを編集して、 backstop test します。

編集後のページのキャプチャ画像
編集後のページのキャプチャ画像
> backstop test
BackstopJS v5.0.6
Loading config:  PATH\TO\TEST_PROJECT\randa_back_stopper\backstop.json

COMMAND | Executing core for "test"
createBitmaps | Selected 1 of 1 scenarios.
Cookie state restored with: [
  {
    "path": "/",
    "name": "yourCookieName",
    "value": "yourCookieValue",
    "expirationDate": 1798790400,
    "hostOnly": false,
    "httpOnly": false,
    "secure": false,
    "session": false,
    "sameSite": "no_restriction",
    "url": "https://.www.yourdomain.com"
  }
]
Cookie state restored with: [
  {
    "path": "/",
    "name": "yourCookieName",
    "value": "yourCookieValue",
    "expirationDate": 1798790400,
    "hostOnly": false,
    "httpOnly": false,
    "secure": false,
    "session": false,
    "sameSite": "no_restriction",
    "url": "https://.www.yourdomain.com"
  }
]
Browser Console Log 0: JSHandle:Invisible Full-moon!
Browser Console Log 0: JSHandle:BackstopTools have been 
installed.
SCENARIO > BackstopJS Homepage
x Close Browser
Browser Console Log 0: JSHandle:Invisible Full-moon!
Browser Console Log 0: JSHandle:BackstopTools have been 
installed.
SCENARIO > BackstopJS Homepage
x Close Browser
      COMMAND | Executing core for "report"
   See: PATH\TO\TEST_PROJECT\randa_back_stopper\backstop_data\bitmaps_test\yyyymmdd-hhiiss\failed_diff_backstop_default_BackstopJS_Homepage_0_document_0_phone.png
      compare | ERROR { requireSameDimensions: true, size: isDifferent, content: 2.06%, threshold: 0.1% }: BackstopJS Homepage backstop_default_BackstopJS_Homepage_0_document_0_phone.png
   See: PATH\TO\TEST_PROJECT\randa_back_stopper\backstop_data\bitmaps_test\yyyymmdd-hhiiss\failed_diff_backstop_default_BackstopJS_Homepage_0_document_1_tablet.png
      compare | ERROR { requireSameDimensions: true, size: isDifferent, content: 0.70%, threshold: 0.1% }: BackstopJS Homepage backstop_default_BackstopJS_Homepage_0_document_1_tablet.png
       report | Test completed...
       report | 0 Passed
       report | 2 Failed
       report | Writing browser report
       report | Resources copied
       report | Copied json report to: PATH\TO\TEST_PROJECT\randa_back_stopper\backstop_data\bitmaps_test\yyyymmdd-hhiiss\report.json
       report | Copied jsonp report to: PATH\TO\TEST_PROJECT\randa_back_stopper\backstop_data\html_report\config.js
      COMMAND | Executing core for "openReport"
   openReport | Attempting to ping 
   openReport | Remote not found. Opening backstop_data\html_report\index.html
       report | *** Mismatch errors found ***
      COMMAND | Command "report" ended with an error after [1.539s]
      COMMAND | Error: Mismatch errors found.
                    at PATH\TO\TEST_PROJECT\randa_back_stopper\node_modules\backstopjs\core\command\report.js:189:17
                    at processTicksAndRejections (internal/process/task_queues.js:93:5)
      COMMAND | Command "test" ended with an error after [5.431s]
      COMMAND | Error: Mismatch errors found.
                    at PATH\TO\TEST_PROJECT\randa_back_stopper\node_modules\backstopjs\core\command\report.js:189:17
                    at processTicksAndRejections (internal/process/task_queues.js:93:5)

エラーが出ましたね。

BackstopJS のテスト失敗画面
BackstopJS のテスト失敗画面

また、自動的にブラウザも上がってきて上のように差分も表示されます。

BackstopJS のテスト失敗、差分の比較画面
BackstopJS のテスト失敗、差分の比較画面

「DIFF」の画像をクリックした先では backstop reference で撮影したスクリーンショットと backstop test で撮影したスクリーンショットの差分をワイパーのように比較することができます。

BackstopJS の比較画面のデモ

細かく比較できて良いですね。


今度は修正を元に戻して再度 backstop test

> backstop test
BackstopJS v5.0.6
Loading config:  PATH\TO\TEST_PROJECT\randa_back_stopper\backstop.json

COMMAND | Executing core for "test"
createBitmaps | Selected 1 of 1 scenarios.
Cookie state restored with: [
  {
    "path": "/",
    "name": "yourCookieName",
    "value": "yourCookieValue",
    "expirationDate": 1798790400,
    "hostOnly": false,
    "httpOnly": false,
    "secure": false,
    "session": false,
    "sameSite": "no_restriction",
    "url": "https://.www.yourdomain.com"
  }
]
Cookie state restored with: [
  {
    "path": "/",
    "name": "yourCookieName",
    "value": "yourCookieValue",
    "expirationDate": 1798790400,
    "hostOnly": false,
    "httpOnly": false,
    "secure": false,
    "session": false,
    "sameSite": "no_restriction",
    "url": "https://.www.yourdomain.com"
  }
]
Browser Console Log 0: JSHandle:BackstopTools have been 
installed.
SCENARIO > BackstopJS Homepage
Browser Console Log 0: JSHandle:BackstopTools have been 
installed.
SCENARIO > BackstopJS Homepage
x Close Browser
x Close Browser
      COMMAND | Executing core for "report"
      compare | OK: BackstopJS Homepage backstop_default_BackstopJS_Homepage_0_document_0_phone.png
      compare | OK: BackstopJS Homepage backstop_default_BackstopJS_Homepage_0_document_1_tablet.png
       report | Test completed...
       report | 2 Passed
       report | 0 Failed
       report | Writing browser report
       report | Resources copied
       report | Copied json report to: PATH\TO\TEST_PROJECT\randa_back_stopper\backstop_data\bitmaps_test\yyyymmdd-hhiiss\report.json
       report | Copied jsonp report to: PATH\TO\TEST_PROJECT\randa_back_stopper\backstop_data\html_report\config.js
      COMMAND | Executing core for "openReport"
   openReport | Attempting to ping
   openReport | Remote not found. Opening backstop_data\html_report\index.html
      COMMAND | Command "report" successfully executed in [0.698s]
      COMMAND | Command "test" successfully executed in 
[4.135s]

成功です。

BackstopJS のテスト成功画面
BackstopJS のテスト成功画面

今度はテスト成功の画面で、同じスクリーンショットが撮影できていることが確認できます。


ということで、軽くですが BackstopJS が使えることを確認しました。

実際にはシナリオの編集とか色々あるようですが、ひとまず動作確認が取れたということで。

追加検証

動作確認が取れたところで、より実践的になりそうな部分を深堀り。

  • ブラウザサイズ: PCサイズも追加
  • BrowserSync のリロード通知を削除: スクリーンショットのタイミングである/なしで分かれてしまうので一律非表示にしてDIFF検知されないようにする
  • 複数ページの場合:
    • シナリオを追加
    • label をファイル名に使用するので半角英数字で記述、ページごとにユニークな名前にする
  • 縦長ページの場合: 普通に全体のスクリーンショットを撮影する
  • position: fixed; のナビゲーションバー: 上端

何回かテストを試して気になったのはこの辺りですかね。いずれもクリア、問題なさそうです。

BackstopJS のテスト失敗画面 (複数ページテスト実行、Browsersyncの通知非表示、縦長ページ、position:fixed:top;のナビゲーションバーあり)
BackstopJS のテスト失敗画面 (複数ページテスト実行、Browsersyncの通知非表示、縦長ページ、position:fixed:top;のナビゲーションバーあり)

わざと失敗させた場合の画面。

BackstopJS の比較画面
BackstopJS の比較画面

縦長ページでは普通に下までスクロールできます。

BackstopJS のテスト成功画面 (複数ページテスト実行、Browsersyncの通知非表示、縦長ページ、position:fixed:top;のナビゲーションバーあり)
BackstopJS のテスト成功画面 (複数ページテスト実行、Browsersyncの通知非表示、縦長ページ、position:fixed:top;のナビゲーションバーあり)

成功画面。最初のテストのときと同様、特に問題はなさそうです。

リポジトリ

作成したリポジトリを貼っておきます。

参考

BackstopJS

Puppeteer の Error: Failed to launch the browser process! エラー対処

Yarn の reasolutions

この記事を書いた人

アルム=バンド

フロントエンド・バックエンド・サーバエンジニア。LAMPやNodeからWP、Gulpを使ってejs,Scss,JSのコーディングまで一通り。たまにRasPiで遊んだり、趣味で開発したり。