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,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