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.
- data/lib/winwindow.rb +1364 -0
- data/lib/winwindow/ext.rb +88 -0
- data/test/winwindow_test.rb +292 -0
- metadata +86 -0
@@ -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
|