AngularJS 用 e2e テストフレームワーク Protractor のチュートリアルをやってみる

[Potractor のチュートリアル](https://github.com/angular/protractor/blob/master/docs/tutorial.md) をやってみます。

まずは npm で Protractor をインストールします。

```zsh
npm install -g protractor
```

Selenium サーバのインスタンスを作るヘルパーツール `webdriver-manager` を使うために下記のコマンドでバイナリをダウンロードします。

```zsh
webdriver-manager update
```

Selenium サーバを起動します。

```zsh
webdriver-manager start
```

ブラウザで [http://localhost:4444/wd/hub](http://localhost:4444/wd/hub) にアクセスすると Selenium サーバのステータス情報が確認できます。

## テストを書く

まずは Spec ファイルと Configuration ファイルを用意します。ここでは、 [http://juliemr.github.io/protractor-demo/](http://juliemr.github.io/protractor-demo/) の Super Calculator をテストする Spec ファイルを用意します。`spec.js`というファイルを作成して、下記のように記述します。

```js:spec.js
describe('Protractor のデモページ', function() {
  it('は、Super Calculator というタイトルが設定されている', function() {
    browser.get('http://juliemr.github.io/protractor-demo/');

    expect(browser.getTitle()).toEqual('Super Calculator');
  });
});
```

次に Configuration ファイルを書きます。`conf.js` というファイルを作成して下記のコードを書きます。

```js:conf.js
exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['spec.js']
}
```

今書いたテストを実行します。

```zsh
protractor conf.js
```

デフォルトで設定されているブラウザとして Chrome が起動してテストが実行されます。 

## 要素とインタラクションしてテストする

次はドキュメント内の要素とインタラクションしてテストします。`spec.js` を下記のように変更します。

```js:spec.js
describe('Protractor のデモページ', function() {
  it('は、入力された 1 と 2 を足して 3 を出力する', function() {
    browser.get('http://juliemr.github.io/protractor-demo/');
    element(by.model('first')).sendKeys(1);
    element(by.model('second')).sendKeys(2);

    element(by.id('gobutton')).click();

    expect(element(by.binding('latest')).getText()).toEqual('3');
  });
});
```

Protractor に 任意の DOM 要素を取得するように伝える Locator である `by` オブジェクトを使います。 

ここで使っている Locator は

* `by.model('name')`
  * `ng-model` に `name` が設定された要素を取得
* `by.id('name')`
  * id に `name` が設定された要素を取得
* `by.binding('name')`
  * `name` 変数にバインドされている要素を取得

## 複数要素を扱う

`element` 関数は単一の要素を扱いますが、`element.all` を使うと複数の要素を取得することができるので、`ng-repeat` で複数生成された要素に対してもテストを実行できます。

```js:spec.js
describe('Protractor のデモページ', function() {
  var firstNumber = element(by.model('first'));
  var secondNumber = element(by.model('second'));
  var goButton = element(by.id('gobutton'));
  var latestResult = element(by.binding('latest'));
  var history = element.all(by.repeater('result in memory'));

  function add(first, second) {
    firstNumber.sendKeys(first);
    secondNumber.sendKeys(second);
    goButton.click();
  }

  beforeEach(function() {
    browser.get('http://juliemr.github.io/protractor-demo/');
  });

  it('は、履歴を残す', function() {
    add(1, 2);
    add(3, 4);

    // 履歴が 2 つ残っている
    expect(history.count()).toEqual(2);

    add(5, 6);

    // 履歴が 1 つ追加されて 3 つ残っている
    expect(history.count()).toEqual(3);

    // 一番古い履歴は 1 + 2 を計算したときのもの
    expect(history.last().getText()).toContain('1 + 2');

    // 最新の履歴は 5 + 6 を計算したときのもの
    expect(history.first().getText()).toContain('5 + 6');
  });
});
```

`ng-repeat`、`ng-repeat-start` と `ng-repeat-end` で生成された要素を取得する Locator は `by.repeater` です。

ここではインタラクション後の履歴リストの数を取得したり、最初と最後の要素を `first()` と `last()` メソッドで取り出して、各々テストしています。

## テストに使用するブラウザを変更する

`conf.js` での `capabilities` を追加するとテストに使用するブラウザを変更できます。

```js:conf.js
exports.config = {
  seleniumAddress: 'http://localhost:4444/wd/hub',
  specs: ['spec.js'],
  capabilities: {
    browserName: 'firefox'
  }
}
```

変更後に `protractor conf.js` すると、今度は Firefox が起動してテストが実行されます。