プロジェクト専用のDSLで,効率的にIEを自動操作する(WSH/JScript)
以下のエントリでは,JScriptでブラウザのオートメーションを行なうための基礎を述べた。
JScript / VBScript (WSH)で,IEを自動操作しよう
http://d.hatena.ne.jp/language_and_engineering/20090713/p1
その続編として,プロジェクト内でオートメーションスクリプトを使うための実用的なファイル構成を述べる。
「プロジェクトに特化した便利メソッド(DSL)を作って,楽にブラウザを動かそう」というコンセプト。
概要
ファイル構成:
- 実行.bat
- ie.wsfを起動するためのバッチ。
- ie.wsf
- プロジェクトの機能を実行するファイル。
- eg. 具体的なユーザ名を利用したログインなど。
- lib_site.js
- プロジェクトの機能を定義するファイル。
- eg. ログインなど。
- lib_ie.js
- ブラウザの機能を定義するファイル。
- eg. DOM操作など。
上の階層が下の階層を呼び出している。
4には,IEを自動操作するための便利メソッドをたくさん詰め込んでおく。
普段はあまり編集しない。
3には,対象プロジェクトのWebアプリケーションを操作するための便利メソッドを書く。
いわば,ユースケースの雛形。
プロジェクトの機能を利用するためのDSLとも言える。
2は,3と4を呼び出している。
3でメソッドを定義しておけば,2では,多数の具体的なユースケースを簡潔に記述できる。
※ユースケースを直接wsf上に記述するのではなく,外部jsファイルに切り分けてもOK。wsfはそれを呼び出せばよい。
特に,コード量が増えてきたらそうする必要がある。
1は,2をキックするだけ。wsfの実行方法がわからない,という人もいるから。
具体的なファイル内容のサンプルは下記の通り。
(1)キック用のバッチ
実行.bat
cscript ie.wsf
(2)ユースケース
ie.wsf
<job> <!-- IE自動操作のためのライブラリ --> <script language="JavaScript" src="lib_ie.js"></script> <!-- プロジェクトの機能を利用するためのライブラリ --> <script language="JavaScript" src="lib_site.js"></script> <!-- プロジェクトの機能の呼び出しを以下に記述します。 --> <script language="JavaScript"> var ie = new IE(); open_top_page(); replace_body_html(); // 検索実行 search_by_keyword( "hoge" ); search_by_keyword( "fuga" ); search_by_keyword( "boo" ); ie.quit(); </script> </job>
(3)ユースケース雛形
lib_site.js
// プロジェクトに特化したメソッドを記述します。 // トップページを開く function open_top_page() { ie.goto_url( "http://www.yahoo.co.jp" ); } // 画面上の表示を加工する function replace_body_html() { ie.exec_js(function(){ document.body.innerHTML = document.body.innerHTML.replace( /ー/g, "━━━(+A+)━━━" ); focus(); }); ie.sleep( 1000 ); } // キーワード検索 function search_by_keyword( kwd ) { open_top_page(); ie.type( "srchtxt", kwd ); ie.click_and_wait( "srchbtn" ); ie.sleep( 1000 ); }
具体的にどういうキーワードで検索するのか,という情報は,(3)ではなく(2)に書く。
(4)ブラウザ操作用のライブラリ
lib_ie.js
// IE自動操作のためのクラス var IE = function( obj ){ this._build( obj ); }; IE.prototype = { // ブラウザオブジェクト _ie : null, // ---------- セットアップ系 ---------- // 初回セットアップ _build : function( obj ){ this._create_new_ie(); var default_url = "about:blank"; if( obj && obj[ "url" ] ) { default_url = obj[ "url" ]; } this.goto_url( default_url ); this.set_visible( true ); } , // 新規IEをセット _create_new_ie : function(){ this._ie = WScript.CreateObject( "InternetExplorer.Application" ); } , // 可視状態 set_visible : function( bool ){ this._ie.Visible = bool; } , // 終了 quit : function(){ this._ie.Quit(); this._ie = null; } , // ---------- システム待機系 ---------- // ビジー状態の間待つ wait_while_busy : function(){ var timeout_ms = 10000; var step_ms = 100; var total_waited_ms = 0; while( this.is_busy() ) { this.sleep( step_ms ); // タイムアウトか? total_waited_ms += step_ms; if( total_waited_ms >= timeout_ms ) { this.debug( "警告:タイムアウトのため,リロードします。(" + this.get_current_url() + ")" ); this.reload(); break; } } this.sleep( 500 ); } , // ビジー状態か判定 is_busy : function(){ return ( ( this._ie.Busy ) || ( this._ie.readystate != 4 ) ); } , // 指定ミリ秒だけ待機 sleep : function( ms ){ WScript.Sleep( ms ); } , // ---------- アドレスバー関連 ---------- // ページを移動 goto_url : function( url ){ this._ie.Navigate( url ); this.wait_while_busy(); } , // ページをリロード reload : function(){ this._ie.document.location.reload( true ); // http://www.microsoft.com/japan/technet/scriptcenter/resources/qanda/sept05/hey0927.mspx this.wait_while_busy(); } , // 現在表示中のページのURL get_current_url : function() { return this._ie.LocationURL; // http://blog.livedoor.jp/programlog/archives/298228.html } , // 受け取ったJavaScript関数オブジェクトをブラウザ上で実行します exec_js : function( func ) { var str_address = "javascript:(" + func.toString() + ")();void(0);"; //this.debug( str_address ); // toSource()のかわりにtoString()を // http://blog.livedoor.jp/dankogai/archives/50957994.html this._ie.Navigate( str_address ); } , // ---------- DOM操作 ---------- // セーフな要素取得 $ : function( dom_id ){ // 10秒までは待ってあげる this.wait_for_element_present( dom_id, 10000 ); return this.gid( dom_id ); } , // IDで要素取得 gid : function( dom_id ){ return this._ie.document.getElementById( dom_id ); } , // 存在判定 is_element_present : function( dom_id ){ return ( this.gid( dom_id ) != null ); } , // 存在待ち wait_for_element_present : function( dom_id, ms_timeout ) { var ms_spent = 0; while( true ) { // 要素が現れたか? if( this.is_element_present( dom_id ) ) { break; } else { ms_spent += 100; this.sleep( 100 ); } // タイムアウトか? if( ms_timeout <= ms_spent ) { this.debug( dom_id + "が存在しません。" ); break; } } return; } , // 入力 type : function( dom_id, value ){ this.$( dom_id ).value = value; } , // クリック click : function( dom_id ){ this.$( dom_id ).click(); } , // クリックして待機 click_and_wait : function( dom_id ){ this.click( dom_id ); this.wait_while_busy(); } , // セレクトボックス(文言ベース) select_by_label : function( dom_id, target_label ) { var opts = this.$( dom_id ).options; for( var i = 0; i < opts.length; i ++ ) { if( "" + opts[i].innerText == "" + target_label ) { opts[i].selected = true; } } this.$( dom_id ).fireEvent( "onchange" ); } , // ---------- デバッグ用 ---------- debug : function( str ) { WScript.Echo( str ); } };
解説:
- InternetExplorer.ApplicationというActiveXオブジェクトそのものを拡張することは不可能。だからかわりに,そのオブジェクトをフィールドに持つようなラッパークラスを定義して,やりたい放題にメソッドを追加する。つまりコンポジション。
- exec_jsは,クロージャを受け取ってブラウザ上でブックマークレットとして実行してくれる。例えば,wsf側では alert() という関数が定義されていなくても,wsf内のクロージャ内に alert() と書くことができる。クロージャはすばらしい。
- ブラウザの自動操作につきものの問題は,タイムアウトだ。しかし,このコードなら,タイムアウト発生を検出した時点でページは自動的にリロードされ,オートメーションは中断しない。
補足
Selenium RCのテストスクリプトと似たイメージ。
Part4 テストを自動化する注目のツールSelenium:こまめなテストがバグ早期発見の決め手に
http://itpro.nikkeibp.co.jp/article/COLUMN/20071011/284284/?ST=develop&P=3一番下の test会員登録() というメソッドを参考に
本稿は,Webアプリケーションへのテストデータ投入のような定型操作などに利用可。