Nightwatch で E2E テストを行う時に webpack-dev-server を自動で起動・停止させる

# 環境

* Node.js＠v15.3.0
* Nightwatch＠v2.0.6
* webpack＠v4.46.0

# やりたいこと

* 実際の e2e テストを行う前に、ローカル環境で e2e テストスクリプトが正しく動作するか、確認したい。
* ターミナルから webpack-dev-server を起動 → Nightwatch起動でもいいが、そうじゃない。
* コマンド１つで webpack-dev-server起動 → Nightwatchによるe2eテスト実行 → webpack-dev-server停止までを自動化したい。

# 結論
以下の様にする

## 前提
* webpack-dev-server 単体で、正しく動作する事
* Nightwatch 単体で、正しく動作する事
* ターゲットブラウザはChromeのみ

## ディレクトリ構成
/build
　webpack.conf.json
/test
　/e2e
　　/specs
　　　test.js
　　nightwatch.conf.js
　　nightwatch.js
　　runner.js

## Nightwatch 周り

### Nightwatch 設定
```javascript:test/e2e/nightwatch.conf.js
const webpackDevConfig = require('../../build/webpack.dev.conf')  // webpack の設定を読み込み
const E2E_DIR = 'test/e2e' // e2e テスト用ディレクトリ

// Nightwatch の設定
// @see https://nightwatchjs.org/guide/configuration/
module.exports = {
  // 基本設定
  src_folders: [`${E2E_DIR}/specs/*.spec.js`],
  output_folder: `${E2E_DIR}/.reports`

  // セレニウムサーバーの設定
  selenium: {
    start_process: true,
    server_path: require('selenium-server').path,
    log_path: `${E2E_DIR}/.selenium-server-log`,
    host: 'localhost',
    port: 4444,
    cli_args: {
      'webdriver.chrome.driver': require('chromedriver').path // Nightwatch 公式のパス指定だとエラーになる
    }
  },

  // テストの設定
  test_settings: {
    default: {
      // 起動 URL を webpack-dev-server のローカルホストに向ける
      launch_url: `http://${webpackConfig.dev.host}:${webpackConfig.dev.port}`,
      silent: true,
      desiredCapabilities: {
        browserName: 'chrome'
      }
    },

    chrome: {
      desiredCapabilities: {
        browserName: 'chrome',
        javascriptEnabled: true,
        acceptSslCerts: true
      }
    }

  }
}
```

### nightwatch ローダー
```javascript:test/e2e/nightwatch.js
// @see https://nightwatchjs.org/guide/running-tests/nightwatch-runner.html
require('nightwatch/bin/runner.js')

```

### nightwatch ランナー
```javascript:test/e2e/runner.js
// @see https://github.com/Marak/colors.js
require('colors') // コンソールを色づける

// @see https://webpack.js.org/concepts/
const webpack = require('webpack')
const merge = require('webpack-merge')
const WebpackDevServer = require('webpack-dev-server')
const webpackDevConfig = require('../../build/webpack.dev.conf') // webpack の設定を読み込み

const E2E_DIR = 'test/e2e' // e2e テスト用ディレクトリ

// 不要なwebpack設定を一時的に上書き
let webpackConfig = merge(webpackDevConfig, {
  devServer: {
    client: {
      logging: 'none',
      overlay: false
    },
    open: false,
    static: {
      watch: false
    }
  },
  stats: 'none' // webpack-dev-server のすべてのログ出力を停止
})

// build
const compiler = webpack(webpackConfig)

let webpackDevServer = new WebpackDevServer({ ...webpackConfig.devServer, open: false }, compiler)

// webpack-dev-server start
webpackDevServer
  .start()
  .then(function () {
    // webpack-dev-server の起動ログ
    console.log(' Successfully '.bgGreen.black +
      ' webpack-dev-server'.blue +
      ' is running on ' +
      `'http://${webpackConfig.devServer.host}:${webpackConfig.devServer.port}'`.cyan)

    // @see https://github.com/moxystudio/node-cross-spawn
    const spawn = require('cross-spawn')

    // Nightwatch の起動。
    // node コマンドライン引数として、nightwatchローダーである nightwatch.js と、
    // nightwatch コマンドラインの引数 --config nightwatch.conf.js を指定する
    // @see https://nightwatchjs.org/guide/running-tests/nightwatch-runner.html
    const runner = spawn('node', [`${E2E_DIR}/nightwatch.js`, '--config', `${E2E_DIR}/nightwatch.conf.js`], { stdio: 'inherit' })

    // 正常終了
    runner.on('exit', function (code) {
      webpackDevServer.stopCallback(() => {
        // webpack-dev-server の停止ログ
        console.log(' Successfully '.bgGreen.black + ' webpack-dev-server'.blue + ' has been stopped.')
        process.exit(code)
      })
    })

    // 異常終了
    runner.on('error', function (err) {
      webpackDevServer.stopCallback(() => {
        // webpack-dev-server の停止ログ
        console.log(' Successfully '.bgGreen.black + ' webpack-dev-server'.blue + ' has been stopped.')
      throw err
    })
  })

```

## Node.js 周り
### package.json
```json:package.json
{
  "scripts": {
    "e2e-dev": "node test/e2e/runner.js"
  }
}
```

## E2E テストスクリプト
```javascript:test/e2e/specs/test.spec.js
// NightWatch API
// @see https://nightwatchjs.org/api/

describe('Test demo', function () {
  it('Run demo', function (browser) {
    browser
      .url(browser.baseUrl)
      .assert.visible('#app')
      .end()
  })
})

```

ここまで。で用意は完了。


## E2E テストの実行結果
* ターミナルから `npm run e2e-dev` を投下
* webpack-dev-server が自動起動。起動完了を待機。
* 起動完了したら、Nightwatch ランナーをコマンドラインで呼び出し
* e2e テストスクリプトを全て実行し終えたら、webpack-dev-server を自動で終了

```bash:console
$ npm run e2e-dev

> node test/e2e/runner.js

 Successfully  started webpack-dev-server on http://localhost:8080

[Test demo] Test Suite
──────────────────────────────────────────────
ℹ Connected to Selenium Server on port 4444 (2903ms).
  Using: chrome (98.0.4758.102) on WINDOWS.


  Running Run demo:
───────────────────────────────────────────────────────────────────────────────────────────────────
  ⠋ Loading url: http://localhost:8080


 DONE  Compiled successfully in 6688ms                                                                                                                 15:59:11

  ℹ Loaded url http://localhost:8080 in 4133ms
  √ Testing if element <#app> is visible (68ms)

OK. 1 assertions passed. (4.698s)
 Successfully  webpack-dev-server stopped.

```

以上