Selenium IDE　で　PageObjectsパターン

この記事はSelenium/Appium Advent Calendar 2016の7日目の記事です。
6日目は「Seleniumでブラウザーと一緒に用意しなきゃいけないアレの更新がめんどいのをどうにかしよう」でした。

#今更？
世間ではすでにselenium3に移行されていると思いますが、seleniumIDEネタです。

## ロケーション指定の抽象化

テストケースからロケーションの具体的な記述はできるだけ分離したいところなのは言うまでもありません。
　webdriver版では　PageObjectsパターンがありますが、SeleniumIDEではどうすればよいでしょう。
そう、UI-Elementです。

なにそれ？美味しいの？？　状態だと思うのでこれについて書いてみます。

## 実例
[日本Seleniumユーザーコミュニティの提供するサンプルページ](http://example.selenium.jp/reserveApp/)のサイトを利用させてもらい実例を見てみましょう。

まずは通常通り自動記録します。
記録ボタンを押してから、任意の値を入れて画面遷移してゆきます。
言うまでもなく、自動的に記録されていきます。

![スクリーンショット 2016-12-05 19.00.54.png](https://qiita-image-store.s3.amazonaws.com/0/51335/b748f960-3fe1-b137-265d-40a9a30aa05b.png)


さて、ここでネタを仕込みます
公式では、オプションメニューの　seleniumCore拡張スクリプトに設定とありますが

![スクリーンショット 2016-12-05 19.02.38.png](https://qiita-image-store.s3.amazonaws.com/0/51335/7df80e53-4140-79af-8a46-765de12f90fa.png)


実行時に読み込まれていれば良いという点と、デバックのしやすさから
記録したテストケースの最初の行に下記を追加します。

```
<tr>
	<td>getEval</td>
	<td>
		var map = new UIMap();
		map.addPageset({
			name:'reservePages'
			,description:'contains elements set val page'
			,pathRegexp:'.*'
		});
		map.addElement('reservePages',{
			name:'reserve_var_year'
			,description:'set year'
			,locator:'id=reserve_year'
		});
		map.addElement('reservePages',{
			name:'reserve_var_month'
			,description:'set month'
			,locator:'id=reserve_month'
		});
		map.addElement('reservePages',{
			name:'reserve_var_day'
			,description:'set day'
			,locator:'id=reserve_day'
		});
		map.addElement('reservePages',{
			name:'reserve_var_term'
			,description:'set term'
			,locator:'id=reserve_term'
		});
		map.addElement('reservePages',{
			name:'reserve_var_headcount'
			,description:'set headcount'
			,locator:'id=reserve_headcount'
		});
		map.addElement('reservePages',{
			name:'plana'
			,description:'button plan'
			,locator:'id=plan_a'
		});
		map.addElement('reservePages',{
			name:'reserve_var_guestname'
			,description:'set name'
			,locator:'id=guestname'
		});
		map.addElement('reservePages',{
			name:'goto_nextpage'
			, description:'link to the next page'
			,locator:'id=goto_next'
		});
		map.addElement('reservePages',{
			name:'reserve_var_price'
			,description:'set price'
			,locator:'id=price'
		});
		map.addElement('reservePages',{
			name:'reserve_go_commit'
			,description:'button commit'
			,locator:'id=commit'
		});
		map.addElement('reservePages',{
			name:'result'
			,description:'result value'
			,locator:'css=h1'
		});
		map.addElement('reservePages',{
			name:'reserve_go_checkInfo'
			,description:'button goto checkinfo page'
			,locator:'id=returnto_checkInfo'
		});
		map.addElement('reservePages',{
			name:'reserve_go_index'
			,description:'button goto index page'
			,locator:'id=returnto_index'
		});

var manager = new RollupManager();

manager.addRollupRule({
    name: 'go_index',
    description: 'goto index page',
    args: [{
        name: 'term',
        description: 'the reserve term'
    }],
    commandMatchers: [{
        command: 'clickAndWait',
        target: 'ui=reservePages::reserve_go_checkInfo\\(.+'
    },{
        command: 'clickAndWait',
        target: 'ui=reservePages::reserve_go_index\\(.+'
    }],
    getExpandedCommands: function (args) {
        var commands = [];
        var uiSpecifier = new UISpecifier(
            'reservePages', 'reserve_var', {
                term: args.term
            });
        commands.push({
            command: 'clickAndWait',
            target: 'ui=reservePages::reserve_go_checkInfo()'
        });
        commands.push({
            command: 'clickAndWait',
            target: 'ui=reservePages::reserve_go_index()'
        });
        return commands;
    }
});


manager.addRollupRule({
    name: 'set_year',
    description: 'set var page',
    args: [{
        name: 'year',
        description: 'the reserve year'
    }],
    commandMatchers: [{
        command: 'type'
        ,target: 'ui=reservePages::reserve_var_year\\(.+'
        ,updateArgs:function(command, args){
			args.year = command.value;
			return args;
		}
    }],
    getExpandedCommands: function (args) {
        var commands = [];
        var uiSpecifier = new UISpecifier(
            'reservePages'
			,'reserve_var'
			,{ term: args.term });
        commands.push({
            command: 'type'
            ,target: 'ui=reservePages::reserve_var_year()'
			,value: args.year
        });
        return commands;
    }
});
	</td>
	<td></td>
</tr>
```

一度実行してみます。もちろん、オールグリーンで実行されます。

さて、このテストケースにもう一つテストケースを追加してみましょう。
操作は、最終行にカーソルを置いた状態で、記録ボタンを押すだけですね。


何ということでしょう！（古）今度の自動記録されたコマンドのロケーション指定がＵＩマップによる指定になっています。
実行も問題なしです。

![スクリーンショット 2016-12-05 19.18.06.png](https://qiita-image-store.s3.amazonaws.com/0/51335/f484d53b-da57-c2d4-5eb9-4edbd7997ca2.png)


コンテキストメニューにも反映されています。
![スクリーンショット 2016-12-05 19.07.30.png](https://qiita-image-store.s3.amazonaws.com/0/51335/57a2a3b2-fced-9f33-dc3c-62bbf3fafd2d.png)



このようにテストケースからロケーション指定の分離が簡単にできるわけです。
設計の段階でＩＤが確定していれば、それを元にＵＩマップによるエレメント指定を先行して作成しておき、画面が出来上がってから自動記録によるシナリオ作成が可能ということですね。

#One more Thing.


IDEの画面を良く見るといつもと異なる点に気づかれたでしょうか？。それは・・・

![スクリーンショット 2016-12-05 20.22.01.png](https://qiita-image-store.s3.amazonaws.com/0/51335/931a958e-f130-774a-d87b-24184f5332c0.png)

わかりましたか？



ちなみに、通常はこちら
![スクリーンショット 2016-12-05 20.20.48.png](https://qiita-image-store.s3.amazonaws.com/0/51335/828d9f22-9d17-4185-4570-6094b5bf42fb.png)



そう！これです、これ！！
![スクリーンショット 2016-12-05 19.19.50.png](https://qiita-image-store.s3.amazonaws.com/0/51335/5e14675f-da5a-23c5-f4e7-0dbf3e306c08.png)


SeleniumIDEを利用している人で、このボタンを押したことがある人がどのくらいるの疑問ですが
これが、アクティブになっています！

これを押すとどうなるのか？
せっかくなので押してみましょう。
すると・・・・


１６行目を書き換える替えるけどいいか？的な確認

![スクリーンショット 2016-12-05 19.21.13.png](https://qiita-image-store.s3.amazonaws.com/0/51335/957f1188-731c-32dd-eb37-44c35bc8b573.png)

さらに26行目と２７行目を書き換えるけどいいか？的な確認

![スクリーンショット 2016-12-05 19.21.41.png](https://qiita-image-store.s3.amazonaws.com/0/51335/db062eb2-43ac-9e0f-cbb8-29698ea690ff.png)

結果これまたほとんどの人が使ったことがないだろうと思われる、rollupコマンドに書き換わっています。

![スクリーンショット 2016-12-05 19.21.53.png](https://qiita-image-store.s3.amazonaws.com/0/51335/99904c6c-bdcd-fdbe-a163-8f3dcde915ec.png)

この機能、最初の置換は１コマンドを対象としているのであまり意味がありませんが２つ目の置換は２つのコマンドが１コマンドになっているのがわかると思います。
つまり、操作を束ねてブラックボックス化してしまえるわけです。

固定的操作をまとめてわかりやすい名前をつけておくことでテストケースの見通しを良くすることができます。
また、実行例で分かるとおり、熟練者がUI-elementsと合わせて作成しておけば、手動で記録したテストケースを自動的に変換するできるので作成のハードルも低くなりますね。

下記は、スクリーンショットを隠蔽してみたケースです。

```
<tr>
<td>getEval</td>
<td>
 var map = new UIMap();
 map.addPageset({
     name: 'allpage',
     description: 'All pages',
     pathRegexp: '(/.+)?'
 });

 var manager = new RollupManager();
 manager.addRollupRule({
     name: 'do_PageScreenshot',
     description: 'take page screenshot.',
     args: [{
         name: 'switch',
         description: 'debugswitch',
         exampleValues: [ 'on', 'off']
     }, {
         name: 'url',
         description: 'terget Page url'
     }]
    ,commandMatchers: {
     command:'captureEntirePageScreenshot',
     target: 'ss\.png$',
    }
 ,getExpandedCommands: function (args) {
     var timestmp = new Date().getTime();
     var commands=[];
     var filename = 'c:' + timestmp + '.png';
     if (args.switch) {
         if (args.url) {
             commands.push({
                 command: 'open'
                ,terget: args.url
                ,values: ''
             });
         }
         commands.push({
             command: 'captureEntirePageScreenshot',
             terget: filename,
             values: ''
         })
     };
     return commands;
 }
 });
</td>
<td></td>
</tr>
<tr>
　<td>rollup</td>
　<td>do_PageScreenshot</td>
　<td>url=,switch=ON</td>
</tr>
```


##最後に

と、今更ながら（実は下書き状態で3年ほど眠っていました）に書きましたが、[こちらの記事](http://qiita.com/hiroshitoda/items/7903c07b076e1d394e5f)にある通り
１年先に　SeleniumIDEが生き残っているかは今の所わかりません。


#お約束
webdriver版に移行しましょうね、というお話です。

個人的には　SeleniumIDEが必要なシーンもあると思っていますので是非　WebExtensionsベースに移行していただきたいのですが・・・