winwindow 0.4.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.
@@ -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