browser-envを使ってブラウザに固有のグローバル変数をNode.jsに用意する

# 要旨

- ブラウザに固有のグローバル変数（`window`, `document`など）[^1] を使うコードをNode.jsで走らせるために `browser-env` を使う
- `ava` を使ったテストに `browser-env` を使用したサンプルを紹介する

[^1]: [AVA](https://github.com/avajs/ava/blob/master/docs/recipes/browser-testing.md) で *"browser specific globals"* と呼んでいるもの

# 背景・課題

ブラウザで動かすことを前提に書いたJavaScriptをNode.jsで走らせたいことがある。例えばユニットテストがそうだ。

次のようなサンプルを用意した。

- https://github.com/h-kanazawa/try-browser-env

このサンプルはlocationのhashを表示する、という操作を実行している（テストのために遠回りに書いている）。ビルドされたファイルをブラウザで開くと期待通り動作する確認できる。

しかし次のような関数を [AVA](https://github.com/avajs/ava) を使ってユニットテストを実行した時に問題が発生した。

```js:src/url.js
const getUrlHash = href => {
  return new URL(href).hash;
};

export { getUrlHash as default };
```

テストコードは次の通りだ。

```js:test/url.test.js
import test from 'ava';
import getUrlHash from '../src/url';

test('getUrlHash should return the hash of the URL', t => {
  const actual = getUrlHash('http://example.com/path/to?key=value#xyz');
  const expected = '#xyz';
  t.is(expected, actual);
});
```

残念ながらブラウザで問題なく動いたこのコードも、ava実行時は `URL` が定義されていないとしてエラーになってしまう。

```js:ava
$ ava

1 failed

url › getUrlHash should return the hash of the input URL

/path/to/try-browser-env/src/url.js:7

Error thrown in test:

ReferenceError {
  message: 'URL is not defined',
}

getUrlHash (src/url.js:7:3)
Test.fn (test/url.test.js:5:18)
```

原因はNode.js環境とブラウザ環境の違いである。Node.jsには `URL` という関数はグローバルに定義されていないが、（一部の）ブラウザにおいては [`URL` は定義されている](https://developer.mozilla.org/ja/docs/Web/API/URL)。

```js:node
// node cliで確認する
$ node
> URL
ReferenceError: URL is not defined
...
```

```js:chrome-developer-tool/console
// ChromのDeveloper Toolのコンソールで確認する
> URL
> ƒ URL() { [native code] }
```

# 解決方法

AVAによると [browser-env](https://github.com/lukechilds/browser-env) を使うことで解決できる。使い方は [AVAのレシピ](https://github.com/avajs/ava/blob/master/docs/recipes/browser-testing.md) にあるがポイントだけ掻い摘んでおこう。

### インストール

```
$ npm install --save-dev browser-env
// or
$ yarn add --dev browser-env
```

### セットアップ

test/helpers/ ディレクトリに以下のようなファイルを用意する。

```js:test/helpers/setup-browser-env.js
import browserEnv from 'browser-env';

browserEnv();
```

package.jsonの `ava.require` にこのヘルパーファイルを設定する。

```json:package.json
{
  "ava": {
    "require": [
      "./test/helpers/setup-browser-env.js"
    ]
  }
}
```

これでAVAのテスト実行時にブラウザ環境を模倣することができたので `URL` の　`ReferenceError` になることはない。もちろん *src/* ディレクトリのファイルは変更しておらず、ブラウザでも問題なく動く。

#### ※補足

上記のように browserEnv への引数を空にすると全てのブラウザ固有のグローバル変数がNode.jsのグローバルスコープに追加されてしまう。ブラウザ環境を模倣するには良いが、把握していない/必要としないグローバル変数を作るのは健全ではない。使用するものだけに限定すると良いだろう。

```js:test/helpers/setup-browser-env.js
import browserEnv from 'browser-env';

browserEnv(['URL']);
```

また個別のテストファイルに対してブラウザ固有のグローバル変数を導入したいときは、各テストファイルに記載しても問題ない。

```js:test/url.test.js
import test from 'ava';
import getUrlHash from '../src/url';
import browserEnv from 'browser-env';
browserEnv(['URL']);

test('getUrlHash should return the hash of the input URL', t => {
  const actual = getUrlHash('http://example.com/path/to?key=value#xyz');
  const expected = '#xyz';
  t.is(expected, actual);
});
```

# その他のブラウザ変数

browser-envが用意するのは `URL` だけではない。 `document` や `location` などのブラウザ固有のグローバル変数もモックしてくれる。

```js:node
import browserEnv = require('browser-env');
browserEnv(['location'], {url: "http://example.com/path/to?key=value#xyz"});

location.pathname
//'/path/to'
location.hash
// '#xyz'
```

browser-envは [jsdom](https://github.com/tmpvar/jsdom) を使っているので、`browserEnv` の第2引数の詳細は `JSDOM` を参考にするとよい。

# 参考

- [サンプルコード](https://github.com/h-kanazawa/try-browser-env)
- [AVAにおけるブラウザテストの設定方法](https://github.com/avajs/ava/blob/master/docs/recipes/browser-testing.md)
- [browser-env](https://github.com/lukechilds/browser-env)
- [jsdom](https://github.com/tmpvar/jsdom)