winwindow 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,88 @@
1
+ # extensions to the language which are external to what the WinWindow library itself does
2
+
3
+ class WinWindow # :nodoc:all
4
+
5
+ # takes given options and default options, and optionally a list of additional allowed keys not specified in default options
6
+ # (this is useful when you want to pass options along to another function but don't want to specify a default that will
7
+ # clobber that function's default)
8
+ # raises ArgumentError if the given options have an invalid key (defined as one not
9
+ # specified in default options or other_allowed_keys), and sets default values in given options where nothing is set.
10
+ def self.handle_options(given_options, default_options, other_allowed_keys=[]) # :nodoc:
11
+ given_options=given_options.dup
12
+ unless (unknown_keys=(given_options.keys-default_options.keys-other_allowed_keys)).empty?
13
+ raise ArgumentError, "Unknown options: #{(given_options.keys-default_options.keys).map(&:inspect).join(', ')}. Known options are #{(default_options.keys+other_allowed_keys).map(&:inspect).join(', ')}"
14
+ end
15
+ (default_options.keys-given_options.keys).each do |key|
16
+ given_options[key]=default_options[key]
17
+ end
18
+ given_options
19
+ end
20
+
21
+ def handle_options(*args) # :nodoc:
22
+ self.class.handle_options(*args)
23
+ end
24
+ # Default exception class raised by Waiter when a timeuot is reached
25
+ class WaiterError < StandardError # :nodoc:
26
+ end
27
+ module Waiter # :nodoc:all
28
+ # Tries for +time+ seconds to get the desired result from the given block. Stops when either:
29
+ # 1. The :condition option (which should be a proc) returns true (that is, not false or nil)
30
+ # 2. The block returns true (that is, anything but false or nil) if no :condition option is given
31
+ # 3. The specified amount of time has passed. By default a WaiterError is raised.
32
+ # If :exception option is given, then if it is nil, no exception is raised; otherwise it should be
33
+ # an exception class or an exception instance which will be raised instead of WaiterError
34
+ #
35
+ # Returns the value of the block, which can be handy for things that return nil on failure and some
36
+ # other object on success, like Enumerable#detect for example:
37
+ # found_thing=Waiter.try_for(30){ all_things().detect{|thing| thing.name=="Bill" } }
38
+ #
39
+ # Examples:
40
+ # Waiter.try_for(30) do
41
+ # Time.now.year == 2015
42
+ # end
43
+ # Raises a WaiterError unless it is called between the last 30 seconds of December 31, 2014 and the end of 2015
44
+ #
45
+ # Waiter.try_for(365*24*60*60, :interval => 0.1, :exception => nil, :condition => proc{ 2+2==5 }) do
46
+ # STDERR.puts "any decisecond now ..."
47
+ # end
48
+ #
49
+ # Complains to STDERR for one year, every tenth of a second, as long as 2+2 does not equal 5. Does not
50
+ # raise an exception if 2+2 does not become equal to 5.
51
+ def self.try_for(time, options={})
52
+ unless time.is_a?(Numeric) && options.is_a?(Hash)
53
+ raise TypeError, "expected arguments are time (a numeric) and, optionally, options (a Hash). received arguments #{time.inspect} (#{time.class}), #{options.inspect} (#{options.class})"
54
+ end
55
+ options=WinWindow.handle_options(options, {:interval => 0.02, :condition => proc{|_ret| _ret}, :exception => WaiterError})
56
+ started=Time.now
57
+ begin
58
+ ret=yield
59
+ break if options[:condition].call(ret)
60
+ sleep options[:interval]
61
+ end while Time.now < started+time && !options[:condition].call(ret)
62
+ if options[:exception] && !options[:condition].call(ret)
63
+ ex=if options[:exception].is_a?(Class)
64
+ options[:exception].new("Waiter waited #{time} seconds and condition was not met")
65
+ else
66
+ options[:exception]
67
+ end
68
+ raise ex
69
+ end
70
+ ret
71
+ end
72
+ end
73
+ end
74
+ module Kernel # :nodoc:
75
+ # this is the Y-combinator, which allows anonymous recursive functions. for a simple example,
76
+ # to define a recursive function to return the length of an array:
77
+ #
78
+ # length = ycomb do |len|
79
+ # proc{|list| list == [] ? 0 : len.call(list[1..-1]) }
80
+ # end
81
+ #
82
+ # see https://secure.wikimedia.org/wikipedia/en/wiki/Fixed_point_combinator#Y_combinator
83
+ # and chapter 9 of the little schemer, available as the sample chapter at http://www.ccs.neu.edu/home/matthias/BTLS/
84
+ def ycomb
85
+ proc{|f| f.call(f) }.call(proc{|f| yield proc{|*x| f.call(f).call(*x) } })
86
+ end
87
+ module_function :ycomb
88
+ end
@@ -0,0 +1,292 @@
1
+ libdir = File.join(File.dirname(__FILE__), '..', 'lib')
2
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.any?{|p| File.expand_path(p) == File.expand_path(libdir) }
3
+
4
+ require 'winwindow'
5
+
6
+ require 'minitest/unit'
7
+ MiniTest::Unit.autorun
8
+
9
+ class TestWinWindow < MiniTest::Unit::TestCase
10
+ def setup
11
+ @ie_ole, @win = *new_ie_ole
12
+ end
13
+
14
+ def teardown
15
+ @ie_ole.Quit
16
+ end
17
+
18
+ # make a new IE win32ole window for testing. also send it to the given url (default is google) and wait for that to load.
19
+ def new_ie_ole(url='http://google.com')
20
+ require 'win32ole'
21
+ ie_ole=nil
22
+ WinWindow::Waiter.try_for(32) do
23
+ begin
24
+ ie_ole = WIN32OLE.new('InternetExplorer.Application')
25
+ ie_ole.Visible=true
26
+ true
27
+ rescue WIN32OLERuntimeError, NoMethodError
28
+ false
29
+ end
30
+ end
31
+ ie_ole.Navigate url
32
+ WinWindow::Waiter.try_for(32, :exception => "The browser's readyState did not become ready for interaction") do
33
+ [3, 4].include?(ie_ole.readyState)
34
+ end
35
+ [ie_ole, WinWindow.new(ie_ole.HWND)]
36
+ end
37
+
38
+ def with_ie(*args)
39
+ ie_ole, win = *new_ie_ole(*args)
40
+ begin
41
+ yield ie_ole, win
42
+ ensure
43
+ ie_ole.Quit
44
+ end
45
+ end
46
+
47
+ def launch_popup(text="popup!", ie_ole=@ie_ole)
48
+ raise ArgumentError, "No double-quotes or backslashes, please" if text =~ /["\\]/
49
+ ie_ole.Navigate('javascript:alert("'+text+'")')
50
+ popup = WinWindow::Waiter.try_for(16, :exception => "No popup appeared on the browser!"){ WinWindow.new(@ie_ole.HWND).enabled_popup }
51
+ end
52
+
53
+ def with_popup(text="popup!", ie_ole=@ie_ole)
54
+ popup = launch_popup(text, ie_ole)
55
+ begin
56
+ yield popup
57
+ ensure
58
+ popup.click_child_button_try_for!(//, 8)
59
+ end
60
+ end
61
+
62
+ def assert_eventually(message=nil, options={}, &block)
63
+ options[:exception] ||= nil
64
+ timeout = options.delete(:timeout) || 16
65
+ result = WinWindow::Waiter.try_for(timeout, options, &block)
66
+ message ||= "Expected block to eventually yield true; after #{timeout} seconds it was #{result}"
67
+ assert result, message
68
+ end
69
+
70
+ def test_hwnd
71
+ assert_equal @win.hwnd, @ie_ole.HWND
72
+ end
73
+ def test_inspect
74
+ assert_includes @win.inspect, @win.hwnd.to_s
75
+ assert_includes @win.inspect, @win.retrieve_text
76
+ end
77
+ def test_text
78
+ assert_match /google/i, @win.retrieve_text
79
+ assert_match /google/i, @win.text # might fail? it is not meant to retrieve text of a control in another application? well, it seems to work.
80
+ end
81
+ def test_set_text
82
+ assert_eventually do
83
+ @win.set_text! 'foobar'
84
+ @win.text=='foobar'
85
+ end
86
+ assert_eventually do
87
+ @win.send_set_text! 'bazqux'
88
+ @win.retrieve_text=='bazqux'
89
+ end
90
+ end
91
+ def test_popup
92
+ text='popup!'
93
+ with_popup(text) do
94
+ assert_instance_of(WinWindow, @win.enabled_popup)
95
+ assert(@win.enabled_popup.children.any?{|child| child.text.include?(text) }, "Enabled popup should include the text #{text}")
96
+ assert_equal(@win.enabled_popup, @win.last_active_popup)
97
+ end
98
+ end
99
+ def test_owner_ancestors_parent_child
100
+ with_popup do |popup|
101
+ assert_equal(popup.owner, @win)
102
+ assert_equal(popup.ancestor_parent, WinWindow.desktop_window)
103
+ assert_equal(popup.ancestor_root, popup)
104
+ assert_equal(popup.ancestor_root_owner, @win)
105
+ assert_equal(popup.parent, @win)
106
+ button = popup.child_button(//)
107
+ assert_equal(button.owner, nil)
108
+ assert_equal(button.ancestor_parent, popup)
109
+ assert_equal(button.ancestor_root, popup)
110
+ assert_equal(button.ancestor_root_owner, @win)
111
+ assert_equal(button.parent, popup)
112
+ assert(button.child_of?(popup))
113
+ end
114
+ end
115
+ def test_set_parent
116
+ with_ie do |ie_ole2, win2|
117
+ with_popup do |popup|
118
+ assert_equal WinWindow.desktop_window, popup.ancestor_parent
119
+ popup.set_parent! win2
120
+ assert_equal win2, popup.ancestor_parent
121
+ end
122
+ end
123
+ end
124
+ def test_hung_app
125
+ assert !@win.hung_app?
126
+ # I don't know how to make IE freeze to test the true case (you wouldn't expect making IE freeze to be a diffult thing, would you?)
127
+ end
128
+ def test_class_name
129
+ with_popup do |popup|
130
+ assert popup.children.any?{|child| child.class_name=="Static"}
131
+ assert popup.children.any?{|child| child.class_name=="Button"}
132
+ assert popup.real_class_name == popup.class_name
133
+ end
134
+ end
135
+ def test_thread_id_process_id
136
+ # I don't know how to properly test this. just check that it looks id-like
137
+ assert @win.thread_id.is_a?(Integer)
138
+ assert @win.thread_id > 0
139
+ assert @win.process_id.is_a?(Integer)
140
+ assert @win.process_id > 0
141
+ end
142
+ def test_exists
143
+ temp_ie, twin = *new_ie_ole
144
+ assert twin.exists?
145
+ temp_ie.Quit
146
+ assert_block do
147
+ WinWindow::Waiter.try_for(8) do
148
+ !twin.exists?
149
+ end
150
+ end
151
+ end
152
+ def test_visible
153
+ @ie_ole.Visible = true
154
+ assert @win.visible?
155
+ @ie_ole.Visible = false
156
+ assert !@win.visible?
157
+ end
158
+ def test_min_max_iconic_foreground
159
+ with_ie do |ie_ole2, win2|
160
+ @win.close! # this is actually minimize
161
+ assert @win.iconic?
162
+ @win.really_set_foreground!
163
+ assert @win.foreground?
164
+ win2.really_set_foreground!
165
+ assert win2.foreground?
166
+ # I don't know what to do with the rest of these - no idea what the differenc
167
+ # is between most of them, and no real way to test their effects.
168
+ # but there's not really much to screw up.
169
+ # I'll just assume that if they don't error all is well.
170
+ @win.set_foreground!
171
+ @win.switch_to!
172
+ @win.bring_to_top!
173
+ @win.hide!
174
+ @win.show_normal!
175
+ @win.show_minimized!
176
+ @win.show_maximized!
177
+ @win.maximize!
178
+ @win.show_no_activate!
179
+ @win.show!
180
+ @win.minimize!
181
+ @win.show_min_no_active!
182
+ @win.show_na!
183
+ @win.restore!
184
+ @win.show_default!
185
+ @win.force_minimize!
186
+ end
187
+ end
188
+ def test_end_close_destroy
189
+ @win.destroy!
190
+ # no idea when this ever does anything, just seems to return false. will be content with it not erroring, I suppose.
191
+ @ie_ole, @win = *new_ie_ole unless @win.exists?
192
+
193
+ assert_eventually do
194
+ @win.really_set_foreground!
195
+ @win.end_task!
196
+ !@win.exists?
197
+ end
198
+ @ie_ole, @win = *new_ie_ole unless @win.exists?
199
+
200
+ assert_eventually do
201
+ @win.really_set_foreground!
202
+ @win.send_close!
203
+ !@win.exists?
204
+ end
205
+ @ie_ole, @win = *new_ie_ole unless @win.exists?
206
+ end
207
+ def test_click
208
+ # dismissing the popup relies on clicking; we'll use that to test.
209
+ with_popup do
210
+ assert @win.enabled_popup
211
+ end
212
+ assert !@win.enabled_popup
213
+ end
214
+ def test_screen_capture
215
+ # writing to file tests all the screen capture functions (at least that they don't error)
216
+ filename = 'winwindow.bmp'
217
+ @win.capture_to_bmp_file filename
218
+ assert File.exists?(filename)
219
+ assert File.size(filename) > 0
220
+ # todo: check it's valid bmp with the right size (check #window_rect/#client_rect)? use rmagick?
221
+ File.unlink filename
222
+ end
223
+ def test_children_and_all
224
+ assert @win.children.is_a?(Enumerable)
225
+ assert WinWindow::All.is_a?(Enumerable)
226
+ cwins=[]
227
+ @win.each_child do |cwin|
228
+ assert cwin.is_a?(WinWindow)
229
+ assert cwin.exists?
230
+ assert cwin.child_of?(@win)
231
+ cwins << cwin
232
+ end
233
+ assert_equal @win.children.to_a, cwins
234
+ assert cwins.size > 0
235
+ all_wins = []
236
+ WinWindow.each_window do |win|
237
+ assert win.is_a?(WinWindow)
238
+ assert win.exists?
239
+ all_wins << win
240
+ end
241
+ assert_equal WinWindow::All.to_a, all_wins
242
+ assert all_wins.size > 0
243
+ end
244
+ def test_children_recursive
245
+ cwins = []
246
+ @win.recurse_each_child do |cwin|
247
+ assert cwin.is_a?(WinWindow)
248
+ assert cwin.exists?
249
+ #assert cwin.child_of?(@win)
250
+ cwins << cwin
251
+ end
252
+ assert_equal @win.children_recursive.to_a, cwins
253
+ require 'set'
254
+ assert Set.new(@win.children).subset?(Set.new(@win.children_recursive))
255
+ end
256
+ def test_system_error
257
+ assert_raises(WinWindow::SystemError) do
258
+ begin
259
+ @win.recurse_each_child(:rescue_enum_child_windows => false) { nil }
260
+ rescue WinWindow::SystemError # not really rescuing, just checking info
261
+ assert_equal 'EnumChildWindows', $!.function
262
+ assert $!.code.is_a?(Integer)
263
+ raise
264
+ end
265
+ end
266
+ end
267
+ def test_finding
268
+ assert WinWindow.find_first_by_text(//).is_a?(WinWindow)
269
+ found_any = false
270
+ WinWindow.find_all_by_text(//).each do |win|
271
+ assert win.is_a?(WinWindow)
272
+ assert win.exists?
273
+ found_any = true
274
+ end
275
+ assert found_any
276
+ assert(WinWindow.find_only_by_text(@win.retrieve_text)==@win)
277
+ with_ie do
278
+ assert_raises(WinWindow::MatchError) do
279
+ WinWindow::Waiter.try_for(32) do # this doesn't always come up immediately, so give it a moment
280
+ WinWindow.find_only_by_text(@win.retrieve_text)
281
+ false
282
+ end
283
+ end
284
+ end
285
+ end
286
+ def test_foreground_desktop
287
+ assert WinWindow.foreground_window.is_a?(WinWindow)
288
+ assert WinWindow.foreground_window.exists?
289
+ assert WinWindow.desktop_window.is_a?(WinWindow)
290
+ assert WinWindow.desktop_window.exists?
291
+ end
292
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: winwindow
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 4
8
+ - 0
9
+ version: 0.4.0
10
+ platform: ruby
11
+ authors:
12
+ - Ethan
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-09-01 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: ffi
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ - 5
30
+ - 4
31
+ version: 0.5.4
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ description: A Ruby library to wrap windows API calls relating to hWnd window handles.
35
+ email: vapir@googlegroups.com
36
+ executables: []
37
+
38
+ extensions: []
39
+
40
+ extra_rdoc_files: []
41
+
42
+ files:
43
+ - lib/winwindow.rb
44
+ - lib/winwindow/ext.rb
45
+ has_rdoc: true
46
+ homepage: http://winwindow.vapir.org/
47
+ licenses: []
48
+
49
+ post_install_message:
50
+ rdoc_options:
51
+ - --charset
52
+ - UTF-8
53
+ - --show-hash
54
+ - --inline-source
55
+ - --main
56
+ - WinWindow
57
+ - --title
58
+ - WinWindow
59
+ - --tab-width
60
+ - "2"
61
+ - lib/winwindow.rb
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ segments:
69
+ - 0
70
+ version: "0"
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ requirements:
79
+ - Microsoft Windows, probably with some sort of NT kernel
80
+ rubyforge_project:
81
+ rubygems_version: 1.3.6
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: A Ruby library to wrap windows API calls relating to hWnd window handles.
85
+ test_files:
86
+ - test/winwindow_test.rb