スポンサーリンク

ブラウザ上で,マウスのドラッグ&ドロップも自動化できるスクリプト(UWSCでIEを自動操作するライブラリ 1.2)


※これより新しいバージョンがリリースされています。
http://d.hatena.ne.jp/language_and_engineering/20090918/p1


下記のライブラリを改良した。

UWSCでIEを自動操作し,回帰テスト/JavaScript実行/ファイル保存 などができるライブラリ
http://d.hatena.ne.jp/language_and_engineering/20090825/p1

変更点:

  • マウスで,要素をドラッグ&ドロップする事が自動化可能に。


JavaScriptではマウス座標を操作することが不可能なので,今まで,複雑なUIをブラウザ上で回帰テストすることは不可能だった。



しかし今回のUWSC用ライブラリを使って,下のようなコードを書けば,3つのdiv要素を順番にドラッグドロップすることが可能。



hoge.uws

call .\IEManipulation.uws

_ie = IE.new()
IE.show( _ie )
IE.jump_with_js( _ie, "http://localhost/a.html" )

// ドラッグドロップ開始
// 要素のIDと,移動距離(x,y)と,所要時間を指定
IE.drag_drop( _ie, "fuga1", 100, 200, 3 )
IE.drag_drop( _ie, "fuga2", 200, 50, 3 )
IE.drag_drop( _ie, "fuga3", 40, 300, 3 )

これだけ。

UWSC.exeにこのファイルをドラッグドロップして動作が開始する。



テスト対象のWebページ:

a.html

<html>
<head></head>
<body>

<br>
<br>
<br>

<div id="fuga1" style="width:100px;height:100px;background-color:green">fuga1</div>
<div id="fuga2" style="width:100px;height:100px;background-color:red">fuga2</div>
<div id="fuga3" style="width:100px;height:100px;background-color:yellow">fuga3</div>

<script language="JavaScript" src="prototype.js"></script>
<script language="JavaScript" src="scriptaculous.js"></script>
<script language="JavaScript" src="effects.js"></script>
<script language="JavaScript" src="dragdrop.js"></script>
<script language="JavaScript">

// ドラッグドロップ可能にする
new Draggable("fuga1");
new Draggable("fuga2");
new Draggable("fuga3");

</script>

</body>
</html>


このためのライブラリ:



IEManipulation.uws

//
//  IEを自動操作するためのライブラリ ver 1.21
//


