AXElements 0.6.0beta2 → 0.7.5
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/.yardopts +1 -2
- data/README.markdown +152 -88
- data/Rakefile +8 -103
- data/docs/Debugging.markdown +9 -2
- data/docs/KeyboardEvents.markdown +114 -49
- data/docs/Setting.markdown +1 -0
- data/docs/images/next_version.png +0 -0
- data/ext/accessibility/key_coder/extconf.rb +22 -0
- data/ext/accessibility/key_coder/key_coder.c +113 -0
- data/lib/AXElements.rb +2 -0
- data/lib/accessibility/core.rb +897 -0
- data/lib/accessibility/debug.rb +168 -0
- data/lib/accessibility/dsl.rb +697 -0
- data/lib/accessibility/enumerators.rb +104 -0
- data/lib/accessibility/errors.rb +32 -0
- data/lib/accessibility/factory.rb +153 -0
- data/lib/accessibility/graph.rb +150 -0
- data/lib/{ax_elements/inspector.rb → accessibility/pp_inspector.rb} +39 -28
- data/lib/accessibility/qualifier.rb +158 -0
- data/lib/accessibility/string.rb +494 -0
- data/lib/accessibility/translator.rb +178 -0
- data/lib/accessibility/version.rb +7 -0
- data/lib/accessibility.rb +79 -0
- data/lib/ax/application.rb +234 -0
- data/lib/{ax_elements/elements → ax}/button.rb +2 -0
- data/lib/ax/element.rb +518 -0
- data/lib/{ax_elements/elements → ax}/radio_button.rb +2 -0
- data/lib/ax/row.rb +37 -0
- data/lib/{ax_elements/elements → ax}/static_text.rb +2 -0
- data/lib/ax/systemwide.rb +86 -0
- data/lib/ax_elements/awesome_print.rb +25 -0
- data/lib/ax_elements/exception_workaround.rb +8 -0
- data/lib/ax_elements/nsarray_compat.rb +64 -0
- data/lib/ax_elements/vendor/inflection_data.rb +65 -0
- data/lib/ax_elements/vendor/inflections.rb +172 -0
- data/lib/ax_elements/vendor/inflector.rb +306 -0
- data/lib/ax_elements.rb +14 -25
- data/lib/minitest/ax_elements.rb +112 -12
- data/lib/mouse.rb +72 -46
- data/lib/rspec/expectations/ax_elements.rb +133 -6
- data/rakelib/doc.rake +13 -0
- data/rakelib/ext.rake +61 -0
- data/rakelib/gem.rake +28 -0
- data/rakelib/test.rake +53 -0
- data/test/helper.rb +11 -97
- data/test/integration/accessibility/test_core.rb +18 -0
- data/test/integration/accessibility/test_debug.rb +44 -0
- data/test/integration/accessibility/test_dsl.rb +225 -0
- data/test/integration/accessibility/test_enumerators.rb +122 -0
- data/test/integration/accessibility/test_errors.rb +38 -0
- data/test/integration/accessibility/test_notifications.rb +22 -0
- data/test/integration/accessibility/test_qualifier.rb +148 -0
- data/test/integration/ax/test_application.rb +56 -0
- data/test/integration/ax/test_element.rb +46 -0
- data/test/integration/ax/test_row.rb +23 -0
- data/test/integration/ax_elements/test_nsarray_compat.rb +43 -0
- data/test/integration/minitest/test_ax_elements.rb +98 -0
- data/test/integration/rspec/expectations/test_ax_elements.rb +58 -0
- data/test/integration/test_mouse.rb +35 -0
- data/test/sanity/accessibility/test_core.rb +553 -0
- data/test/sanity/accessibility/test_debug.rb +63 -0
- data/test/sanity/accessibility/test_dsl.rb +75 -0
- data/test/sanity/accessibility/test_errors.rb +10 -0
- data/test/sanity/accessibility/test_factory.rb +88 -0
- data/test/sanity/accessibility/test_pp_inspector.rb +110 -0
- data/test/sanity/accessibility/test_qualifier.rb +13 -0
- data/test/sanity/accessibility/test_string.rb +238 -0
- data/test/sanity/accessibility/test_translator.rb +145 -0
- data/test/sanity/ax/test_application.rb +90 -0
- data/test/sanity/ax/test_element.rb +80 -0
- data/test/sanity/ax/test_systemwide.rb +66 -0
- data/test/sanity/ax_elements/test_nsarray_compat.rb +16 -0
- data/test/sanity/ax_elements/test_nsobject_inspect.rb +11 -0
- data/test/sanity/minitest/test_ax_elements.rb +15 -0
- data/test/sanity/rspec/expectations/test_ax_elements.rb +12 -0
- data/test/sanity/test_ax_elements.rb +10 -0
- data/test/sanity/test_mouse.rb +19 -0
- metadata +111 -93
- data/LICENSE.txt +0 -25
- data/ext/key_coder/extconf.rb +0 -6
- data/ext/key_coder/key_coder.m +0 -77
- data/lib/ax_elements/accessibility/enumerators.rb +0 -104
- data/lib/ax_elements/accessibility/graph.rb +0 -118
- data/lib/ax_elements/accessibility/language.rb +0 -347
- data/lib/ax_elements/accessibility/qualifier.rb +0 -73
- data/lib/ax_elements/accessibility.rb +0 -166
- data/lib/ax_elements/core.rb +0 -541
- data/lib/ax_elements/element.rb +0 -593
- data/lib/ax_elements/elements/application.rb +0 -88
- data/lib/ax_elements/elements/row.rb +0 -30
- data/lib/ax_elements/elements/systemwide.rb +0 -46
- data/lib/ax_elements/macruby_extensions.rb +0 -255
- data/lib/ax_elements/notification.rb +0 -37
- data/lib/ax_elements/version.rb +0 -9
- data/test/elements/test_application.rb +0 -72
- data/test/elements/test_row.rb +0 -27
- data/test/elements/test_systemwide.rb +0 -38
- data/test/test_accessibility.rb +0 -127
- data/test/test_blankness.rb +0 -26
- data/test/test_core.rb +0 -448
- data/test/test_element.rb +0 -939
- data/test/test_enumerators.rb +0 -81
- data/test/test_inspector.rb +0 -130
- data/test/test_language.rb +0 -157
- data/test/test_macruby_extensions.rb +0 -303
- data/test/test_mouse.rb +0 -5
- data/test/test_search_semantics.rb +0 -143
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require 'mouse'
|
|
4
|
+
require 'ax/element'
|
|
5
|
+
require 'ax/application'
|
|
6
|
+
require 'ax/systemwide'
|
|
7
|
+
require 'accessibility'
|
|
8
|
+
require 'accessibility/debug'
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
# @todo Allow the animation duration to be overridden for Mouse stuff?
|
|
12
|
+
#
|
|
13
|
+
# DSL methods for AXElements.
|
|
14
|
+
#
|
|
15
|
+
# The idea here is to pull actions out from an object and put them
|
|
16
|
+
# in front of object to give AXElements more of a DSL feel to make
|
|
17
|
+
# communicating test steps more clear. See the
|
|
18
|
+
# {file:docs/Acting.markdown Acting tutorial} for examples on how to use
|
|
19
|
+
# methods from this module.
|
|
20
|
+
module Accessibility::DSL
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# @group Actions
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# We assume that any method that has the first argument with a type
|
|
27
|
+
# of {AX::Element} is intended to be an action and so `#method_missing`
|
|
28
|
+
# will forward the message to the element.
|
|
29
|
+
#
|
|
30
|
+
# @param [String] method an action constant
|
|
31
|
+
def method_missing meth, *args
|
|
32
|
+
arg = args.first
|
|
33
|
+
if arg.kind_of? AX::Element
|
|
34
|
+
return arg.perform meth if arg.actions.include? meth
|
|
35
|
+
raise ArgumentError, "`#{meth}' is not an action of #{self}:#{self.class}"
|
|
36
|
+
end
|
|
37
|
+
# @todo do we still need this? we should just call super
|
|
38
|
+
# should be able to just call super, but there is a bug in MacRuby (#1320)
|
|
39
|
+
# so we just recreate what should be happening
|
|
40
|
+
message = "undefined method `#{meth}' for #{self}:#{self.class}"
|
|
41
|
+
raise NoMethodError, message, caller(1)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# Try to perform the `press` action on the given element.
|
|
46
|
+
#
|
|
47
|
+
# @param [AX::Element]
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
def press element
|
|
50
|
+
element.perform :press
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# Try to perform the `show_menu` action on the given element.
|
|
55
|
+
#
|
|
56
|
+
# @param [AX::Element]
|
|
57
|
+
# @return [Boolean]
|
|
58
|
+
def show_menu element
|
|
59
|
+
element.perform :show_menu
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
##
|
|
63
|
+
# Try to perform the `pick` action on the given element.
|
|
64
|
+
#
|
|
65
|
+
# @param [AX::Element]
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
def pick element
|
|
68
|
+
element.perform :pick
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Try to perform the `decrement` action on the given element.
|
|
73
|
+
#
|
|
74
|
+
# @param [AX::Element]
|
|
75
|
+
# @return [Boolean]
|
|
76
|
+
def decrement element
|
|
77
|
+
element.perform :decrement
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
##
|
|
81
|
+
# Try to perform the `confirm` action on the given element.
|
|
82
|
+
#
|
|
83
|
+
# @param [AX::Element]
|
|
84
|
+
# @return [Boolean]
|
|
85
|
+
def confirm element
|
|
86
|
+
element.perform :confirm
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
##
|
|
90
|
+
# Try to perform the `increment` action on the given element.
|
|
91
|
+
#
|
|
92
|
+
# @param [AX::Element]
|
|
93
|
+
# @return [Boolean]
|
|
94
|
+
def increment element
|
|
95
|
+
element.perform :increment
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
##
|
|
99
|
+
# Try to perform the `delete` action on the given element.
|
|
100
|
+
#
|
|
101
|
+
# @param [AX::Element]
|
|
102
|
+
# @return [Boolean]
|
|
103
|
+
def delete element
|
|
104
|
+
element.perform :delete
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
##
|
|
108
|
+
# Try to perform the `cancel` action on the given element.
|
|
109
|
+
#
|
|
110
|
+
# @param [AX::Element]
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
def cancel element
|
|
113
|
+
element.perform :cancel
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
##
|
|
117
|
+
# Tell an app to hide itself.
|
|
118
|
+
#
|
|
119
|
+
# @param [AX::Application]
|
|
120
|
+
# @return [Boolean]
|
|
121
|
+
def hide app
|
|
122
|
+
app.perform :hide
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
##
|
|
126
|
+
# Tell an app to unhide itself. This does not guarantee it will be
|
|
127
|
+
# focused.
|
|
128
|
+
#
|
|
129
|
+
# @param [AX::Application]
|
|
130
|
+
# @return [Boolean]
|
|
131
|
+
def unhide app
|
|
132
|
+
app.perform :unhide
|
|
133
|
+
end
|
|
134
|
+
alias_method :show, :unhide
|
|
135
|
+
|
|
136
|
+
##
|
|
137
|
+
# Tell an app to quit.
|
|
138
|
+
#
|
|
139
|
+
# @param [AX::Application]
|
|
140
|
+
# @return [Boolean]
|
|
141
|
+
def terminate app
|
|
142
|
+
app.perform :terminate
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
##
|
|
146
|
+
# Find the application with the given bundle identifier.
|
|
147
|
+
# If the application is not already running, it will be
|
|
148
|
+
# launched.
|
|
149
|
+
#
|
|
150
|
+
# @example
|
|
151
|
+
#
|
|
152
|
+
# app_with_identifier 'com.apple.finder'
|
|
153
|
+
#
|
|
154
|
+
# @param [String]
|
|
155
|
+
# @return [AX::Application]
|
|
156
|
+
def app_with_bundle_identifier id
|
|
157
|
+
Accessibility.application_with_bundle_identifier id
|
|
158
|
+
end
|
|
159
|
+
alias_method :app_with_bundle_id, :app_with_bundle_identifier
|
|
160
|
+
alias_method :launch, :app_with_bundle_identifier
|
|
161
|
+
|
|
162
|
+
##
|
|
163
|
+
# Find the application with the given name.
|
|
164
|
+
#
|
|
165
|
+
# @example
|
|
166
|
+
#
|
|
167
|
+
# app_with_name 'Finder'
|
|
168
|
+
#
|
|
169
|
+
# @param [String]
|
|
170
|
+
# @return [AX::Application,nil]
|
|
171
|
+
def app_with_name name
|
|
172
|
+
AX::Application.new name
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
##
|
|
176
|
+
# Find the application with the given process identifier.
|
|
177
|
+
#
|
|
178
|
+
# @example
|
|
179
|
+
#
|
|
180
|
+
# app_with_pid 35843
|
|
181
|
+
#
|
|
182
|
+
# @param [Fixnum]
|
|
183
|
+
# @return [AX::Application]
|
|
184
|
+
def app_with_pid pid
|
|
185
|
+
AX::Application.new pid
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
##
|
|
189
|
+
# @note This method overrides `Kernel#raise` so we have to check the
|
|
190
|
+
# class of the first argument to decide which code path to take.
|
|
191
|
+
#
|
|
192
|
+
# Try to perform the `raise` action on the given element.
|
|
193
|
+
#
|
|
194
|
+
# @overload raise element
|
|
195
|
+
# @param [AX::Element] element
|
|
196
|
+
# @return [Boolean]
|
|
197
|
+
#
|
|
198
|
+
# @overload raise exception[, message[, backtrace]]
|
|
199
|
+
# The normal way to raise an exception.
|
|
200
|
+
def raise *args
|
|
201
|
+
arg = args.first
|
|
202
|
+
# @todo Need to check if arg has the raise action
|
|
203
|
+
arg.kind_of?(AX::Element) ? arg.perform(:raise) : super
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
##
|
|
207
|
+
# Focus an element on the screen if it can be focused. It is safe to
|
|
208
|
+
# pass any element into this method as nothing will happen if it is
|
|
209
|
+
# not capable of having focus set on it.
|
|
210
|
+
#
|
|
211
|
+
# @param [AX::Element]
|
|
212
|
+
def set_focus_to element
|
|
213
|
+
element.set(:focused, true) if element.respond_to? :focused?
|
|
214
|
+
end
|
|
215
|
+
alias_method :set_focus, :set_focus_to
|
|
216
|
+
|
|
217
|
+
##
|
|
218
|
+
# Set the value of an attribute on an element.
|
|
219
|
+
#
|
|
220
|
+
# This method will try to set focus to the element first; this is
|
|
221
|
+
# to avoid cases where developers assumed an element would have
|
|
222
|
+
# to have focus before a user could change the value.
|
|
223
|
+
#
|
|
224
|
+
# @overload set element, attribute_name: new_value
|
|
225
|
+
# Set a specified attribute to a new value
|
|
226
|
+
# @param [AX::Element] element
|
|
227
|
+
# @param [Hash{attribute_name=>new_value}] change
|
|
228
|
+
#
|
|
229
|
+
# @example
|
|
230
|
+
#
|
|
231
|
+
# set text_field, selected_text_range: CFRangeMake(1,10)
|
|
232
|
+
#
|
|
233
|
+
# @overload set element, new_value
|
|
234
|
+
# Set the `value` attribute to a new value
|
|
235
|
+
# @param [AX::Element] element
|
|
236
|
+
# @param [Object] change
|
|
237
|
+
#
|
|
238
|
+
# @example
|
|
239
|
+
#
|
|
240
|
+
# set text_field, 'Mark Rada'
|
|
241
|
+
# set radio_button, 1
|
|
242
|
+
#
|
|
243
|
+
# @return [nil] do not rely on a return value
|
|
244
|
+
def set element, change
|
|
245
|
+
if element.respond_to? :focused
|
|
246
|
+
if element.attribute_writable? :focused
|
|
247
|
+
element.set :focused, true
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
return element.set :value, change unless change.kind_of? Hash
|
|
252
|
+
key, value = change.first
|
|
253
|
+
return element.set key, value
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
##
|
|
257
|
+
# Simulate keyboard input by typing out the given string. To learn
|
|
258
|
+
# more about how to encode modifier keys (e.g. Command), see the
|
|
259
|
+
# dedicated documentation page on
|
|
260
|
+
# {file:docs/KeyboardEvents.markdown Keyboard Events}.
|
|
261
|
+
#
|
|
262
|
+
# @overload type string
|
|
263
|
+
# Send input to the currently focused application
|
|
264
|
+
# @param [#to_s]
|
|
265
|
+
#
|
|
266
|
+
# @overload type string, app
|
|
267
|
+
# Send input to a specific application
|
|
268
|
+
# @param [#to_s]
|
|
269
|
+
# @param [AX::Application]
|
|
270
|
+
def type string, app = system_wide
|
|
271
|
+
sleep 0.1
|
|
272
|
+
app.type_string string.to_s
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
##
|
|
276
|
+
# Navigate the menu bar menus for the given application and select
|
|
277
|
+
# the last item in the chain.
|
|
278
|
+
#
|
|
279
|
+
# @example
|
|
280
|
+
#
|
|
281
|
+
# mail = app_with_name 'Mail'
|
|
282
|
+
# select_menu_item mail, 'View', 'Sort By', 'Subject'
|
|
283
|
+
# select_menu_item mail, 'Edit', /Spelling/, /show spelling/i
|
|
284
|
+
#
|
|
285
|
+
# @param [AX::Application]
|
|
286
|
+
# @param [String,Regexp] path
|
|
287
|
+
# @return [Boolean]
|
|
288
|
+
def select_menu_item app, *path
|
|
289
|
+
app.select_menu_item *path
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# @group Notifications
|
|
294
|
+
|
|
295
|
+
##
|
|
296
|
+
# Register for a notification from a specific element.
|
|
297
|
+
#
|
|
298
|
+
# @param [#to_s]
|
|
299
|
+
# @param [Array(#to_s,AX::Element)]
|
|
300
|
+
def register_for notif, from: element, &block
|
|
301
|
+
@registered_elements ||= []
|
|
302
|
+
@registered_elements << element
|
|
303
|
+
element.on_notification notif, &block
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
##
|
|
307
|
+
# @deprecated This API exists for backwards compatability only
|
|
308
|
+
#
|
|
309
|
+
# Register for a notification from a specific element.
|
|
310
|
+
#
|
|
311
|
+
# @param [AX::Element]
|
|
312
|
+
# @param [String]
|
|
313
|
+
def register_for_notification element, notif, &block
|
|
314
|
+
register_for notif, from: element, &block
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
##
|
|
318
|
+
# Pause script execution until notification that has been registered
|
|
319
|
+
# for is received or the full timeout period has passed.
|
|
320
|
+
#
|
|
321
|
+
# If the script is unpaused because of a timeout, then it is assumed
|
|
322
|
+
# that the notification was never received and all notification
|
|
323
|
+
# registrations will be unregistered to avoid future complications.
|
|
324
|
+
#
|
|
325
|
+
# @param [Float] timeout number of seconds to wait for a notification
|
|
326
|
+
# @return [Boolean]
|
|
327
|
+
def wait_for_notification timeout = 10.0
|
|
328
|
+
# We use RunInMode because it has timeout functionality
|
|
329
|
+
case CFRunLoopRunInMode(KCFRunLoopDefaultMode, timeout, false)
|
|
330
|
+
when KCFRunLoopRunStopped then true
|
|
331
|
+
when KCFRunLoopRunTimedOut then false.tap { |_| unregister_notifications }
|
|
332
|
+
when KCFRunLoopFinished then
|
|
333
|
+
raise RuntimeError, 'The run loop was not configured properly'
|
|
334
|
+
when KCFRunLoopRunHandledSource then
|
|
335
|
+
raise RuntimeError, 'Did you start your own run loop?'
|
|
336
|
+
else
|
|
337
|
+
raise 'You just found a bug, might be yours, or OS X, or MacRuby...'
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
##
|
|
342
|
+
# Undo _all_ notification registries.
|
|
343
|
+
def unregister_notifications
|
|
344
|
+
return unless @registered_elements
|
|
345
|
+
@registered_elements.each do |element|
|
|
346
|
+
element.unregister_notifications
|
|
347
|
+
end
|
|
348
|
+
@registered_elements = []
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# @group Polling
|
|
353
|
+
|
|
354
|
+
##
|
|
355
|
+
# Simply wait around for something to show up. This method is similar to
|
|
356
|
+
# performing an explicit search on an element except that the search filters
|
|
357
|
+
# take two extra options which can control how long to wait and from where
|
|
358
|
+
# to start searches from. You __MUST__ supply either the parent or ancestor
|
|
359
|
+
# options to specify where to search from. Searching from the parent implies
|
|
360
|
+
# that what you are waiting for is a child of the parent and not a more
|
|
361
|
+
# distant descendant.
|
|
362
|
+
#
|
|
363
|
+
# This is an alternative to using the notifications system. It is far
|
|
364
|
+
# easier to use than notifications in most cases, but it will perform
|
|
365
|
+
# more slowly (and without all the fun crashes).
|
|
366
|
+
#
|
|
367
|
+
# @example
|
|
368
|
+
#
|
|
369
|
+
# # Waiting for a dialog window to show up
|
|
370
|
+
# wait_for :dialog, parent: app
|
|
371
|
+
#
|
|
372
|
+
# # Waiting for a hypothetical email from Mark Rada to appear
|
|
373
|
+
# wait_for :static_text, value: 'Mark Rada', ancestor: mail.main_window
|
|
374
|
+
#
|
|
375
|
+
# # Waiting for something that will never show up
|
|
376
|
+
# wait_for :a_million_dollars, ancestor: fruit_basket, timeout: 1000000
|
|
377
|
+
#
|
|
378
|
+
# @param [#to_s]
|
|
379
|
+
# @param [Hash] opts
|
|
380
|
+
# @options opts [Number] :timeout (15)
|
|
381
|
+
# @options opts [AX::Element] :parent
|
|
382
|
+
# @options opts [AX::Element] :ancestor
|
|
383
|
+
# @return [AX::Element,nil]
|
|
384
|
+
def wait_for element, opts = {}, &block
|
|
385
|
+
if opts.has_key? :ancestor
|
|
386
|
+
wait_for_descendant element, opts.delete(:ancestor), opts, &block
|
|
387
|
+
elsif opts.has_key? :parent
|
|
388
|
+
wait_for_child element, opts.delete(:parent), opts, &block
|
|
389
|
+
else
|
|
390
|
+
raise ArgumentError, 'parent/ancestor opt required'
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
##
|
|
395
|
+
# Wait around for particular element and then return that element.
|
|
396
|
+
# The options you pass to this method can be any search filter that
|
|
397
|
+
# you can normally use.
|
|
398
|
+
#
|
|
399
|
+
# @param [#to_s]
|
|
400
|
+
# @param [AX::Element]
|
|
401
|
+
# @param [Hash]
|
|
402
|
+
# @return [AX::Element,nil]
|
|
403
|
+
def wait_for_descendant descendant, ancestor, opts, &block
|
|
404
|
+
timeout = opts.delete(:timeout) || 15
|
|
405
|
+
start = Time.now
|
|
406
|
+
until Time.now - start > timeout
|
|
407
|
+
result = ancestor.search(descendant, opts, &block)
|
|
408
|
+
return result unless result.blank?
|
|
409
|
+
sleep 0.2
|
|
410
|
+
end
|
|
411
|
+
nil
|
|
412
|
+
end
|
|
413
|
+
alias_method :wait_for_descendent, :wait_for_descendant
|
|
414
|
+
|
|
415
|
+
##
|
|
416
|
+
# @note This is really just an optimized case of
|
|
417
|
+
# {#wait_for_descendant} when you know what you are waiting
|
|
418
|
+
# for is a child of a particular element.
|
|
419
|
+
#
|
|
420
|
+
# Wait around for particular element and then return that element.
|
|
421
|
+
# The parent option must be the parent of the element you are
|
|
422
|
+
# waiting for, this method will not look further down the hierarchy.
|
|
423
|
+
# The options you pass to this method can be any search filter that
|
|
424
|
+
# you can normally use.
|
|
425
|
+
#
|
|
426
|
+
# @param [#to_s]
|
|
427
|
+
# @param [AX::Element]
|
|
428
|
+
# @param [Hash]
|
|
429
|
+
# @return [AX::Element,nil]
|
|
430
|
+
def wait_for_child child, parent, opts, &block
|
|
431
|
+
timeout = opts.delete(:timeout) || 15
|
|
432
|
+
start = Time.now
|
|
433
|
+
q = Accessibility::Qualifier.new(child, opts, &block)
|
|
434
|
+
until Time.now - start > timeout
|
|
435
|
+
result = parent.attribute(:children).find { |x| q.qualifies? x }
|
|
436
|
+
return result unless result.blank?
|
|
437
|
+
sleep 0.2
|
|
438
|
+
end
|
|
439
|
+
nil
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
# @group Mouse Interaction
|
|
444
|
+
|
|
445
|
+
##
|
|
446
|
+
# Move the mouse cursor to the given point on the screen.
|
|
447
|
+
#
|
|
448
|
+
# @example
|
|
449
|
+
#
|
|
450
|
+
# move_mouse_to button
|
|
451
|
+
# move_mouse_to [344, 516]
|
|
452
|
+
# move_mouse_to CGPointMake(100, 100)
|
|
453
|
+
#
|
|
454
|
+
# @param [#to_point]
|
|
455
|
+
# @param [Hash] opts
|
|
456
|
+
# @option opts [Number] :duration
|
|
457
|
+
# @option opts [Number] :wait
|
|
458
|
+
def move_mouse_to arg, opts = {}
|
|
459
|
+
duration = opts[:duration] || 0.2
|
|
460
|
+
if Accessibility::Debug.on? && arg.respond_to?(:bounds)
|
|
461
|
+
highlight arg, timeout: duration, color: NSColor.orangeColor
|
|
462
|
+
end
|
|
463
|
+
Mouse.move_to arg.to_point, duration
|
|
464
|
+
sleep(opts[:wait] || 0.2)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
##
|
|
468
|
+
# Click and drag the mouse from its current position to the given
|
|
469
|
+
# position.
|
|
470
|
+
#
|
|
471
|
+
# There are many reasons why you would want to cause a drag event
|
|
472
|
+
# with the mouse. Perhaps you want to drag an object to another
|
|
473
|
+
# place, or maybe you want to hightlight an area of the screen.
|
|
474
|
+
#
|
|
475
|
+
# @param [#to_point]
|
|
476
|
+
def drag_mouse_to arg, opts = {}
|
|
477
|
+
Mouse.drag_to arg.to_point, (opts[:duration] || 0.2)
|
|
478
|
+
sleep(opts[:wait] || 0.2)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
##
|
|
482
|
+
# Move the mouse to the first point and then drag to the second point.
|
|
483
|
+
#
|
|
484
|
+
# @param [#to_point]
|
|
485
|
+
# @param [#to_point]
|
|
486
|
+
def drag arg1, to: arg2
|
|
487
|
+
move_mouse_to obj
|
|
488
|
+
drag_mouse_to obj2
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
##
|
|
493
|
+
# @todo Need to expose the units option? Would allow scrolling by pixel.
|
|
494
|
+
#
|
|
495
|
+
# Scrolls an arbitrary number of lines at the mouses current point on
|
|
496
|
+
# the screen. Use a positive number to scroll down, and a negative number
|
|
497
|
+
# to scroll up.
|
|
498
|
+
#
|
|
499
|
+
# If the second argument is provided then the mouse will move to that
|
|
500
|
+
# point first; the argument must respond to `#to_point`.
|
|
501
|
+
#
|
|
502
|
+
# @param [Number]
|
|
503
|
+
# @param [#to_point]
|
|
504
|
+
def scroll lines, obj = nil, wait = 0.1
|
|
505
|
+
move_mouse_to obj, wait: 0 if obj
|
|
506
|
+
Mouse.scroll lines
|
|
507
|
+
sleep wait
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
##
|
|
511
|
+
# Perform a regular click.
|
|
512
|
+
#
|
|
513
|
+
# If an argument is provided then the mouse will move to that point
|
|
514
|
+
# first; the argument must respond to `#to_point`.
|
|
515
|
+
#
|
|
516
|
+
# @param [#to_point]
|
|
517
|
+
def click obj = nil, wait = 0.2
|
|
518
|
+
move_mouse_to obj, wait: 0 if obj
|
|
519
|
+
Mouse.click
|
|
520
|
+
sleep wait
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
##
|
|
524
|
+
# Perform a right (aka secondary) click action.
|
|
525
|
+
#
|
|
526
|
+
# If an argument is provided then the mouse will move to that point
|
|
527
|
+
# first; the argument must respond to `#to_point`.
|
|
528
|
+
#
|
|
529
|
+
# @param [#to_point]
|
|
530
|
+
def right_click obj = nil, wait = 0.2
|
|
531
|
+
move_mouse_to obj, wait: 0 if obj
|
|
532
|
+
Mouse.right_click
|
|
533
|
+
sleep wait
|
|
534
|
+
end
|
|
535
|
+
alias_method :secondary_click, :right_click
|
|
536
|
+
|
|
537
|
+
##
|
|
538
|
+
# Perform a double click action.
|
|
539
|
+
#
|
|
540
|
+
# If an argument is provided then the mouse will move to that point
|
|
541
|
+
# first; the argument must respond to `#to_point`.
|
|
542
|
+
#
|
|
543
|
+
# @param [#to_point]
|
|
544
|
+
def double_click obj = nil, wait = 0.2
|
|
545
|
+
move_mouse_to obj, wait: 0 if obj
|
|
546
|
+
Mouse.double_click
|
|
547
|
+
sleep wait
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
# @group Debug
|
|
552
|
+
|
|
553
|
+
def highlight element, opts = {}
|
|
554
|
+
Accessibility::Debug.highlight element, opts
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def path_for element
|
|
558
|
+
Accessibility::Debug.path element
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def subtree_for element
|
|
562
|
+
# @todo Create Element#descendants
|
|
563
|
+
Accessibility::Debug.text_subtree element
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
##
|
|
567
|
+
# @note This is an unfinished feature
|
|
568
|
+
#
|
|
569
|
+
# Make a `dot` format graph of the tree, meant for graphing with
|
|
570
|
+
# GraphViz.
|
|
571
|
+
#
|
|
572
|
+
# @return [String]
|
|
573
|
+
def graph element, open = true
|
|
574
|
+
Accessibility::Debug.graph_subtree element
|
|
575
|
+
# @todo Use the `open` flag to decide if it should be sent to
|
|
576
|
+
# graphviz and opened right away
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
# @group Misc.
|
|
581
|
+
|
|
582
|
+
##
|
|
583
|
+
# Convenience for `AX::SystemWide.new`.
|
|
584
|
+
#
|
|
585
|
+
# @return [AX::SystemWide]
|
|
586
|
+
def system_wide
|
|
587
|
+
AX::SystemWide.new
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# @group Macros
|
|
592
|
+
|
|
593
|
+
##
|
|
594
|
+
# Get the current mouse position and return the top most element at
|
|
595
|
+
# that point.
|
|
596
|
+
#
|
|
597
|
+
# @return [AX::Element]
|
|
598
|
+
def element_under_mouse
|
|
599
|
+
element_at_point Mouse.current_position
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
##
|
|
603
|
+
# Get the top most object at an arbitrary point on the screen for
|
|
604
|
+
# the given application. The given point can be a CGPoint, an Array,
|
|
605
|
+
# or anything else that responds to `#to_point`.
|
|
606
|
+
#
|
|
607
|
+
# @param [#to_point]
|
|
608
|
+
# @return [AX::Element]
|
|
609
|
+
def element_at_point point, for: app
|
|
610
|
+
app.element_at point
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
##
|
|
614
|
+
# Get the top most object at an arbitrary point on the screen. The
|
|
615
|
+
# given point can be a CGPoint, an Array, or anything else that
|
|
616
|
+
# responds to `#to_point`.
|
|
617
|
+
#
|
|
618
|
+
# @param [#to_point]
|
|
619
|
+
def element_at_point point
|
|
620
|
+
system_wide.element_at point
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
##
|
|
624
|
+
# Show the "About" window for an app. Returns the window that is
|
|
625
|
+
# opened.
|
|
626
|
+
#
|
|
627
|
+
# @param [AX::Application]
|
|
628
|
+
# @return [AX::Window]
|
|
629
|
+
def show_about_window_for app
|
|
630
|
+
app.show_about_window
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
##
|
|
634
|
+
# @note This method assumes that the app has setup the standard
|
|
635
|
+
# CMD+, hotkey to open the pref window
|
|
636
|
+
#
|
|
637
|
+
# Try to open the preferences for an app. Returns the window that
|
|
638
|
+
# is opened.
|
|
639
|
+
#
|
|
640
|
+
# @param [AX::Application]
|
|
641
|
+
# @return [AX::Window]
|
|
642
|
+
def show_preferences_window_for app
|
|
643
|
+
app.show_preferences_window
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
##
|
|
647
|
+
# Scroll though a table until the given element is visible.
|
|
648
|
+
#
|
|
649
|
+
# If you need to scroll an unknown ammount of units through a scroll area
|
|
650
|
+
# you can just pass the element that you need visible and this method
|
|
651
|
+
# will scroll to it for you.
|
|
652
|
+
#
|
|
653
|
+
# @param [AX::Element]
|
|
654
|
+
# @return [Boolean]
|
|
655
|
+
def scroll_to element
|
|
656
|
+
scroll_area = element.ancestor :scroll_area
|
|
657
|
+
|
|
658
|
+
return if NSContainsRect(scroll_area.bounds, element.bounds)
|
|
659
|
+
move_mouse_to scroll_area
|
|
660
|
+
# calculate direction to scroll
|
|
661
|
+
direction = element.position.y > scroll_area.position.y ? -5 : 5
|
|
662
|
+
until NSContainsRect(scroll_area.bounds, element.bounds)
|
|
663
|
+
Mouse.scroll direction
|
|
664
|
+
end
|
|
665
|
+
sleep 0.1
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
##
|
|
669
|
+
# Scroll a popup menu to an item in the menu and then move the
|
|
670
|
+
# mouse pointer to that item.
|
|
671
|
+
#
|
|
672
|
+
# @param [AX::Element]
|
|
673
|
+
# @return [Boolean]
|
|
674
|
+
def scroll_menu_to element
|
|
675
|
+
menu = element.ancestor :menu
|
|
676
|
+
move_mouse_to menu
|
|
677
|
+
|
|
678
|
+
direction = element.position.y > menu.position.y ? -5 : 5
|
|
679
|
+
until NSContainsRect(menu.bounds, element.bounds)
|
|
680
|
+
Mouse.scroll direction
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
start = Time.now
|
|
684
|
+
until Time.now - start > 5
|
|
685
|
+
# This can happen sometimes with the little arrow bars
|
|
686
|
+
# in menus covering up the menu item.
|
|
687
|
+
if element_under_mouse.kind_of? AX::Menu
|
|
688
|
+
scroll direction
|
|
689
|
+
elsif element_under_mouse != element
|
|
690
|
+
move_mouse_to element
|
|
691
|
+
else
|
|
692
|
+
break
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
end
|