win32-autogui 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/.gitattributes +1 -0
  2. data/.gitignore +10 -0
  3. data/.yardopts +6 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +60 -0
  6. data/HISTORY.markdown +11 -0
  7. data/LICENSE +20 -0
  8. data/README.markdown +265 -0
  9. data/Rakefile +55 -0
  10. data/TODO.markdown +9 -0
  11. data/VERSION +1 -0
  12. data/config/cucumber.yml +7 -0
  13. data/examples/quicknote/.gitignore +8 -0
  14. data/examples/quicknote/FormAboutU.dfm +44 -0
  15. data/examples/quicknote/FormAboutU.pas +36 -0
  16. data/examples/quicknote/FormMainU.dfm +110 -0
  17. data/examples/quicknote/FormMainU.pas +268 -0
  18. data/examples/quicknote/FormSplashU.dfm +32 -0
  19. data/examples/quicknote/FormSplashU.pas +52 -0
  20. data/examples/quicknote/LICENSE +20 -0
  21. data/examples/quicknote/README.markdown +28 -0
  22. data/examples/quicknote/Rakefile +12 -0
  23. data/examples/quicknote/TODO.markdown +15 -0
  24. data/examples/quicknote/dcu/.gitignore +1 -0
  25. data/examples/quicknote/exe/.gitignore +0 -0
  26. data/examples/quicknote/exe/quicknote.exe +0 -0
  27. data/examples/quicknote/lib/quicknote.rb +140 -0
  28. data/examples/quicknote/quicknote.cfg +37 -0
  29. data/examples/quicknote/quicknote.dof +158 -0
  30. data/examples/quicknote/quicknote.dpr +16 -0
  31. data/examples/quicknote/quicknote.res +0 -0
  32. data/examples/quicknote/spec/quicknote/form_about_spec.rb +50 -0
  33. data/examples/quicknote/spec/quicknote/form_main_spec.rb +274 -0
  34. data/examples/quicknote/spec/quicknote/form_splash_spec.rb +44 -0
  35. data/examples/quicknote/spec/spec.opts +2 -0
  36. data/examples/quicknote/spec/spec_helper.rb +34 -0
  37. data/examples/quicknote/spec/watchr.rb +143 -0
  38. data/examples/skeleton/.gitignore +8 -0
  39. data/examples/skeleton/LICENSE +20 -0
  40. data/examples/skeleton/README.markdown +62 -0
  41. data/examples/skeleton/Rakefile +21 -0
  42. data/examples/skeleton/TODO.markdown +9 -0
  43. data/examples/skeleton/config/cucumber.yml +7 -0
  44. data/examples/skeleton/dcu/.gitignore +1 -0
  45. data/examples/skeleton/exe/.gitignore +1 -0
  46. data/examples/skeleton/features/basic.feature +6 -0
  47. data/examples/skeleton/features/step_definitions/.gitignore +0 -0
  48. data/examples/skeleton/features/step_definitions/application_steps.rb +43 -0
  49. data/examples/skeleton/features/support/env.rb +5 -0
  50. data/examples/skeleton/lib/myapp.rb +73 -0
  51. data/examples/skeleton/spec/myapp/form_about_spec.rb +50 -0
  52. data/examples/skeleton/spec/myapp/form_main_spec.rb +60 -0
  53. data/examples/skeleton/spec/spec.opts +2 -0
  54. data/examples/skeleton/spec/spec_helper.rb +29 -0
  55. data/examples/skeleton/spec/watchr.rb +143 -0
  56. data/features/automating_an_application.feature +11 -0
  57. data/features/step_definitions/.gitignore +0 -0
  58. data/features/step_definitions/calculator_steps.rb +37 -0
  59. data/features/support/env.rb +4 -0
  60. data/lib/win32/autogui.rb +27 -0
  61. data/lib/win32/autogui/application.rb +249 -0
  62. data/lib/win32/autogui/input.rb +238 -0
  63. data/lib/win32/autogui/window.rb +191 -0
  64. data/lib/win32/autogui/windows/window.rb +22 -0
  65. data/spec/applications/calculator.rb +34 -0
  66. data/spec/auto_gui/application_spec.rb +132 -0
  67. data/spec/basic_gem/basic_gem_spec.rb +13 -0
  68. data/spec/spec.opts +2 -0
  69. data/spec/spec_helper.rb +31 -0
  70. data/spec/watchr.rb +144 -0
  71. data/win32-autogui.gemspec +43 -0
  72. metadata +329 -0
