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,191 @@
1
+ require 'windows/window'
2
+ require 'windows/window/message'
3
+ require 'win32/autogui/windows/window'
4
+
5
+ module Autogui
6
+
7
+ # Enumerate desktop child windows
8
+ #
9
+ # Start at the desktop and work down through all the child windows
10
+ #
11
+ class EnumerateDesktopWindows
12
+ include Enumerable
13
+ include Windows::Window
14
+
15
+ def each
16
+ child_after = 0
17
+ while (child_after = FindWindowEx(nil, child_after, nil, nil)) > 0 do
18
+ yield Window.new child_after
19
+ end
20
+ end
21
+ end
22
+
23
+ # Enumerate just the child windows one level down from the parent window
24
+ #
25
+ class Children
26
+ include Enumerable
27
+ include Windows::Window
28
+
29
+ # @param [Number] parent window handle
30
+ #
31
+ def initialize(parent)
32
+ @parent = parent
33
+ end
34
+
35
+ # @yield [Window]
36
+ #
37
+ def each
38
+ child_after = 0
39
+ while (child_after = FindWindowEx(@parent.handle, child_after, nil, nil)) > 0 do
40
+ window = Window.new child_after
41
+ # immediate children only
42
+ yield window if (window.parent.handle == @parent.handle)
43
+ end
44
+ end
45
+ end
46
+
47
+ # Wrapper for window
48
+ #
49
+ class Window
50
+ include Windows::Window # instance methods from windows-pr gem
51
+ include Windows::Window::Message # PostMessage and constants
52
+
53
+ attr_reader :handle
54
+
55
+ def initialize(handle)
56
+ @handle = handle
57
+ end
58
+
59
+ # enumerable immeadiate child windows
60
+ #
61
+ # @see Children
62
+ #
63
+ def children
64
+ Children.new(self)
65
+ end
66
+
67
+ # @return [Object] Window or nil
68
+ #
69
+ def parent
70
+ h = GetParent(handle)
71
+ Window.new h if h > 0
72
+ end
73
+
74
+ # PostMessage SC_CLOSE and optionally wait for the window to close
75
+ #
76
+ # @param [Hash] options
77
+ # @option options [Boolean] :wait_for_close (true) sleep while waiting for timeout or close
78
+ # @option options [Boolean] :timeout (5) wait_for_close timeout in seconds
79
+ #
80
+ def close(options={})
81
+ PostMessage(handle, WM_SYSCOMMAND, SC_CLOSE, 0)
82
+ wait_for_close(options) if (options[:wait_for_close] == true)
83
+ end
84
+
85
+ # Wait for the window to close
86
+ #
87
+ # @param [Hash] options
88
+ # @option options [Boolean] :timeout (5) timeout in seconds
89
+ #
90
+ def wait_for_close(options={})
91
+ seconds = options[:timeout] || 5
92
+ timeout(seconds) do
93
+ sleep 0.05 until 0 == IsWindow(handle)
94
+ end
95
+ end
96
+
97
+ # @return [String] the ANSI Windows ClassName
98
+ #
99
+ def window_class
100
+ buffer = "\0" * 255
101
+ length = GetClassNameA(handle, buffer, buffer.length)
102
+ length == 0 ? '' : buffer[0..length - 1]
103
+ end
104
+
105
+ # Window text (WM_GETTEXT)
106
+ #
107
+ # @param [Number] max_length (2048)
108
+ #
109
+ # @return [String] of max_length (2048)
110
+ #
111
+ def text(max_length = 2048)
112
+ buffer = "\0" * max_length
113
+ length = SendMessageA(handle, WM_GETTEXT, buffer.length, buffer)
114
+ length == 0 ? '' : buffer[0..length - 1]
115
+ end
116
+ alias :title :text
117
+
118
+ # Determines whether the specified window handle identifies an existing window
119
+ #
120
+ # @return [Boolean]
121
+ #
122
+ def is_window?
123
+ (handle != 0) && (IsWindow(handle) != 0)
124
+ end
125
+
126
+ # Brings the window into the foreground and activates it.
127
+ # Keyboard input is directed to the window, and various visual cues
128
+ # are changed for the user.
129
+ #
130
+ # @return [Number] nonzero number if sucessful, nil or zero if failed
131
+ #
132
+ def set_focus
133
+ SetForegroundWindow(handle) if is_window?
134
+ end
135
+
136
+ # The identifier (pid) of the process that created the window
137
+ #
138
+ # @return [Integer] process id if the window exists, otherwise nil
139
+ #
140
+ def pid
141
+ return nil unless is_window?
142
+ process_id = 0.chr * 4
143
+ GetWindowThreadProcessId(handle, process_id)
144
+ process_id = process_id.unpack('L').first
145
+ end
146
+
147
+ # The identifier of the thread that created the window
148
+ #
149
+ # @return [Integer] thread id if the window exists, otherwise nil
150
+ #
151
+ def thread_id
152
+ return nil unless is_window?
153
+ GetWindowThreadProcessId(handle, nil)
154
+ end
155
+
156
+ # The window text including all child windows
157
+ # joined together with newlines. Faciliates matching text.
158
+ # Text from any given window is limited to 2048 characters
159
+ #
160
+ # @example partial match of the Window's calulator's about dialog copywrite text
161
+ #
162
+ # dialog_about = @calculator.dialog_about
163
+ # dialog_about.title.should == "About Calculator"
164
+ # dialog_about.combined_text.should match(/Microsoft . Calculator/)
165
+ #
166
+ # @return [String] with newlines
167
+ #
168
+ def combined_text
169
+ return unless is_window?
170
+ t = []
171
+ t << text unless text == ''
172
+ children.each do |w|
173
+ t << w.combined_text unless w.combined_text == ''
174
+ end
175
+ t.join("\n")
176
+ end
177
+
178
+ # Debugging information
179
+ #
180
+ # @return [String] with child window information
181
+ def inspect
182
+ c = []
183
+ children.each do |w|
184
+ c << w.inspect
185
+ end
186
+ s = super + " #{self.class}=<window_class:#{window_class} pid:#{pid} thread_id:#{thread_id} title:\"#{title}\" children=<" + c.join("\n") + ">>"
187
+ end
188
+
189
+ end
190
+
191
+ end
@@ -0,0 +1,22 @@
1
+ # Reopen module and supply missing constants and
2
+ # functions from windows-pr gem
3
+ #
4
+ # TODO: Fork and send pull request for Windows::Window module, be sure to lock bundle before sending request
5
+ #
6
+ module Windows
7
+ module Window
8
+
9
+ SC_CLOSE = 0xF060
10
+
11
+ API.auto_namespace = 'Windows::Window'
12
+ API.auto_constant = true
13
+ API.auto_method = true
14
+ API.auto_unicode = false
15
+
16
+ API.new('IsWindow', 'L', 'I', 'user32')
17
+ API.new('SetForegroundWindow', 'L', 'I', 'user32')
18
+ API.new('SendMessageA', 'LIIP', 'I', 'user32')
19
+ API.new('GetClassNameA', 'LPI', 'I', 'user32')
20
+
21
+ end
22
+ end
@@ -0,0 +1,34 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ class Calculator < Autogui::Application
4
+
5
+ # initialize with the binary name 'calc' and the window title
6
+ # 'Calculator' used along with the application pid to find the
7
+ # main application window
8
+ def initialize(options = {})
9
+ defaults = {
10
+ :name => "calc",
11
+ :title => "Calculator"
12
+ }
13
+ super defaults.merge(options)
14
+ end
15
+
16
+ # the calculator's results window
17
+ def edit_window
18
+ main_window.children.find {|w| w.window_class == 'Edit'}
19
+ end
20
+
21
+ # About dialog, hotkey (VK_MENU, VK_H, VK_A)
22
+ def dialog_about
23
+ Autogui::EnumerateDesktopWindows.new.find do |w|
24
+ w.title.match(/About Calculator/) && (w.pid == pid)
25
+ end
26
+ end
27
+
28
+ # the 'CE' button
29
+ def clear_entry
30
+ set_focus
31
+ keystroke(VK_DELETE)
32
+ end
33
+
34
+ end
@@ -0,0 +1,132 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ include Autogui::Input
4
+
5
+ describe Autogui::Application do
6
+
7
+ describe "driving calc.exe" do
8
+
9
+ before(:all) do
10
+ @calculator = Calculator.new
11
+ @calculator.set_focus
12
+ end
13
+
14
+ after(:all) do
15
+ @calculator.close(:wait_for_close => true) if @calculator.running?
16
+ @calculator.should_not be_running
17
+ end
18
+
19
+ it "should start when initialized" do
20
+ @calculator.should be_running
21
+ end
22
+
23
+ it "should die when sending the kill signal" do
24
+ killme = Calculator.new
25
+ killme.should be_running
26
+ killme.kill
27
+ killme.should_not be_running
28
+ end
29
+
30
+ it "should have the title 'Calculator' that matches the main_window title" do
31
+ @calculator.main_window.title.should == 'Calculator'
32
+ @calculator.main_window.title.should == @calculator.title
33
+ end
34
+
35
+ it "should have an inspect method showing child window information" do
36
+ @calculator.inspect.should match(/children=</)
37
+ end
38
+
39
+ it "should raise an error if setting focus and the application title is incorrect" do
40
+ goodcalc = Calculator.new :title => "Calculator"
41
+ lambda { goodcalc.set_focus }.should_not raise_error
42
+ goodcalc.close
43
+
44
+ badcalc = Calculator.new :title => "BaDTitle"
45
+ lambda {
46
+ begin
47
+ badcalc.setfocus
48
+ ensure
49
+ badcalc.kill
50
+ end
51
+ }.should raise_error
52
+ end
53
+
54
+ it "should control the focus with 'set_focus'" do
55
+ @calculator.set_focus
56
+ keystroke(VK_9)
57
+ @calculator.edit_window.text.strip.should == "9."
58
+
59
+ calculator2 = Calculator.new
60
+ calculator2.pid.should_not == @calculator.pid
61
+ calculator2.set_focus
62
+ keystroke(VK_1, VK_0)
63
+ calculator2.edit_window.text.strip.should == "10."
64
+
65
+ @calculator.set_focus
66
+ @calculator.edit_window.text.strip.should == "9."
67
+
68
+ calculator2.close(:wait_for_close => true)
69
+ end
70
+
71
+ it "should open and close the 'About Calculator' dialog via (VK_MENU, VK_H, VK_A)" do
72
+ @calculator.set_focus
73
+ dialog_about = @calculator.dialog_about
74
+ dialog_about.should be_nil
75
+ keystroke(VK_MENU, VK_H, VK_A)
76
+ dialog_about = @calculator.dialog_about
77
+ dialog_about.title.should == "About Calculator"
78
+ dialog_about.combined_text.should match(/Microsoft . Calculator/)
79
+ dialog_about.close
80
+ @calculator.dialog_about.should be_nil
81
+ end
82
+
83
+ describe "calculations" do
84
+ before(:each) do
85
+ @calculator.clear_entry
86
+ end
87
+
88
+ it "should calculate '2+2=4' using the keystroke method" do
89
+ @calculator.set_focus
90
+ keystroke(VK_2, VK_ADD, VK_2, VK_RETURN)
91
+ @calculator.edit_window.text.strip.should == "4."
92
+ end
93
+
94
+ it "should calculate '2+12=14' using the type_in method" do
95
+ @calculator.set_focus
96
+ type_in("2+12=")
97
+ @calculator.edit_window.text.strip.should == "14."
98
+ end
99
+ end
100
+
101
+ describe "clipboard" do
102
+ before(:each) do
103
+ @calculator.clear_entry
104
+ @calculator.clipboard.text = ""
105
+ @calculator.clipboard.text.should == ""
106
+ end
107
+
108
+ describe "copy (VK_CONTROL, VK_C)" do
109
+ it "should copy the edit window" do
110
+ @calculator.set_focus
111
+ type_in("3002")
112
+ @calculator.edit_window.text.strip.should == "3,002."
113
+ @calculator.edit_window.set_focus
114
+ keystroke(VK_CONTROL, VK_C)
115
+ @calculator.clipboard.text.should == "3002"
116
+ end
117
+ end
118
+
119
+ describe "paste (VK_CONTROL, VK_V)" do
120
+ it "should paste into the edit window" do
121
+ @calculator.edit_window.set_focus
122
+ @calculator.clipboard.text = "12345"
123
+ @calculator.edit_window.text.strip.should == "0."
124
+ keystroke(VK_CONTROL, VK_V)
125
+ @calculator.edit_window.text.strip.should == "12,345."
126
+ end
127
+ end
128
+
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,13 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Autogui do
4
+
5
+ describe 'version' do
6
+
7
+ it "should return a string formatted '#.#.#'" do
8
+ Autogui::version.should match(/(^[\d]+\.[\d]+\.[\d]+$)/)
9
+ end
10
+
11
+ end
12
+
13
+ end
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format nested
@@ -0,0 +1,31 @@
1
+ $LOAD_PATH.unshift File.expand_path('..', __FILE__) unless
2
+ $LOAD_PATH.include? File.expand_path('..', __FILE__)
3
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) unless
4
+ $LOAD_PATH.include? File.expand_path('../../lib', __FILE__)
5
+
6
+ require 'rubygems'
7
+ require 'win32/autogui'
8
+ require 'spec'
9
+ require 'spec/autorun'
10
+ require 'aruba/api'
11
+
12
+ # applications
13
+ require File.expand_path(File.dirname(__FILE__) + '/applications/calculator')
14
+
15
+ # aruba helper, returns full path to files in the aruba tmp folder
16
+ def fullpath(filename)
17
+ path = File.expand_path(File.join(current_dir, filename))
18
+ path = `cygpath -w #{path}`.chomp if path.match(/^\/cygdrive/) # cygwin?
19
+ path
20
+ end
21
+
22
+ # return the contents of "filename" in the aruba tmp folder
23
+ def get_file_content(filename)
24
+ in_current_dir do
25
+ IO.read(filename)
26
+ end
27
+ end
28
+
29
+ Spec::Runner.configure do |config|
30
+ config.include Aruba::Api
31
+ end
@@ -0,0 +1,144 @@
1
+ # Watchr: Autotest like functionality
2
+ #
3
+ # gem install watchr
4
+ #
5
+ # Run me with:
6
+ #
7
+ # $ watchr spec/watchr.rb
8
+
9
+ require 'term/ansicolor'
10
+
11
+ $c = Term::ANSIColor
12
+
13
+ def getch
14
+ state = `stty -g`
15
+ begin
16
+ `stty raw -echo cbreak`
17
+ $stdin.getc
18
+ ensure
19
+ `stty #{state}`
20
+ end
21
+ end
22
+
23
+ # --------------------------------------------------
24
+ # Convenience Methods
25
+ # --------------------------------------------------
26
+ def all_feature_files
27
+ Dir['features/*.feature']
28
+ end
29
+
30
+ def all_spec_files
31
+ files = Dir['spec/**/*_spec\.rb']
32
+ end
33
+
34
+ def run(cmd)
35
+
36
+ pid = fork do
37
+ puts "\n"
38
+ print $c.cyan, cmd, $c.clear, "\n"
39
+ exec(cmd)
40
+ end
41
+ Signal.trap('INT') do
42
+ puts "sending KILL to pid: #{pid}"
43
+ Process.kill("KILL", pid)
44
+ end
45
+ Process.waitpid(pid)
46
+
47
+ prompt
48
+ end
49
+
50
+ def run_all
51
+ run_all_specs
52
+ run_default_cucumber
53
+ end
54
+
55
+ # allow cucumber rerun.txt smarts
56
+ def run_default_cucumber
57
+ cmd = "cucumber"
58
+ run(cmd)
59
+ end
60
+
61
+ def run_all_features
62
+ cmd = "cucumber #{all_feature_files.join(' ')}"
63
+ run(cmd)
64
+ end
65
+
66
+ def run_feature(feature)
67
+ cmd = "cucumber #{feature}"
68
+ $last_feature = feature
69
+ run(cmd)
70
+ end
71
+
72
+ def run_last_feature
73
+ run_feature($last_feature) if $last_feature
74
+ end
75
+
76
+ def run_default_spec
77
+ cmd = "spec _1.3.1_ --color --format s ./spec"
78
+ run(cmd)
79
+ end
80
+
81
+ def run_all_specs
82
+ cmd = "spec _1.3.1_ --color --format s #{all_spec_files.join(' ')}"
83
+ p cmd
84
+ run(cmd)
85
+ end
86
+
87
+ def run_spec(spec)
88
+ cmd = "spec _1.3.1_ --color --format s #{spec}"
89
+ $last_spec = spec
90
+ run(cmd)
91
+ end
92
+
93
+ def run_last_spec
94
+ run_spec($last_spec) if $last_spec
95
+ end
96
+
97
+ def prompt
98
+ puts "Ctrl-\\ for menu, Ctrl-C to quit"
99
+ end
100
+
101
+ # init
102
+ $last_feature = nil
103
+ prompt
104
+
105
+ # --------------------------------------------------
106
+ # Watchr Rules
107
+ # --------------------------------------------------
108
+ watch( '^features/(.*)\.feature' ) { run_default_cucumber }
109
+
110
+ watch( '^bin/(.*)' ) { run_default_cucumber }
111
+ watch( '^lib/(.*)' ) { run_default_cucumber }
112
+
113
+ watch( '^features/step_definitions/(.*)\.rb' ) { run_default_cucumber }
114
+ watch( '^features/support/(.*)\.rb' ) { run_default_cucumber }
115
+
116
+ watch( '^spec/(.*)_spec\.rb' ) { |m| run_spec(m[0]) }
117
+ # specify just the lib files that have specs
118
+ # TODO: This can be determined automatically from the spec file naming convention
119
+ watch( '^lib/(.*)' ) { run_default_spec }
120
+ watch( '^spec/applications/(.*)' ) { run_default_spec }
121
+
122
+ # --------------------------------------------------
123
+ # Signal Handling
124
+ # --------------------------------------------------
125
+
126
+ # Ctrl-\
127
+ Signal.trap('QUIT') do
128
+
129
+ puts "\n\nMENU: a = all , f = features s = specs, l = last feature (#{$last_feature ? $last_feature : 'none'}), q = quit\n\n"
130
+ c = getch
131
+ puts c.chr
132
+ if c.chr == "a"
133
+ run_all
134
+ elsif c.chr == "f"
135
+ run_default_cucumber
136
+ elsif c.chr == "s"
137
+ run_all_specs
138
+ elsif c.chr == "q"
139
+ abort("exiting\n")
140
+ elsif c.chr == "l"
141
+ run_last_feature
142
+ end
143
+
144
+ end