win32-autogui 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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