@@ -0,0 +1,11 @@
1
+ Feature: Automating a GUI application
2
+
3
+ As a developer, I want to run automated tests on GUI applications
4
+ so that my specifications are testable in a repeatable manner.
5
+
6
+ Background: A running GUI application
7
+ Given a GUI application named calculator
8
+
9
+ Scenario: Simple calculation
10
+ When I type in "2+2="
11
+ Then the edit window text should match /4/
File without changes
@@ -0,0 +1,37 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '../../../spec/applications/calculator')
2
+
3
+ include Autogui::Input
4
+
5
+ After('@calculator') do
6
+ if @calculator
7
+ @calculator.close(:wait_for_close => true) if @calculator.running?
8
+ @calculator.should_not be_running
9
+ end
10
+ end
11
+
12
+ Given /^a GUI application named calculator$/ do
13
+ @calculator = Calculator.new
14
+ @calculator.should be_running
15
+ end
16
+
17
+ When /^I type in "([^"]*)"$/ do |string|
18
+ @calculator.set_focus
19
+ type_in(string)
20
+ end
21
+
22
+ # "the window text should match" allows regex in the partial_output, if
23
+ # you don't need regex, use "the output should contain" instead since
24
+ # that way, you don't have to escape regex characters that
25
+ # appear naturally in the output
26
+ Then /^the edit window text should match \/([^\/]*)\/$/ do |partial_output|
27
+ @calculator.edit_window.text.should =~ /#{partial_output}/
28
+ end
29
+
30
+ Then /^the edit window text should contain exactly "([^"]*)"$/ do |exact_output|
31
+ @calculator.edit_window.text.should == unescape(exact_output)
32
+ end
33
+
34
+ Then /^the edit window text should contain exactly:$/ do |exact_output|
35
+ @calculator.edit_window.text.should == exact_output
36
+ end
37
+
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
2
+ require 'win32/autogui'
3
+ require 'aruba'
4
+ require 'spec/expectations'
@@ -0,0 +1,27 @@
1
+ # require all files here
2
+ require 'win32/autogui/input'
3
+ require 'win32/autogui/window'
4
+ require 'win32/autogui/application'
5
+
6
+ # Master namespace
7
+ module Autogui
8
+
9
+ # Contents of the VERSION file
10
+ #
11
+ # Example format: 0.0.1
12
+ #
13
+ # @return [String] the contents of the version file in #.#.# format
14
+ def self.version
15
+ version_info_file = File.join(File.dirname(__FILE__), *%w[.. .. VERSION])
16
+ File.open(version_info_file, "r") do |f|
17
+ f.read.strip
18
+ end
19
+ end
20
+
21
+ # @return False (nil) or True (Integer)
22
+ def self.win32?
23
+ RUBY_PLATFORM =~ /mingw|mswin|cygwin/i
24
+ end
25
+
26
+ end
27
+
@@ -0,0 +1,249 @@
1
+ require 'windows/process'
2
+ require 'windows/synchronize'
3
+ require 'windows/handle'
4
+ require "win32/process"
5
+ require "win32/clipboard"
6
+
7
+ module Autogui
8
+
9
+ # Wrapper class for text portion of the RubyGem win32/clipboard
10
+ # @see http://github.com/djberg96/win32-clipboard
11
+ class Clipboard
12
+
13
+ # Clipboard text getter
14
+ #
15
+ # @return [String] clipboard data
16
+ #
17
+ def text
18
+ Win32::Clipboard.data
19
+ end
20
+
21
+ # Clipboard text setter
22
+ #
23
+ # @param [String] str text to load onto the clipboard
24
+ #
25
+ def text=(str)
26
+ Win32::Clipboard.set_data(str)
27
+ end
28
+
29
+ end
30
+
31
+ # The Application class wraps a binary application so
32
+ # that it can be started and controlled via Ruby. This
33
+ # class is meant to be subclassed.
34
+ #
35
+ # @example
36
+ #
37
+ # class Calculator < Autogui::Application
38
+ #
39
+ # def initialize(options = {})
40
+ # defaults = {
41
+ # :name => "calc",
42
+ # :title => "Calculator"
43
+ # }
44
+ # super defaults.merge(options)
45
+ # end
46
+ #
47
+ # def edit_window
48
+ # main_window.children.find {|w| w.window_class == 'Edit'}
49
+ # end
50
+ #
51
+ # def dialog_about
52
+ # Autogui::EnumerateDesktopWindows.new.find do |w|
53
+ # w.title.match(/About Calculator/) && (w.pid == pid)
54
+ # end
55
+ # end
56
+ #
57
+ # def clear_entry
58
+ # set_focus
59
+ # keystroke(VK_DELETE)
60
+ # end
61
+ # end
62
+ #
63
+ class Application
64
+ include Windows::Process
65
+ include Windows::Synchronize
66
+ include Windows::Handle
67
+
68
+ # @return [String] the executable name of the application
69
+ attr_accessor :name
70
+
71
+ # @return [String] the executable application parameters
72
+ attr_accessor :parameters
73
+
74
+ # @return [String] window title of the application
75
+ attr_accessor :title
76
+
77
+ # @return [Number] the process identifier (PID) returned by Process.create
78
+ attr_reader :pid
79
+
80
+ # @return [Number] the process thread id returned by Process.create
81
+ attr_reader :thread_id
82
+
83
+ # @return [Number] the main_window wait timeout in seconds
84
+ attr_accessor :main_window_timeout
85
+
86
+ # @return [Number] the wait timeout in seconds used by Process.create
87
+ attr_accessor :create_process_timeout
88
+
89
+ # @example initialize an application on the path
90
+ #
91
+ # Application.new :name => "calc"
92
+ #
93
+ # @example initialize with full DOS path
94
+ #
95
+ # Application.new :name => "\\windows\\system32\\calc.exe"
96
+ #
97
+ # @param [Hash] options initialize options
98
+ # @option options [String] :name a valid win32 exe name with optional path
99
+ # @option options [String] :title the application window title, used along with the pid to locate the application main window, defaults to :name
100
+ # @option options [Number] :parameters command line parameters used by Process.create
101
+ # @option options [Number] :create_process_timeout (10) timeout in seconds to wait for the create_process to return
102
+ # @option options [Number] :main_window_timeout (10) timeout in seconds to wait for main_window to appear
103
+ #
104
+ def initialize(options = {})
105
+
106
+ unless options.kind_of?(Hash)
107
+ raise ArgumentError, 'Initialize expecting options to be a Hash'
108
+ end
109
+
110
+ @name = options[:name] || name
111
+ @title = options[:title] || name
112
+ @main_window_timeout = options[:main_window_timeout] || 10
113
+ @create_process_timeout = options[:create_process_timeout] || 10
114
+ @parameters = options[:parameters]
115
+
116
+ # sanity checks
117
+ raise 'Application name not set' unless name
118
+
119
+ start
120
+ end
121
+
122
+ # Start up the binary application via Process.create and
123
+ # set the window focus to the main_window
124
+ #
125
+ # @raise [Exception] if create_process_timeout exceeded
126
+ # @raise [Exception] if start failed for any reason other than create_process_timeout
127
+ #
128
+ # @return [Number] the pid
129
+ #
130
+ def start
131
+
132
+ command_line = name
133
+ command_line = name + ' ' + parameters if parameters
134
+
135
+ # returns a struct, raises an error if fails
136
+ process_info = Process.create(
137
+ :command_line => command_line,
138
+ :close_handles => false,
139
+ :creation_flags => Process::DETACHED_PROCESS
140
+ )
141
+ @pid = process_info.process_id
142
+ @thread_id = process_info.thread_id
143
+ process_handle = process_info.process_handle
144
+ thread_handle = process_info.thread_handle
145
+
146
+ # wait for process
147
+ ret = WaitForInputIdle(process_handle, (create_process_timeout * 1000))
148
+
149
+ # done with the handles
150
+ CloseHandle(process_handle)
151
+ CloseHandle(thread_handle)
152
+
153
+ raise "Start command failed on create_process_timeout" if ret == WAIT_TIMEOUT
154
+ raise "Start command failed while waiting for idle input, reason unknown" unless (ret == 0)
155
+ @pid
156
+ end
157
+
158
+ # The application main window found by enumerating windows
159
+ # by title and application pid. This method will keep looking
160
+ # unit main_window_timeout (default: 10s) is exceeded.
161
+ #
162
+ # @raise [Exception] if the main window cannot be found
163
+ #
164
+ # @return [Autogui::Window]
165
+ # @see initialize for options
166
+ #
167
+ def main_window
168
+ return @main_window if @main_window
169
+
170
+ timeout(main_window_timeout) do
171
+ begin
172
+ # There may be multiple instances, use title and pid to id our main window
173
+ @main_window = Autogui::EnumerateDesktopWindows.new.find do |w|
174
+ w.title.match(title) && w.pid == pid
175
+ end
176
+ sleep 0.1
177
+ end until @main_window
178
+ end
179
+
180
+ # sanity checks
181
+ raise "cannot find main_window, check application title" unless @main_window
182
+
183
+ @main_window
184
+ end
185
+
186
+ # Call the main_window's close method
187
+ #
188
+ # PostMessage SC_CLOSE and optionally wait for the window to close
189
+ #
190
+ # @param [Hash] options
191
+ # @option options [Boolean] :wait_for_close (true) sleep while waiting for timeout or close
192
+ # @option options [Boolean] :timeout (5) wait_for_close timeout in seconds
193
+ #
194
+ def close(options={})
195
+ main_window.close(options)
196
+ end
197
+
198
+ # Send SIGKILL to force the application to die
199
+ def kill
200
+ Process::kill(9, pid)
201
+ end
202
+
203
+ # @return [Boolean] if the application is currently running
204
+ def running?
205
+ main_window && (main_window.is_window?)
206
+ end
207
+
208
+ # Set the application input focus to the main_window
209
+ #
210
+ # @return [Number] nonzero number if sucess, nil or zero if failed
211
+ #
212
+ def set_focus
213
+ main_window.set_focus if running?
214
+ end
215
+
216
+ # The main_window text including all child windows
217
+ # joined together with newlines. Faciliates matching text.
218
+ #
219
+ # @example partial match of the Window's calulator's about dialog copywrite text
220
+ #
221
+ # dialog_about = @calculator.dialog_about
222
+ # dialog_about.title.should == "About Calculator"
223
+ # dialog_about.combined_text.should match(/Microsoft . Calculator/)
224
+ #
225
+ # @return [String] with newlines
226
+ #
227
+ def combined_text
228
+ main_window.combined_text if running?
229
+ end
230
+
231
+ # @example set the clipboard text and paste it with Control-V
232
+ #
233
+ # @calculator.edit_window.set_focus
234
+ # @calculator.clipboard.text = "12345"
235
+ # @calculator.edit_window.text.strip.should == "0."
236
+ # keystroke(VK_CONTROL, VK_V)
237
+ # @calculator.edit_window.text.strip.should == "12,345."
238
+ #
239
+ # @return [Clipboard]
240
+ #
241
+ def clipboard
242
+ @clipboard || Autogui::Clipboard.new
243
+ end
244
+
245
+ private
246
+
247
+ end
248
+
249
+ end
@@ -0,0 +1,238 @@
1
+ # The algorithms in this module are presented in the book
2
+ # Scripted GUI testing with Ruby by Ian Dees
3
+ # @see http://pragprog.com/titles/idgtr/scripted-gui-testing-with-ruby
4
+
5
+ require 'windows/api'
6
+
7
+ # methods for simulating user input
8
+ module Autogui
9
+ module Input
10
+
11
+ # MSDN virtual key codes
12
+ VK_LBUTTON = 0x01
13
+ VK_RBUTTON = 0x02
14
+
15
+ VK_CANCEL = 0x03
16
+ VK_BACK = 0x08
17
+ VK_TAB = 0x09
18
+ VK_CLEAR = 0x0c
19
+ VK_RETURN = 0x0d
20
+ VK_SHIFT = 0x10
21
+ VK_CONTROL = 0x11
22
+ VK_MENU = 0x12
23
+ VK_PAUSE = 0x13
24
+ VK_ESCAPE = 0x1b
25
+ VK_SPACE = 0x20
26
+ VK_PRIOR = 0x21
27
+ VK_NEXT = 0x22
28
+ VK_END = 0x23
29
+ VK_HOME = 0x24
30
+ VK_LEFT = 0x25
31
+ VK_UP = 0x26
32
+ VK_RIGHT = 0x27
33
+ VK_DOWN = 0x28
34
+ VK_SELECT = 0x29
35
+ VK_EXECUTE = 0x2b
36
+ VK_SNAPSHOT = 0x2c
37
+ VK_INSERT = 0x2d
38
+ VK_DELETE = 0x2e
39
+ VK_HELP = 0x2f
40
+
41
+ VK_0 = 0x30
42
+ VK_1 = 0x31
43
+ VK_2 = 0x32
44
+ VK_3 = 0x33
45
+ VK_4 = 0x34
46
+ VK_5 = 0x35
47
+ VK_6 = 0x36
48
+ VK_7 = 0x37
49
+ VK_8 = 0x38
50
+ VK_9 = 0x39
51
+ VK_A = 0x41
52
+ VK_B = 0x42
53
+ VK_C = 0x43
54
+ VK_D = 0x44
55
+ VK_E = 0x45
56
+ VK_F = 0x46
57
+ VK_G = 0x47
58
+ VK_H = 0x48
59
+ VK_I = 0x49
60
+ VK_J = 0x4a
61
+ VK_K = 0x4b
62
+ VK_L = 0x4c
63
+ VK_M = 0x4d
64
+ VK_N = 0x4e
65
+ VK_O = 0x4f
66
+ VK_P = 0x50
67
+ VK_Q = 0x51
68
+ VK_R = 0x52
69
+ VK_S = 0x53
70
+ VK_T = 0x54
71
+ VK_U = 0x55
72
+ VK_V = 0x56
73
+ VK_W = 0x57
74
+ VK_X = 0x58
75
+ VK_Y = 0x59
76
+ VK_Z = 0x5a
77
+
78
+ VK_LWIN = 0x5b
79
+ VK_RWIN = 0x5c
80
+ VK_APPS = 0x5d
81
+
82
+ VK_NUMPAD0 = 0x60
83
+ VK_NUMPAD1 = 0x61
84
+ VK_NUMPAD2 = 0x62
85
+ VK_NUMPAD3 = 0x63
86
+ VK_NUMPAD4 = 0x64
87
+ VK_NUMPAD5 = 0x65
88
+ VK_NUMPAD6 = 0x66
89
+ VK_NUMPAD7 = 0x67
90
+ VK_NUMPAD8 = 0x68
91
+ VK_NUMPAD9 = 0x69
92
+ VK_MULTIPLY = 0x6a
93
+ VK_ADD = 0x6b
94
+ VK_SEPARATOR = 0x6c
95
+ VK_SUBTRACT = 0x6d
96
+ VK_DECIMAL = 0x6e
97
+ VK_DIVIDE = 0x6f
98
+
99
+ VK_F1 = 0x70
100
+ VK_F2 = 0x71
101
+ VK_F3 = 0x72
102
+ VK_F4 = 0x73
103
+ VK_F5 = 0x74
104
+ VK_F6 = 0x75
105
+ VK_F7 = 0x76
106
+ VK_F8 = 0x77
107
+ VK_F9 = 0x78
108
+ VK_F10 = 0x79
109
+ VK_F11 = 0x7a
110
+ VK_F12 = 0x7b
111
+
112
+ VK_NUMLOCK = 0x90
113
+ VK_SCROLL = 0x91
114
+ VK_OEM_EQU = 0x92
115
+ VK_LSHIFT = 0xa0
116
+ VK_RSHIFT = 0xa1
117
+ VK_LCONTROL = 0xa2
118
+ VK_RCONTROL = 0xa3
119
+ VK_LMENU = 0xa4
120
+ VK_RMENU = 0xa5
121
+
122
+ VK_OEM_1 = 0xba
123
+ VK_OEM_PLUS = 0xbb
124
+ VK_OEM_COMMA = 0xbc
125
+ VK_OEM_MINUS = 0xbd
126
+ VK_OEM_PERIOD = 0xbe
127
+ VK_OEM_2 = 0xbf
128
+ VK_OEM_3 = 0xc0 # US '~' key
129
+ VK_OEM_4 = 0xdb
130
+ VK_OEM_5 = 0xdc # US '\' key
131
+ VK_OEM_6 = 0xdd
132
+ VK_OEM_7 = 0xde # US quotes key
133
+ VK_OEM_8 = 0xdf
134
+
135
+ # delay in seconds between keystrokes
136
+ KEYBD_KEYDELAY = 0.050
137
+
138
+ # keybd_event
139
+ KEYBD_EVENT_KEYUP = 2
140
+ KEYBD_EVENT_KEYDOWN = 0
141
+
142
+ Windows::API.auto_namespace = 'Autogui::Input'
143
+ Windows::API.auto_constant = true
144
+ Windows::API.auto_method = true
145
+ Windows::API.auto_unicode = false
146
+
147
+ Windows::API.new('keybd_event', 'IILL', 'V', 'user32')
148
+ Windows::API.new('mouse_event', 'LLLLL', 'V', 'user32')
149
+
150
+ # Send keystroke to the focused window, keystrokes are virtual keycodes
151
+ #
152
+ # @example send 2+2<CR>
153
+ #
154
+ # keystroke(VK_2, VK_ADD, VK_2, VK_RETURN)
155
+ #
156
+ def keystroke(*keys)
157
+ return if keys.empty?
158
+
159
+ keybd_event keys.first, 0, KEYBD_EVENT_KEYDOWN, 0
160
+ sleep KEYBD_KEYDELAY
161
+ keystroke *keys[1..-1]
162
+ sleep KEYBD_KEYDELAY
163
+ keybd_event keys.first, 0, KEYBD_EVENT_KEYUP, 0
164
+ end
165
+
166
+ # String together keystrokes, simulates the user typing.
167
+ #
168
+ # Note: This method can be slow for large strings. Consider using
169
+ # the clipboard instead.
170
+ #
171
+ # @see Clipboard
172
+ #
173
+ # @example send 2+2<CR>
174
+ #
175
+ # type_in("2+2\n")
176
+ #
177
+ # @param [String] string of characters to simulate typing
178
+ def type_in(string)
179
+ string.each_char do |char|
180
+ keystroke(*char_to_virtual_keycode(char))
181
+ end
182
+ end
183
+
184
+ private
185
+
186
+ # convert a single character to a virtual keycode
187
+ #
188
+ # @param [Char] char is the character to convert
189
+ #
190
+ # @return [Array] of virtual keycodes
191
+ def char_to_virtual_keycode(char)
192
+
193
+ unless char.size == 1
194
+ raise "virtual keycode conversion is for single characters only"
195
+ end
196
+
197
+ code = char.unpack('U')[0]
198
+
199
+ case char
200
+ when '0'..'9'
201
+ [code - ?0 + 0x30]
202
+ when 'A'..'Z'
203
+ [VK_SHIFT, code]
204
+ when 'a'..'z'
205
+ [code - ?a + ?A]
206
+ when ' '
207
+ [code]
208
+ when '+'
209
+ [VK_ADD]
210
+ when '='
211
+ [VK_OEM_PLUS]
212
+ when ','
213
+ [VK_OEM_COMMA]
214
+ when '.'
215
+ [VK_OEM_PERIOD]
216
+ when '-'
217
+ [VK_OEM_MINUS]
218
+ when '_'
219
+ [VK_SHIFT, VK_OEM_MINUS]
220
+ when ':'
221
+ [VK_SHIFT, VK_OEM_1]
222
+ when ';'
223
+ [VK_OEM_1]
224
+ when '\''
225
+ [VK_OEM_7]
226
+ when '\"'
227
+ [VK_SHIFT, VK_OEM_7]
228
+ when "\\"
229
+ [VK_OEM_5]
230
+ when "\n"
231
+ [VK_RETURN]
232
+ else
233
+ raise "No conversion exists for character #{char}"
234
+ end
235
+ end
236
+
237
+ end
238
+ end