class IE

  // -------------------- 制御 --------------------


  // 新規IEオブジェクトを作成して返す
  function new()
    result = createOLEobj("InternetExplorer.Application")
  fend


  // 起動中のIEを見えるように
  procedure show( browser )
    browser.visible = True
    
    // 位置調整
    wid = hndtoid( browser.hwnd )
    acw( wid, 0, 0 )
    
    // 最大化
    ctrlwin( wid, MAX )
    
    // マウス相対座標をこのブラウザ基準に設定
    MouseOrg( wid )
    
    pause( browser )
  fend


  // IEがビジー状態の間待ちます
  procedure wait( browser )
    repeat
      sleep( 0.1 )
    until ( ! browser.busy ) and ( browser.readystate = 4 )
    pause( browser )
  fend


  // URLにジャンプ
  procedure jump( browser, url )
    browser.navigate( url )
    wait( browser )
  fend


  // ポーズ
  procedure pause( browser )
    sleep( 0.2 )
  fend


  // 要素が出現するまで待ちます
  procedure wait_for_element_present( browser, dom_id, timeout_sec )
    interval_sec = 0.2
    total_wait_sec = 0
    loop_flag = True

    while loop_flag
    
      // 要素は現れたか
      ifb browser.document.getElementById( dom_id ) = Nothing then
        // 出現していないのでスリープ
        sleep( interval_sec )
        total_wait_sec = total_wait_sec + interval_sec
      else
        // 出現したのでループ終了
        loop_flag = false
      endif
    
      // タイムアウトか
      ifb total_wait_sec > timeout_sec then
        msgbox( "element '" + dom_id + "' did not appear." )
        exitexit
      endif
    wend
  
  
  fend

  // -------------------- DOM操作 --------------------


  // IDが渡された場合はDOM要素にして返します
  function to_elem( browser, locator )
    ifb VarType( locator ) = 8 then
      // 変数の型が文字列の場合はDOM IDとみなす
      result = gid( browser, locator )
    else
      // それ以外の場合はスルー
      result = locator
    endif

      // VarTypeのヘルプ:http://msdn.microsoft.com/ja-jp/library/cc392346.aspx
  fend


  // $
  function gid( browser, dom_id )
    result = browser.document.getElementById( dom_id )
  fend


  // 入力
  procedure type( browser, locator, str )
    elem = to_elem( browser, locator )
    elem.value = str
    pause( browser )
  fend


  // クリック
  procedure click( browser, locator )
    elem = to_elem( browser, locator )
    elem.click
    pause( browser )
  fend


  // クリックして待機
  procedure click_and_wait( browser, locator )
    click( browser, locator )
    wait( browser )
  fend


  // 文言ベースでセレクトボックスを選択
  procedure select_by_label( browser, locator, label )
    elem = to_elem( browser, locator )
    for i = 0 to elem.options.length - 1
      ifb elem.options[ i ].innerText = label then
        elem.options[ i ].selected = True
      endif
    next
    pause( browser )
  fend


  // 値ベースでセレクトボックスを選択
  procedure select_by_value( browser, locator, val )
    elem = to_elem( browser, locator )
    for i = 0 to elem.options.length - 1
      ifb elem.options[ i ].Value = val then
        elem.options[ i ].selected = True
      endif
    next
    pause( browser )
  fend


  // indexベースでセレクトボックスを選択
  procedure select_by_index( browser, locator, index )
    elem = to_elem( browser, locator )
    elem.options[ index ].selected = True
    pause( browser )
  fend


  // ファイルアップロード
  // DOM IDではなくnameで要素を指定するので注意
  procedure file_upload( browser, post_name, file_path )
    IESetData( browser, file_path, post_name )
    pause( browser )
  fend


  // -------------------- JavaScriptの制御 --------------------


  // URLにジャンプし,WebページにJSコードを注入する
  procedure jump_with_js( browser, url )
    
    IE.jump( browser, url )
    IE.create_js_proxy( browser )
    
  fend


  // Webページ中にJS経由用のオブジェクトを生成して返す
  procedure create_js_proxy( browser )
    doc = browser.document
    
    TextBlock js_proxy_code
    
