AXElements 0.7.8 → 0.8.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/.yardopts +1 -10
- data/README.markdown +7 -14
- data/ext/accessibility/key_coder/key_coder.c +7 -0
- data/lib/AXElements.rb +0 -2
- data/lib/accessibility/core.rb +180 -123
- data/lib/accessibility/dsl.rb +310 -191
- data/lib/accessibility/enumerators.rb +9 -8
- data/lib/accessibility/errors.rb +7 -8
- data/lib/accessibility/factory.rb +16 -9
- data/lib/accessibility/graph.rb +68 -22
- data/lib/accessibility/highlighter.rb +86 -0
- data/lib/accessibility/pp_inspector.rb +4 -4
- data/lib/accessibility/qualifier.rb +11 -9
- data/lib/accessibility/string.rb +12 -4
- data/lib/accessibility/translator.rb +19 -10
- data/lib/accessibility/version.rb +3 -1
- data/lib/accessibility.rb +42 -17
- data/lib/ax/application.rb +90 -30
- data/lib/ax/button.rb +5 -2
- data/lib/ax/element.rb +133 -149
- data/lib/ax/pop_up_button.rb +12 -0
- data/lib/ax/radio_button.rb +5 -2
- data/lib/ax/row.rb +2 -2
- data/lib/ax/static_text.rb +5 -2
- data/lib/ax/systemwide.rb +24 -12
- data/lib/ax_elements/awesome_print.rb +13 -0
- data/lib/ax_elements/exception_workaround.rb +5 -0
- data/lib/ax_elements/nsarray_compat.rb +1 -0
- data/lib/ax_elements.rb +2 -1
- data/lib/minitest/ax_elements.rb +60 -4
- data/lib/mouse.rb +47 -20
- data/lib/rspec/expectations/ax_elements.rb +180 -88
- data/rakelib/doc.rake +7 -0
- data/test/helper.rb +2 -1
- data/test/integration/accessibility/test_dsl.rb +126 -18
- data/test/integration/accessibility/test_errors.rb +1 -1
- data/test/integration/ax/test_element.rb +17 -0
- data/test/integration/minitest/test_ax_elements.rb +33 -38
- data/test/integration/rspec/expectations/test_ax_elements.rb +68 -19
- data/test/sanity/accessibility/test_core.rb +45 -37
- data/test/sanity/accessibility/test_highlighter.rb +56 -0
- data/test/sanity/ax/test_application.rb +8 -0
- data/test/sanity/ax/test_element.rb +7 -3
- data/test/sanity/minitest/test_ax_elements.rb +2 -0
- data/test/sanity/rspec/expectations/test_ax_elements.rb +3 -0
- data/test/sanity/test_accessibility.rb +9 -0
- data/test/sanity/test_mouse.rb +2 -2
- metadata +11 -38
- data/docs/AccessibilityTips.markdown +0 -119
- data/docs/Acting.markdown +0 -340
- data/docs/Debugging.markdown +0 -165
- data/docs/Inspecting.markdown +0 -261
- data/docs/KeyboardEvents.markdown +0 -122
- data/docs/NewBehaviour.markdown +0 -151
- data/docs/Notifications.markdown +0 -271
- data/docs/Searching.markdown +0 -250
- data/docs/TestingExtensions.markdown +0 -52
- data/docs/images/all_the_buttons.jpg +0 -0
- data/docs/images/next_version.png +0 -0
- data/docs/images/ui_hierarchy.dot +0 -34
- data/docs/images/ui_hierarchy.png +0 -0
- data/lib/accessibility/debug.rb +0 -164
- data/test/integration/accessibility/test_debug.rb +0 -44
- data/test/sanity/accessibility/test_debug.rb +0 -63
data/lib/accessibility/dsl.rb
CHANGED
@@ -5,18 +5,16 @@ require 'ax/element'
|
|
5
5
|
require 'ax/application'
|
6
6
|
require 'ax/systemwide'
|
7
7
|
require 'accessibility'
|
8
|
-
require 'accessibility/
|
8
|
+
require 'accessibility/enumerators'
|
9
9
|
|
10
10
|
##
|
11
|
-
# @todo Allow the animation duration to be overridden for Mouse stuff?
|
12
|
-
#
|
13
11
|
# DSL methods for AXElements.
|
14
12
|
#
|
15
13
|
# The idea here is to pull actions out from an object and put them
|
16
14
|
# in front of object to give AXElements more of a DSL feel to make
|
17
15
|
# communicating test steps more clear. See the
|
18
|
-
#
|
19
|
-
#
|
16
|
+
# [Acting tutorial](http://github.com/Marketcircle/AXElements/wiki/Acting)
|
17
|
+
# for a more in depth tutorial on using this module.
|
20
18
|
module Accessibility::DSL
|
21
19
|
|
22
20
|
|
@@ -113,6 +111,23 @@ module Accessibility::DSL
|
|
113
111
|
element.perform :cancel
|
114
112
|
end
|
115
113
|
|
114
|
+
##
|
115
|
+
# @note This method overrides `Kernel#raise` so we have to check the
|
116
|
+
# class of the first argument to decide which code path to take.
|
117
|
+
#
|
118
|
+
# Try to perform the `raise` action on the given element.
|
119
|
+
#
|
120
|
+
# @overload raise element
|
121
|
+
# @param [AX::Element] element
|
122
|
+
# @return [Boolean]
|
123
|
+
#
|
124
|
+
# @overload raise exception[, message[, backtrace]]
|
125
|
+
# The normal way to raise an exception.
|
126
|
+
def raise *args
|
127
|
+
arg = args.first
|
128
|
+
arg.kind_of?(AX::Element) ? arg.perform(:raise) : super(*args)
|
129
|
+
end
|
130
|
+
|
116
131
|
##
|
117
132
|
# Tell an app to hide itself.
|
118
133
|
#
|
@@ -123,8 +138,7 @@ module Accessibility::DSL
|
|
123
138
|
end
|
124
139
|
|
125
140
|
##
|
126
|
-
# Tell an app to unhide itself.
|
127
|
-
# focused.
|
141
|
+
# Tell an app to unhide itself.
|
128
142
|
#
|
129
143
|
# @param [AX::Application]
|
130
144
|
# @return [Boolean]
|
@@ -143,16 +157,16 @@ module Accessibility::DSL
|
|
143
157
|
end
|
144
158
|
|
145
159
|
##
|
146
|
-
# Find the application with the given bundle identifier.
|
147
|
-
#
|
148
|
-
# launched.
|
160
|
+
# Find the application with the given bundle identifier. If the
|
161
|
+
# application is not already running, it will be launched.
|
149
162
|
#
|
150
163
|
# @example
|
151
164
|
#
|
152
|
-
#
|
165
|
+
# app_with_bundle_identifier 'com.apple.finder'
|
166
|
+
# launch 'com.apple.mail'
|
153
167
|
#
|
154
168
|
# @param [String]
|
155
|
-
# @return [AX::Application]
|
169
|
+
# @return [AX::Application,nil]
|
156
170
|
def app_with_bundle_identifier id
|
157
171
|
Accessibility.application_with_bundle_identifier id
|
158
172
|
end
|
@@ -160,7 +174,9 @@ module Accessibility::DSL
|
|
160
174
|
alias_method :launch, :app_with_bundle_identifier
|
161
175
|
|
162
176
|
##
|
163
|
-
# Find the application with the given name.
|
177
|
+
# Find the application with the given name. If the application
|
178
|
+
# is not already running, it will NOT be launched and this
|
179
|
+
# method will return `nil`.
|
164
180
|
#
|
165
181
|
# @example
|
166
182
|
#
|
@@ -173,7 +189,8 @@ module Accessibility::DSL
|
|
173
189
|
end
|
174
190
|
|
175
191
|
##
|
176
|
-
# Find the application with the given process identifier.
|
192
|
+
# Find the application with the given process identifier. An
|
193
|
+
# invalid PID will cause an exception to be raised.
|
177
194
|
#
|
178
195
|
# @example
|
179
196
|
#
|
@@ -186,27 +203,9 @@ module Accessibility::DSL
|
|
186
203
|
end
|
187
204
|
|
188
205
|
##
|
189
|
-
#
|
190
|
-
#
|
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.
|
206
|
+
# Focus an element on the screen, but only if it can be directly
|
207
|
+
# focused. It is safe to pass any element into this method as nothing
|
208
|
+
# will happen if it does not have a writable focused state attribute.
|
210
209
|
#
|
211
210
|
# @param [AX::Element]
|
212
211
|
def set_focus_to element
|
@@ -218,8 +217,8 @@ module Accessibility::DSL
|
|
218
217
|
# Set the value of an attribute on an element.
|
219
218
|
#
|
220
219
|
# This method will try to set focus to the element first; this is
|
221
|
-
# to
|
222
|
-
# to have focus before a user could change the value.
|
220
|
+
# to compensate for cases where app developers assumed an element
|
221
|
+
# would have to have focus before a user could change the value.
|
223
222
|
#
|
224
223
|
# @overload set element, attribute_name: new_value
|
225
224
|
# Set a specified attribute to a new value
|
@@ -228,7 +227,7 @@ module Accessibility::DSL
|
|
228
227
|
#
|
229
228
|
# @example
|
230
229
|
#
|
231
|
-
# set text_field, selected_text_range:
|
230
|
+
# set text_field, selected_text_range: 1..10
|
232
231
|
#
|
233
232
|
# @overload set element, new_value
|
234
233
|
# Set the `value` attribute to a new value
|
@@ -240,7 +239,6 @@ module Accessibility::DSL
|
|
240
239
|
# set text_field, 'Mark Rada'
|
241
240
|
# set radio_button, 1
|
242
241
|
#
|
243
|
-
# @return [nil] do not rely on a return value
|
244
242
|
def set element, change
|
245
243
|
set_focus_to element
|
246
244
|
|
@@ -255,12 +253,17 @@ module Accessibility::DSL
|
|
255
253
|
# Simulate keyboard input by typing out the given string. To learn
|
256
254
|
# more about how to encode modifier keys (e.g. Command), see the
|
257
255
|
# dedicated documentation page on
|
258
|
-
#
|
256
|
+
# [Keyboard Events](http://github.com/Marketcircle/AXElements/wiki/Keyboarding)
|
257
|
+
# wiki page.
|
259
258
|
#
|
260
259
|
# @overload type string
|
261
260
|
# Send input to the currently focused application
|
262
261
|
# @param [#to_s]
|
263
262
|
#
|
263
|
+
# @example
|
264
|
+
#
|
265
|
+
# type "Hello, world!"
|
266
|
+
#
|
264
267
|
# @overload type string, app
|
265
268
|
# Send input to a specific application
|
266
269
|
# @param [#to_s]
|
@@ -270,6 +273,7 @@ module Accessibility::DSL
|
|
270
273
|
sleep 0.1
|
271
274
|
app.type string.to_s
|
272
275
|
end
|
276
|
+
alias_method :type_string, :type
|
273
277
|
|
274
278
|
##
|
275
279
|
# Navigate the menu bar menus for the given application and select
|
@@ -289,75 +293,16 @@ module Accessibility::DSL
|
|
289
293
|
end
|
290
294
|
|
291
295
|
|
292
|
-
# @group Notifications
|
293
|
-
|
294
|
-
##
|
295
|
-
# Register for a notification from a specific element.
|
296
|
-
#
|
297
|
-
# @param [#to_s]
|
298
|
-
# @param [Array(#to_s,AX::Element)]
|
299
|
-
def register_for notif, from: element, &block
|
300
|
-
@registered_elements ||= []
|
301
|
-
@registered_elements << element
|
302
|
-
element.on_notification notif, &block
|
303
|
-
end
|
304
|
-
|
305
|
-
##
|
306
|
-
# @deprecated This API exists for backwards compatability only
|
307
|
-
#
|
308
|
-
# Register for a notification from a specific element.
|
309
|
-
#
|
310
|
-
# @param [AX::Element]
|
311
|
-
# @param [String]
|
312
|
-
def register_for_notification element, notif, &block
|
313
|
-
register_for notif, from: element, &block
|
314
|
-
end
|
315
|
-
|
316
|
-
##
|
317
|
-
# Pause script execution until notification that has been registered
|
318
|
-
# for is received or the full timeout period has passed.
|
319
|
-
#
|
320
|
-
# If the script is unpaused because of a timeout, then it is assumed
|
321
|
-
# that the notification was never received and all notification
|
322
|
-
# registrations will be unregistered to avoid future complications.
|
323
|
-
#
|
324
|
-
# @param [Float] timeout number of seconds to wait for a notification
|
325
|
-
# @return [Boolean]
|
326
|
-
def wait_for_notification timeout = 10.0
|
327
|
-
# We use RunInMode because it has timeout functionality
|
328
|
-
case CFRunLoopRunInMode(KCFRunLoopDefaultMode, timeout, false)
|
329
|
-
when KCFRunLoopRunStopped then true
|
330
|
-
when KCFRunLoopRunTimedOut then false.tap { |_| unregister_notifications }
|
331
|
-
when KCFRunLoopFinished then
|
332
|
-
raise RuntimeError, 'The run loop was not configured properly'
|
333
|
-
when KCFRunLoopRunHandledSource then
|
334
|
-
raise RuntimeError, 'Did you start your own run loop?'
|
335
|
-
else
|
336
|
-
raise 'You just found a bug, might be yours, or OS X, or MacRuby...'
|
337
|
-
end
|
338
|
-
end
|
339
|
-
|
340
|
-
##
|
341
|
-
# Undo _all_ notification registries.
|
342
|
-
def unregister_notifications
|
343
|
-
return unless @registered_elements
|
344
|
-
@registered_elements.each do |element|
|
345
|
-
element.unregister_notifications
|
346
|
-
end
|
347
|
-
@registered_elements = []
|
348
|
-
end
|
349
|
-
|
350
|
-
|
351
296
|
# @group Polling
|
352
297
|
|
353
298
|
##
|
354
|
-
# Simply wait around for something to show up. This method is similar
|
355
|
-
# performing an explicit search on an element except that the search
|
356
|
-
# take two extra options which can control
|
357
|
-
#
|
358
|
-
#
|
359
|
-
# that what you are waiting for is a child of the parent
|
360
|
-
# distant descendant.
|
299
|
+
# Simply wait around for something to show up. This method is similar
|
300
|
+
# to performing an explicit search on an element except that the search
|
301
|
+
# filters take two extra options which can control the timeout period
|
302
|
+
# and the search subtree. You __MUST__ supply either the parent or
|
303
|
+
# ancestor option to specify where to search from. Searching from the
|
304
|
+
# parent implies that what you are waiting for is a child of the parent
|
305
|
+
# and not a more distant descendant.
|
361
306
|
#
|
362
307
|
# This is an alternative to using the notifications system. It is far
|
363
308
|
# easier to use than notifications in most cases, but it will perform
|
@@ -375,18 +320,18 @@ module Accessibility::DSL
|
|
375
320
|
# wait_for :a_million_dollars, ancestor: fruit_basket, timeout: 1000000
|
376
321
|
#
|
377
322
|
# @param [#to_s]
|
378
|
-
# @param [Hash]
|
379
|
-
# @
|
380
|
-
# @
|
381
|
-
# @
|
323
|
+
# @param [Hash] filters
|
324
|
+
# @option filters [Number] :timeout (5) timeout in seconds
|
325
|
+
# @option filters [AX::Element] :parent
|
326
|
+
# @option filters [AX::Element] :ancestor
|
382
327
|
# @return [AX::Element,nil]
|
383
|
-
def wait_for element,
|
384
|
-
if
|
385
|
-
wait_for_descendant element,
|
386
|
-
elsif
|
387
|
-
wait_for_child element,
|
328
|
+
def wait_for element, filters = {}, &block
|
329
|
+
if filters.has_key? :ancestor
|
330
|
+
wait_for_descendant element, filters.delete(:ancestor), filters, &block
|
331
|
+
elsif filters.has_key? :parent
|
332
|
+
wait_for_child element, filters.delete(:parent), filters, &block
|
388
333
|
else
|
389
|
-
raise ArgumentError, 'parent/ancestor
|
334
|
+
raise ArgumentError, 'parent/ancestor filter required'
|
390
335
|
end
|
391
336
|
end
|
392
337
|
|
@@ -395,17 +340,19 @@ module Accessibility::DSL
|
|
395
340
|
# The options you pass to this method can be any search filter that
|
396
341
|
# you can normally use.
|
397
342
|
#
|
343
|
+
# See {#wait_for} for more details.
|
344
|
+
#
|
398
345
|
# @param [#to_s]
|
399
346
|
# @param [AX::Element]
|
400
347
|
# @param [Hash]
|
401
348
|
# @return [AX::Element,nil]
|
402
|
-
def wait_for_descendant descendant, ancestor,
|
403
|
-
timeout =
|
349
|
+
def wait_for_descendant descendant, ancestor, filters, &block
|
350
|
+
timeout = filters.delete(:timeout) || 5
|
404
351
|
start = Time.now
|
405
352
|
until Time.now - start > timeout
|
406
|
-
result = ancestor.search(descendant,
|
353
|
+
result = ancestor.search(descendant, filters, &block)
|
407
354
|
return result unless result.blank?
|
408
|
-
sleep 0.
|
355
|
+
sleep 0.1
|
409
356
|
end
|
410
357
|
nil
|
411
358
|
end
|
@@ -414,49 +361,105 @@ module Accessibility::DSL
|
|
414
361
|
##
|
415
362
|
# @note This is really just an optimized case of
|
416
363
|
# {#wait_for_descendant} when you know what you are waiting
|
417
|
-
# for is a child of a particular element.
|
364
|
+
# for is a child of a particular element. Use
|
365
|
+
# {#wait_for_descendant} if you are unsure of the relationship.
|
418
366
|
#
|
419
367
|
# Wait around for particular element and then return that element.
|
420
|
-
# The parent
|
368
|
+
# The parent argument must be the parent of the element you are
|
421
369
|
# waiting for, this method will not look further down the hierarchy.
|
422
370
|
# The options you pass to this method can be any search filter that
|
423
371
|
# you can normally use.
|
424
372
|
#
|
373
|
+
# See {#wait_for} for more details.
|
374
|
+
#
|
425
375
|
# @param [#to_s]
|
426
376
|
# @param [AX::Element]
|
427
377
|
# @param [Hash]
|
428
378
|
# @return [AX::Element,nil]
|
429
|
-
def wait_for_child child, parent,
|
430
|
-
timeout =
|
379
|
+
def wait_for_child child, parent, filters, &block
|
380
|
+
timeout = filters.delete(:timeout) || 5
|
431
381
|
start = Time.now
|
432
|
-
q = Accessibility::Qualifier.new(child,
|
382
|
+
q = Accessibility::Qualifier.new(child, filters, &block)
|
433
383
|
until Time.now - start > timeout
|
434
384
|
result = parent.children.find { |x| q.qualifies? x }
|
435
385
|
return result unless result.blank?
|
436
|
-
sleep 0.
|
386
|
+
sleep 0.1
|
437
387
|
end
|
438
388
|
nil
|
439
389
|
end
|
440
390
|
|
391
|
+
##
|
392
|
+
# Simply wait for an element to disappear. Optionally wait for the
|
393
|
+
# element to appear first.
|
394
|
+
#
|
395
|
+
# Like {#wait_for}, you can pass any search filters that you normally
|
396
|
+
# would, including blocks. However, this method also supports the
|
397
|
+
# ability to pass an {AX::Element} and simply wait for it to become
|
398
|
+
# invalid.
|
399
|
+
#
|
400
|
+
# An example usage would be typing into a search field and then
|
401
|
+
# waiting for the busy indicator to disappear and indicate that
|
402
|
+
# all search results have been returned.
|
403
|
+
#
|
404
|
+
# @overload wait_for_invalidation_of element
|
405
|
+
# @param [AX::Element]
|
406
|
+
# @param [Hash] filters
|
407
|
+
# @option filters [Number] :timeout (5) in seconds
|
408
|
+
# @return [Boolean]
|
409
|
+
#
|
410
|
+
# @example
|
411
|
+
#
|
412
|
+
# wait_for_invalidation_of table.row(static_text: { value: 'Cake' })
|
413
|
+
#
|
414
|
+
# @overload wait_for_invalidation_of kind, filters = {}, &block
|
415
|
+
# @param [#to_s]
|
416
|
+
# @param [Hash] filters
|
417
|
+
# @option filters [Number] :timeout (5) in seconds
|
418
|
+
# @return [Boolean]
|
419
|
+
#
|
420
|
+
# @example
|
421
|
+
#
|
422
|
+
# wait_for_invalidation_of :row, parent: table, static_text: { value: 'Cake' }
|
423
|
+
#
|
424
|
+
# @return [Boolean]
|
425
|
+
def wait_for_invalidation_of element, filters = {}, &block
|
426
|
+
timeout = filters[:timeout] || 5
|
427
|
+
start = Time.now
|
428
|
+
|
429
|
+
unless element.kind_of? AX::Element
|
430
|
+
element = wait_for element, filters, &block
|
431
|
+
# this is a tricky situation,
|
432
|
+
return true unless element
|
433
|
+
end
|
434
|
+
|
435
|
+
until Time.now - start > timeout
|
436
|
+
return true if element.invalid?
|
437
|
+
sleep 0.1
|
438
|
+
end
|
439
|
+
false
|
440
|
+
end
|
441
|
+
alias_method :wait_for_invalidation, :wait_for_invalidation_of
|
442
|
+
alias_method :wait_for_invalid, :wait_for_invalidation_of
|
443
|
+
|
441
444
|
|
442
|
-
# @group Mouse
|
445
|
+
# @group Mouse Manipulation
|
443
446
|
|
444
447
|
##
|
445
|
-
# Move the mouse cursor to the given point on the screen.
|
448
|
+
# Move the mouse cursor to the given point or object on the screen.
|
446
449
|
#
|
447
450
|
# @example
|
448
451
|
#
|
449
452
|
# move_mouse_to button
|
450
453
|
# move_mouse_to [344, 516]
|
451
|
-
# move_mouse_to
|
454
|
+
# move_mouse_to CGPoint.new(100, 100)
|
452
455
|
#
|
453
456
|
# @param [#to_point]
|
454
457
|
# @param [Hash] opts
|
455
|
-
# @option opts [Number] :duration
|
456
|
-
# @option opts [Number] :wait
|
458
|
+
# @option opts [Number] :duration (0.2) in seconds
|
459
|
+
# @option opts [Number] :wait (0.2) in seconds
|
457
460
|
def move_mouse_to arg, opts = {}
|
458
461
|
duration = opts[:duration] || 0.2
|
459
|
-
if Accessibility
|
462
|
+
if Accessibility.debug? && arg.respond_to?(:bounds)
|
460
463
|
highlight arg, timeout: duration, color: NSColor.orangeColor
|
461
464
|
end
|
462
465
|
Mouse.move_to arg.to_point, duration
|
@@ -469,25 +472,24 @@ module Accessibility::DSL
|
|
469
472
|
#
|
470
473
|
# There are many reasons why you would want to cause a drag event
|
471
474
|
# with the mouse. Perhaps you want to drag an object to another
|
472
|
-
# place, or maybe you want to
|
475
|
+
# place, or maybe you want to select a group of objects on the screen.
|
476
|
+
#
|
477
|
+
# @example
|
478
|
+
#
|
479
|
+
# drag_mouse_to [100,100]
|
480
|
+
# drag_mouse_to drop_zone, from: desktop_icon
|
473
481
|
#
|
474
482
|
# @param [#to_point]
|
483
|
+
# @param [Hash] opts
|
484
|
+
# @option opts [#to_point] :from a point to move to before dragging
|
485
|
+
# @option opts [Number] :duration (0.2) in seconds
|
486
|
+
# @option opts [Number] :wait (0.2) in seconds
|
475
487
|
def drag_mouse_to arg, opts = {}
|
488
|
+
move_mouse_to opts[:from] if opts[:from]
|
476
489
|
Mouse.drag_to arg.to_point, (opts[:duration] || 0.2)
|
477
490
|
sleep(opts[:wait] || 0.2)
|
478
491
|
end
|
479
492
|
|
480
|
-
##
|
481
|
-
# Move the mouse to the first point and then drag to the second point.
|
482
|
-
#
|
483
|
-
# @param [#to_point]
|
484
|
-
# @param [#to_point]
|
485
|
-
def drag arg1, to: arg2
|
486
|
-
move_mouse_to obj
|
487
|
-
drag_mouse_to obj2
|
488
|
-
end
|
489
|
-
|
490
|
-
|
491
493
|
##
|
492
494
|
# @todo Need to expose the units option? Would allow scrolling by pixel.
|
493
495
|
#
|
@@ -509,13 +511,27 @@ module Accessibility::DSL
|
|
509
511
|
##
|
510
512
|
# Perform a regular click.
|
511
513
|
#
|
512
|
-
# If
|
514
|
+
# If a parameter is provided then the mouse will move to that point
|
513
515
|
# first; the argument must respond to `#to_point`.
|
514
516
|
#
|
517
|
+
# If a block is given, it will be yielded to between the click down
|
518
|
+
# and click up event.
|
519
|
+
#
|
520
|
+
# @example
|
521
|
+
#
|
522
|
+
# click
|
523
|
+
# click window.close_button
|
524
|
+
#
|
515
525
|
# @param [#to_point]
|
516
526
|
def click obj = nil, wait = 0.2
|
517
527
|
move_mouse_to obj, wait: 0 if obj
|
518
|
-
|
528
|
+
if block_given?
|
529
|
+
Mouse.click_down
|
530
|
+
yield
|
531
|
+
Mouse.click_up
|
532
|
+
else
|
533
|
+
Mouse.click
|
534
|
+
end
|
519
535
|
sleep wait
|
520
536
|
end
|
521
537
|
|
@@ -547,36 +563,122 @@ module Accessibility::DSL
|
|
547
563
|
end
|
548
564
|
|
549
565
|
|
550
|
-
# @group Debug
|
566
|
+
# @group Debug Helpers
|
551
567
|
|
552
|
-
|
553
|
-
|
568
|
+
##
|
569
|
+
# Highlight an element on screen. You can optionally specify the
|
570
|
+
# highlight colour or pass a timeout to automatically have the
|
571
|
+
# highlighter disappear.
|
572
|
+
#
|
573
|
+
# The highlighter is actually a window, so if you do not set a
|
574
|
+
# timeout, you will need to call `#stop` or `#close` on the returned
|
575
|
+
# highlighter object in order to get rid of the highlighter.
|
576
|
+
#
|
577
|
+
# You could use this method to highlight an arbitrary number of
|
578
|
+
# elements on screen, with a rainbow of colours. Great for debugging.
|
579
|
+
#
|
580
|
+
# @example
|
581
|
+
#
|
582
|
+
# highlighter = highlight window.outline
|
583
|
+
# # wait a few seconds...
|
584
|
+
# highlighter.stop
|
585
|
+
#
|
586
|
+
# # highlighter automatically turns off after 5 seconds
|
587
|
+
# highlight window.outline.row, colour: NSColor.greenColor, timeout: 5
|
588
|
+
#
|
589
|
+
# @param [#bounds]
|
590
|
+
# @param [Hash] opts
|
591
|
+
# @option opts [Number] :timeout
|
592
|
+
# @option opts [NSColor] :colour (NSColor.magentaColor)
|
593
|
+
# @return [Accessibility::Highlighter]
|
594
|
+
def highlight obj, opts = {}
|
595
|
+
require 'accessibility/highlighter'
|
596
|
+
Accessibility::Highlighter.new obj.bounds, opts
|
554
597
|
end
|
555
598
|
|
556
|
-
|
557
|
-
|
599
|
+
##
|
600
|
+
# Get the dump of the subtree of children and descendants for the given
|
601
|
+
# element. Each generation down the tree will be indented another level,
|
602
|
+
# and each element will be inspected.
|
603
|
+
#
|
604
|
+
# @example
|
605
|
+
#
|
606
|
+
# puts subtree_for app
|
607
|
+
#
|
608
|
+
# @return [String]
|
609
|
+
def subtree_for element
|
610
|
+
element.inspect_subtree
|
558
611
|
end
|
612
|
+
alias_method :subtree, :subtree_for
|
559
613
|
|
560
|
-
|
561
|
-
|
562
|
-
|
614
|
+
##
|
615
|
+
# @note You will need to have GraphViz command line tools installed
|
616
|
+
# in order for this to work.
|
617
|
+
#
|
618
|
+
# Make and open a `dot` format graph of the tree, meant for graphing
|
619
|
+
# with GraphViz.
|
620
|
+
#
|
621
|
+
# @example
|
622
|
+
#
|
623
|
+
# graph app.main_window
|
624
|
+
#
|
625
|
+
# @param [AX::Element]
|
626
|
+
# @return [String] path to the saved image
|
627
|
+
def graph element
|
628
|
+
require 'accessibility/graph'
|
629
|
+
graph = Accessibility::Graph.new(element)
|
630
|
+
graph.build!
|
631
|
+
|
632
|
+
require 'tempfile'
|
633
|
+
file = Tempfile.new('graph')
|
634
|
+
File.open(file.path, 'w') do |fd| fd.write graph.to_dot end
|
635
|
+
`dot -Tpng #{file.path} > #{file.path}.png`
|
636
|
+
`open #{file.path}.png`
|
637
|
+
|
638
|
+
file.path
|
563
639
|
end
|
564
640
|
|
565
641
|
##
|
566
|
-
#
|
642
|
+
# Take a screen shot and save it to disk. If a file name and path are
|
643
|
+
# not given then default values will be used; given paths will be
|
644
|
+
# expanded automatically.A timestamp and file extension will always
|
645
|
+
# automatically be appended to the file name.
|
567
646
|
#
|
568
|
-
#
|
569
|
-
# GraphViz.
|
647
|
+
# @example
|
570
648
|
#
|
571
|
-
#
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
649
|
+
# screenshot
|
650
|
+
# # => "~/Desktop/AXElements-ScreenShot-20120422184650.png"
|
651
|
+
#
|
652
|
+
# screenshot app.title
|
653
|
+
# # => "~/Desktop/Safari-20120422184650.png"
|
654
|
+
#
|
655
|
+
# screenshot app.title, "/Volumes/SecretStash"
|
656
|
+
# # => "/Volumes/SecretStash/Safari-20120422184650.png"
|
657
|
+
#
|
658
|
+
# @param [#to_s]
|
659
|
+
# @param [#to_s]
|
660
|
+
# @return [String] path to the screenshot
|
661
|
+
def screenshot name = "AXElements-ScreenShot", dir = '~/Desktop'
|
662
|
+
dir = File.expand_path dir.to_s
|
663
|
+
file = "#{dir}/#{name}-#{Time.now.strftime '%Y%m%d%H%M%S'}.png"
|
664
|
+
|
665
|
+
cg_image = CGWindowListCreateImage(CGRectInfinite,
|
666
|
+
KCGWindowListOptionOnScreenOnly,
|
667
|
+
KCGNullWindowID,
|
668
|
+
KCGWindowImageDefault)
|
669
|
+
NSBitmapImageRep
|
670
|
+
.alloc
|
671
|
+
.initWithCGImage(cg_image)
|
672
|
+
.representationUsingType(NSPNGFileType, properties: nil)
|
673
|
+
.writeToFile(file, atomically: false)
|
674
|
+
|
675
|
+
file
|
576
676
|
end
|
677
|
+
alias_method :capture_screen, :screenshot
|
678
|
+
alias_method :shoot_screen, :screenshot
|
577
679
|
|
578
680
|
|
579
|
-
# @group
|
681
|
+
# @group Macros
|
580
682
|
|
581
683
|
##
|
582
684
|
# Convenience for `AX::SystemWide.new`.
|
@@ -586,12 +688,10 @@ module Accessibility::DSL
|
|
586
688
|
AX::SystemWide.new
|
587
689
|
end
|
588
690
|
|
589
|
-
|
590
|
-
# @group Macros
|
591
|
-
|
592
691
|
##
|
593
|
-
#
|
594
|
-
#
|
692
|
+
# Return the top most element at the current mouse position.
|
693
|
+
#
|
694
|
+
# See {#element_at_point} for more details.
|
595
695
|
#
|
596
696
|
# @return [AX::Element]
|
597
697
|
def element_under_mouse
|
@@ -603,20 +703,24 @@ module Accessibility::DSL
|
|
603
703
|
# the given application. The given point can be a CGPoint, an Array,
|
604
704
|
# or anything else that responds to `#to_point`.
|
605
705
|
#
|
606
|
-
#
|
607
|
-
#
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
#
|
614
|
-
#
|
615
|
-
#
|
706
|
+
# Optionally, you can look for the top-most element for a specific
|
707
|
+
# application by passing an {AX::Application} object using the `for:`
|
708
|
+
# key.
|
709
|
+
#
|
710
|
+
# @example
|
711
|
+
#
|
712
|
+
# element_at [100, 456]
|
713
|
+
# element_at CGPoint.new(33, 45), for: safari
|
714
|
+
#
|
715
|
+
# element_at window # find out what is in the middle of the window
|
616
716
|
#
|
617
717
|
# @param [#to_point]
|
618
|
-
|
619
|
-
|
718
|
+
# @param [Hash] opts
|
719
|
+
# @option opts [AX::Application] :for
|
720
|
+
# @return [AX::Element]
|
721
|
+
def element_at_point point, opts = {}
|
722
|
+
base = opts[:for] || system_wide
|
723
|
+
base.element_at point
|
620
724
|
end
|
621
725
|
|
622
726
|
##
|
@@ -643,14 +747,18 @@ module Accessibility::DSL
|
|
643
747
|
end
|
644
748
|
|
645
749
|
##
|
646
|
-
# Scroll though a
|
750
|
+
# Scroll though a scroll area until the given element is visible.
|
647
751
|
#
|
648
752
|
# If you need to scroll an unknown ammount of units through a scroll area
|
649
753
|
# you can just pass the element that you need visible and this method
|
650
754
|
# will scroll to it for you.
|
651
755
|
#
|
756
|
+
# @example
|
757
|
+
#
|
758
|
+
# scroll_to table.rows.last
|
759
|
+
#
|
652
760
|
# @param [AX::Element]
|
653
|
-
# @return [
|
761
|
+
# @return [void]
|
654
762
|
def scroll_to element
|
655
763
|
scroll_area = element.ancestor :scroll_area
|
656
764
|
|
@@ -663,29 +771,40 @@ module Accessibility::DSL
|
|
663
771
|
end
|
664
772
|
sleep 0.1
|
665
773
|
end
|
774
|
+
alias_method :scroll_to_visible, :scroll_to
|
666
775
|
|
667
776
|
##
|
668
|
-
# Scroll a
|
669
|
-
#
|
777
|
+
# Scroll a menu to an item in the menu and then move the mouse
|
778
|
+
# pointer to that item.
|
670
779
|
#
|
671
|
-
# @
|
672
|
-
#
|
780
|
+
# @example
|
781
|
+
#
|
782
|
+
# scroll_menu_to menu.element(title: "Expensive Cake")
|
783
|
+
#
|
784
|
+
# @param [AX:]
|
785
|
+
# @return [void]
|
673
786
|
def scroll_menu_to element
|
674
787
|
menu = element.ancestor :menu
|
675
788
|
move_mouse_to menu
|
676
789
|
|
677
|
-
|
790
|
+
row_height = menu.menu_item.size.height
|
791
|
+
point = menu.position
|
792
|
+
point.x += menu.size.width / 2
|
793
|
+
point.y += if element.position.y > menu.position.y
|
794
|
+
menu.size.height - (row_height * 0.1)
|
795
|
+
else
|
796
|
+
row_height * 0.1
|
797
|
+
end
|
798
|
+
|
678
799
|
until NSContainsRect(menu.bounds, element.bounds)
|
679
|
-
|
800
|
+
move_mouse_to point
|
680
801
|
end
|
681
802
|
|
682
803
|
start = Time.now
|
683
804
|
until Time.now - start > 5
|
684
805
|
# This can happen sometimes with the little arrow bars
|
685
806
|
# in menus covering up the menu item.
|
686
|
-
if element_under_mouse
|
687
|
-
scroll direction
|
688
|
-
elsif element_under_mouse != element
|
807
|
+
if element_under_mouse != element
|
689
808
|
move_mouse_to element
|
690
809
|
else
|
691
810
|
break
|