#--- # Excerpted from "Scripted GUI Testing With Ruby", # published by The Pragmatic Bookshelf. # Copyrights apply to this code. It may not be used to create training material, # courses, books, articles, and the like. Contact us if you are in doubt. # We make no guarantees that this code is fit for any purpose. # Visit http://www.pragmaticprogrammer.com/titles/idgtr for more book information. #--- require 'Win32API' class String def snake_case gsub(/([a-z])([A-Z0-9])/, '\1_\2').downcase end def to_keys unless size == 1 raise "conversion is for single characters only" end ascii = unpack('C')[0] case self when '0'..'9' [ascii - ?0 + 0x30] when 'A'..'Z' [WindowsGui.const_get(:VK_SHIFT), ascii] when 'a'..'z' [ascii - ?a + ?A] when ' ' [ascii] when ',' [WindowsGui.const_get(:VK_OEM_COMMA)] when '.' [WindowsGui.const_get(:VK_OEM_PERIOD)] when ':' [:VK_SHIFT, :VK_OEM_1].map {|s| WindowsGui.const_get s} when "\\" [WindowsGui.const_get(:VK_OEM_102)] else raise "Can't convert unknown character #{self}" end end end module WindowsGui def self.def_api(function, parameters, return_value, rename = nil) api = Win32API.new 'user32', function, parameters, return_value define_method(rename || function.snake_case) do |*args| api.call *args end end def self.load_symbols(header) File.open(header) do |f| f.grep(/#define\s+(ID\w+)\s+(\w+)/) do name = $1 value = (0 == $2.to_i) ? $2.hex : $2.to_i #(1) WindowsGui.const_set name, value #(2) end end end def_api 'FindWindow', ['P', 'P'], 'L' def_api 'FindWindowEx', ['L', 'L', 'P', 'P'], 'L' def_api 'SendMessage', ['L', 'L', 'L', 'P'], 'L', :send_with_buffer def_api 'SendMessage', ['L', 'L', 'L', 'L'], 'L' def_api 'PostMessage', ['L', 'L', 'L', 'L'], 'L' def_api 'keybd_event', ['I', 'I', 'L', 'L'], 'V' def_api 'GetDlgItem', ['L', 'L'], 'L' def_api 'GetWindowRect', ['L', 'P'], 'I' def_api 'SetCursorPos', ['L', 'L'], 'I' def_api 'mouse_event', ['L', 'L', 'L', 'L', 'L'], 'V' def_api 'IsWindow', ['L'], 'L' def_api 'IsWindowVisible', ['L'], 'L' def_api 'SetForegroundWindow', ['L'], 'L' # Windows messages - general WM_COMMAND = 0x0111 WM_SYSCOMMAND = 0x0112 SC_CLOSE = 0xF060 # Windows messages - text WM_GETTEXT = 0x000D EM_GETSEL = 0x00B0 EM_SETSEL = 0x00B1 # Commonly-used control IDs IDOK = 1 IDCANCEL = 2 IDYES = 6 IDNO = 7 # Mouse and keyboard flags MOUSEEVENTF_LEFTDOWN = 0x0002 MOUSEEVENTF_LEFTUP = 0x0004 KEYEVENTF_KEYDOWN = 0 KEYEVENTF_KEYUP = 2 # Modifier keys VK_SHIFT = 0x10 VK_CONTROL = 0x11 VK_MENU = 0x12 # Alt # Commonly-used keys VK_BACK = 0x08 VK_TAB = 0x09 VK_RETURN = 0x0D VK_ESCAPE = 0x1B VK_OEM_1 = 0xBA # semicolon (US) VK_OEM_102 = 0xE2 # backslash (US) VK_OEM_PERIOD = 0xBE VK_HOME = 0x24 VK_END = 0x23 VK_OEM_COMMA = 0xBC def keystroke(*keys) return if keys.empty? keybd_event keys.first, 0, KEYEVENTF_KEYDOWN, 0 sleep 0.05 keystroke *keys[1..-1] sleep 0.05 keybd_event keys.first, 0, KEYEVENTF_KEYUP, 0 end def type_in(message) message.scan(/./m) do |char| keystroke(*char.to_keys) end end def wait_for_window(title, seconds = 5) timeout(seconds) do sleep 0.2 while (h = find_window nil, title) <= 0 || window_text(h) != title h end end class Window include WindowsGui extend WindowsGui attr_reader :handle def initialize(handle) @handle = handle end def close post_message @handle, WM_SYSCOMMAND, SC_CLOSE, 0 end def wait_for_close timeout(5) do sleep 0.2 until 0 == is_window_visible(@handle) end end def text(max_length = 2048) buffer = '\0' * max_length length = send_with_buffer @handle, WM_GETTEXT, buffer.length, buffer length == 0 ? '' : buffer[0..length - 1] end def child(id) result = case id when String by_title = find_window_ex @handle, 0, nil, id.gsub('_', '&') #(3) by_class = find_window_ex @handle, 0, id, nil by_title > 0 ? by_title : by_class when Fixnum get_dlg_item @handle, id else 0 end raise "Control '#{id}' not found" if result == 0 Window.new result end def click(id) h = child(id).handle rectangle = [0, 0, 0, 0].pack 'LLLL' get_window_rect h, rectangle left, top, right, bottom = rectangle.unpack 'LLLL' center = [(left + right) / 2, (top + bottom) / 2] set_cursor_pos *center mouse_event MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0 mouse_event MOUSEEVENTF_LEFTUP, 0, 0, 0, 0 end def self.top_level(title, seconds=10, wnd_class = nil) @handle = timeout(seconds) do loop do h = find_window wnd_class, title break h if h > 0 sleep 0.3 end end Window.new @handle end end DialogWndClass = '#32770' def dialog(title, seconds=5) close, dlg = begin sleep 0.25 w = Window.top_level(title, seconds, DialogWndClass) Window.set_foreground_window w.handle sleep 0.25 [yield(w), w] rescue TimeoutError end dlg.wait_for_close if dlg && close return dlg end end