// UWSCからコード注入するためのオブジェクト
document._uwsc_proxy = {
  global : this,
  _window : window,
  eval_code : function( str ){
    try{
      return eval( str );
    }catch( e ){
      // エラーメッセージを表示
      alert( e.number + " : " + e.description );
      return null;
    }
    
  }
};
    
    endTextBlock
    
    // 生成
    elem_s = doc.createElement("script")
    elem_s.text = js_proxy_code;
    elem_s.type = "text/javascript";
    
    // 注入
    doc.getElementsByTagName("head").Item(0).appendChild( elem_s );

  fend


  // 文字列をJSコードとしてブラウザ側で評価
  procedure export_js( browser, str_jscode )
    browser.document._uwsc_proxy.eval_code( str_jscode )
  fend


  // 文字列をJSコードとして評価した結果をUWSC側へ読み込み
  function import_js( browser, str_jscode )
    // いったん文字列をブラウザ側にexportし,その結果をUWSC側にimport
    result = browser.document._uwsc_proxy.eval_code( str_jscode )
  fend


  // -------------------- ファイルダウンロード用 --------------------


  // ダイアログが現れるまで待機
  procedure wait_for_dialog( dialog_title, timeout_sec )
    interval_sec = 0.2
    total_wait_sec = 0
    loop_flag = True

    while loop_flag
    
      // ダイアログは現れたか
      ifb getid( dialog_title, "#32770", -1 ) > -1 then
        loop_flag = false
      else
        sleep( interval_sec )
        total_wait_sec = total_wait_sec + interval_sec
      endif
    
      // タイムアウトか
      ifb total_wait_sec > timeout_sec then
        msgbox( "dialog '" + dialog_title + "' did not appear." )
        exitexit
      endif
      
    wend
      
  fend
  
  
  // ダイアログにキーを送信
  procedure send_dialog( dialog_title, key_code )
    // 出現を待つ
    wait_for_dialog( dialog_title, 10 )
    sleep(1)
    
    // キー押下
    id = getid( dialog_title, "#32770", -1 )
    sckey( id, key_code )
  fend
  
  
  // ファイルのダウンロードダイアログが出たときに,ダウンロード+保存を実行
  procedure save_downloaded_file( browser )
    sleep(2)
    IE.send_dialog( "ファイルのダウンロード", vk_s )
    IE.send_dialog( "名前を付けて保存", vk_return )
  fend


  // -------------------- テスト実行用 --------------------


  // 要素の値を検証
  procedure assert_value( browser, locator, val_expected )
  
    elem = to_elem( browser, locator )
    val_real = elem.value
    
    assert( val_expected, val_real )
  
  fend


  // 要素内の文字列を検証
  procedure assert_text( browser, locator, val_expected )
  
    elem = to_elem( browser, locator )
    val_real = elem.innerText
    
    assert( val_expected, val_real )
  
  fend


  // 文字列同士を比較
  procedure assert( val_expected, val_real )
  
    ifb val_expected = val_real then
      // OK
    else
      msgbox( "actual value '" + val_real + "' did not match '" + val_expected + "'" )
      
      // スクリプトを強制終了
      exitexit
    endif
  
  fend


  // -------------------- マウス操作用 --------------------


  // ブラウザのbody(表示領域)からwindowまでの余白長を計算
  procedure get_client_margins( browser, var xy_margin[] )

    // mousemove時の挙動を定義
    IE.export_js( _
      browser, _
      "document.onmousemove = function(){ " _
      + "document._uwsc_proxy._mouse_x = event.x;" _
      + "document._uwsc_proxy._mouse_y = event.y;" _
      + "};" _
    ) // 可視領域中だけの座標を見ればよいので,document.scrollTopを足さない

    // mousemoveイベントを発生させて,body内の座標を取得
    LockHard(True)
    mouse_screen_x = 500
    mouse_screen_y = 500
    mmv( mouse_screen_x, mouse_screen_y )
    mouse_client_x = IE.import_js( browser, "document._uwsc_proxy._mouse_x" )
    mouse_client_y = IE.import_js( browser, "document._uwsc_proxy._mouse_y" )
    LockHard(False)
    
    // 差分を取得
    client_margin_x = mouse_screen_x - mouse_client_x
    client_margin_y = mouse_screen_y - mouse_client_y

    // 返却
    xy_margin["x"] = client_margin_x
    xy_margin["y"] = client_margin_y
    
  fend


  // DOM要素のスクリーン上での座標を取得
  procedure get_element_screen_position( browser, locator, var xy_elem_screen[] )
    
    // 余白長を取得
    HashTbl xy_margin
    IE.get_client_margins( browser, xy_margin )
    client_margin_x = xy_margin["x"]
    client_margin_y = xy_margin["y"]

    // 要素のスクリーン上位置を計算
    elem = IE.to_elem( browser, locator )
    elem_client_y = elem.offsetTop  - IE.import_js( browser, "document.body.scrollTop" )
    elem_client_x = elem.offsetLeft - IE.import_js( browser, "document.body.scrollLeft" )
    elem_screen_y = elem_client_y + client_margin_y
    elem_screen_x = elem_client_x + client_margin_x
  
    // 返却
    xy_elem_screen["x"] = elem_screen_x
    xy_elem_screen["y"] = elem_screen_y
  
  fend


  // 要素の上にマウスを移動
  procedure mouse_over_on_elem( browser, locator, var xy_elem_center_screen[] )
  
    elem = IE.to_elem( browser, locator )

    // 要素のスクリーン上の位置を取得
    HashTbl xy_elem_screen
    IE.get_element_screen_position( browser, elem, xy_elem_screen )
    elem_screen_x = xy_elem_screen["x"]
    elem_screen_y = xy_elem_screen["y"]
      //msgbox( elem_screen_x + " " + elem_screen_y )

    // 要素の中心へマウス移動
    elem_center_x = elem_screen_x + ( Val( ChgMoj( elem.offsetWidth, "px", "" ) ) / 2 )
    elem_center_y = elem_screen_y + ( Val( ChgMoj( elem.offsetHeight, "px", "" ) ) / 2 )
      //msgbox( elem_center_x + " " + elem_center_y )
    mmv( elem_center_x, elem_center_y )
    sleep(1)
    
    // 開始点を返す
    xy_elem_center_screen["x"] = elem_center_x
    xy_elem_center_screen["y"] = elem_center_y
  fend



  // 指定要素のドラッグを開始
  procedure start_drag( browser, locator, var xy_start_screen[] )
  
    elem = IE.to_elem( browser, locator )

    // 要素の中心へマウス移動
    HashTbl xy_elem_center_screen
    IE.mouse_over_on_elem( browser, elem, xy_elem_center_screen )
    drag_start_x = xy_elem_center_screen["x"]
    drag_start_y = xy_elem_center_screen["y"]
    
    // 要素をクリック
    btn( LEFT, DOWN, drag_start_x, drag_start_y )
    sleep(1)
    
    // 開始点を返す
    xy_start_screen["x"] = drag_start_x
    xy_start_screen["y"] = drag_start_y

  fend


  // 指定要素をドラッグドロップ
  // 要素と,移動距離と,所要時間を指定
  procedure drag_drop( browser, locator, move_by_x, move_by_y, total_sec )
    elem = IE.to_elem( browser, locator )
    
    // ドラッグ開始
    HashTbl xy_start_screen
    IE.start_drag( browser, elem, xy_start_screen )
    start_screen_x = xy_start_screen["x"]
    start_screen_y = xy_start_screen["y"]
    
    // 移動
    LockHard(True)
    interval_sec = 0.2 // ループ一回にかかる秒数
    move_step_num = total_sec / interval_sec // 移動に要するステップ数
    step_x = move_by_x / move_step_num // 1ステップの移動距離
    step_y = move_by_y / move_step_num
    moved_x = 0 // 累積移動距離
    moved_y = 0
    i = 0
    while i < move_step_num
      moved_x = step_x * ( i + 1 )
      moved_y = step_y * ( i + 1 )
      mmv( start_screen_x + moved_x, start_screen_y + moved_y )
      sleep( interval_sec )
      i = i + 1
    wend
    
    // 左ボタンを上げる
    btn( LEFT, UP, start_screen_x + move_by_x, start_screen_y + move_by_y )
    LockHard(False)
    sleep(1)
    
  fend


endclass


mouse_over_on_elemという関数で,要素の上にマウスを移動させることができる。

実装上のツボ:

  • HashTblを構造体のつもりで扱い,xy座標を持ちまわらせている。
  • ブラウザの「windowの端」から「bodyの端」までの長さを求めるところが面倒。
  • IEではwidthもstyle.widthも取得できなかったりする。そこでかわりにoffsetWidth / offsetHeightを使っている。

onmousemoveイベントを使っているので,一部のページでは動作しない。

補足

Webアプリがリッチになるにつれ,Selenium系の限界を感じるようになる。

複雑なUIも自動で回帰テストができればかなり楽。

UWSCなら可能。

※なお,最初はWSHでオートメーションを組もうとがんばったが,不安定でつまずいた。


関連する記事:

UWSCのマクロで,IEを起動して自動操作するサンプルコード - 主に言語とシステム開発に関して
http://d.hatena.ne.jp/language_and_engineering/20140204/controlIeBrowserbyUWSC


JScript / VBScript (WSH)で,IEを自動操作しよう - 主に言語とシステム開発に関して
http://d.hatena.ne.jp/language_and_engineering/20090713/p1


IE AutoTester で,UIの回帰テストを完全自動化 - 主に言語とシステム開発に関して
http://d.hatena.ne.jp/language_and_engineering/20090922/p1